forms.gno
5.50 Kb · 245 lines
1package test
2
3import (
4 "chain"
5 "chain/runtime"
6 "strings"
7
8 "gno.land/p/nt/avl/v0"
9 "gno.land/p/nt/ownable/v0"
10 "gno.land/p/nt/seqid/v0"
11 "gno.land/p/nt/ufmt/v0"
12)
13
14var (
15 forms = avl.NewTree() // form id -> *Form
16 nextFormID seqid.ID
17 nextRespID seqid.ID
18)
19
20// Create publishes a new form and returns its ID.
21//
22// Fields are described by four parallel, pipe-separated strings so the call
23// stays usable from gnokey and gnoweb without structured arguments:
24//
25// labels: "Name|Why gno.land?|Server type"
26// kinds: "text|textarea|select"
27// required: "1|1|0"
28// options: "||cloud,on-prem,data-center" (comma-separated; only select uses it)
29//
30// deadline is a chain height after which the form stops accepting responses,
31// or 0 for none. onePerAddr limits each address to a single response.
32func Create(cur realm, title, description, labels, kinds, required, options string, onePerAddr bool, deadline int64) string {
33 caller := mustUserCaller(cur)
34
35 title = strings.TrimSpace(title)
36 description = strings.TrimSpace(description)
37
38 if title == "" {
39 panic(ErrEmptyTitle)
40 }
41 if len(title) > MaxTitleLen {
42 panic(ErrTitleTooLong)
43 }
44 if len(description) > MaxDescriptionLen {
45 panic(ErrDescriptionTooLong)
46 }
47
48 height := runtime.ChainHeight()
49 if deadline < 0 || (deadline > 0 && deadline <= height) {
50 panic(ErrBadDeadline)
51 }
52
53 fields := parseFields(labels, kinds, required, options)
54
55 id := nextFormID.Next()
56 f := &Form{
57 ID: id,
58 Title: title,
59 Description: description,
60 Fields: fields,
61 OnePerAddr: onePerAddr,
62 Deadline: deadline,
63 CreatedAt: height,
64 owner: ownable.NewWithAddress(caller),
65 responses: avl.NewTree(),
66 byAddr: avl.NewTree(),
67 }
68
69 forms.Set(id.String(), f)
70
71 chain.Emit("FormCreated", "id", id.String(), "owner", caller.String(), "title", title)
72
73 return id.String()
74}
75
76// Close stops a form from accepting responses. Owner only.
77func Close(cur realm, id string) {
78 f := mustGetForm(id)
79 f.owner.AssertOwnedBy(mustUserCaller(cur))
80
81 if f.Closed {
82 panic(ErrFormClosed)
83 }
84
85 f.Closed = true
86
87 chain.Emit("FormClosed", "id", id)
88}
89
90// Reopen lets a closed form accept responses again. Owner only. A form
91// whose deadline has passed stays closed regardless.
92func Reopen(cur realm, id string) {
93 f := mustGetForm(id)
94 f.owner.AssertOwnedBy(mustUserCaller(cur))
95
96 if !f.Closed {
97 panic(ErrFormOpen)
98 }
99 if f.Deadline > 0 && runtime.ChainHeight() >= f.Deadline {
100 panic(ErrDeadlinePassed)
101 }
102
103 f.Closed = false
104
105 chain.Emit("FormReopened", "id", id)
106}
107
108// TransferOwnership hands a form to another address. Owner only.
109func TransferOwnership(cur realm, id string, to address) {
110 f := mustGetForm(id)
111 f.owner.AssertOwnedBy(mustUserCaller(cur))
112
113 if err := f.owner.TransferOwnership(0, cur, to); err != nil {
114 panic(err)
115 }
116
117 chain.Emit("FormTransferred", "id", id, "to", to.String())
118}
119
120// GetForm returns a form by ID.
121func GetForm(id string) (*Form, bool) {
122 raw := forms.Get(id)
123 if raw == nil {
124 return nil, false
125 }
126
127 return raw.(*Form), true
128}
129
130// ResponseCount returns the number of responses a form has, or 0 if the
131// form does not exist.
132func ResponseCount(id string) int {
133 f, ok := GetForm(id)
134 if !ok {
135 return 0
136 }
137
138 return f.ResponseCount()
139}
140
141// FormCount returns how many forms exist.
142func FormCount() int {
143 return forms.Size()
144}
145
146func mustGetForm(id string) *Form {
147 f, ok := GetForm(id)
148 if !ok {
149 panic(ErrFormNotFound)
150 }
151
152 return f
153}
154
155// mustUserCaller returns the address of the user account that made the
156// call. Only user accounts may create forms or respond: a realm submitting
157// on someone's behalf would be attributed to the realm, which is never what
158// a form wants.
159func mustUserCaller(cur realm) address {
160 if !cur.IsCurrent() {
161 panic("realm value is not the caller's live cur")
162 }
163
164 prev := cur.Previous()
165 if !prev.IsUser() {
166 panic(ErrNotUserCall)
167 }
168
169 return prev.Address()
170}
171
172// parseFields decodes the four parallel field-spec strings.
173func parseFields(labels, kinds, required, options string) []Field {
174 labelList := splitFields(labels)
175 kindList := splitFields(kinds)
176 reqList := splitFields(required)
177 optList := splitFields(options)
178
179 n := len(labelList)
180 if n == 0 || (n == 1 && labelList[0] == "") {
181 panic(ErrNoFields)
182 }
183 if n > MaxFields {
184 panic(ErrTooManyFields)
185 }
186 if len(kindList) != n || len(reqList) != n {
187 panic(ErrFieldSpecMismatch)
188 }
189 // options may be omitted entirely when no field is a select.
190 if len(optList) != n && !(len(optList) == 1 && optList[0] == "") {
191 panic(ErrFieldSpecMismatch)
192 }
193
194 fields := make([]Field, 0, n)
195 for i := 0; i < n; i++ {
196 label := strings.TrimSpace(labelList[i])
197 if label == "" {
198 panic(ErrEmptyLabel)
199 }
200 if len(label) > MaxLabelLen {
201 panic(ufmt.Errorf("field %d: label is too long", i+1))
202 }
203
204 kind := FieldKind(strings.TrimSpace(kindList[i]))
205 switch kind {
206 case KindText, KindTextarea, KindNumber, KindSelect:
207 default:
208 panic(ErrBadFieldKind)
209 }
210
211 field := Field{
212 Label: label,
213 Kind: kind,
214 Required: strings.TrimSpace(reqList[i]) == "1",
215 }
216
217 if kind == KindSelect {
218 raw := ""
219 if len(optList) == n {
220 raw = optList[i]
221 }
222 for _, o := range strings.Split(raw, ",") {
223 o = strings.TrimSpace(o)
224 if o == "" {
225 continue
226 }
227 if len(o) > MaxOptionLen {
228 panic(ufmt.Errorf("field %d: option is too long", i+1))
229 }
230 field.Options = append(field.Options, o)
231 }
232 if len(field.Options) == 0 {
233 panic(ErrSelectNoOptions)
234 }
235 }
236
237 fields = append(fields, field)
238 }
239
240 return fields
241}
242
243func splitFields(s string) []string {
244 return strings.Split(s, "|")
245}