Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}