-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbuilder.go
More file actions
540 lines (442 loc) · 17.8 KB
/
Copy pathbuilder.go
File metadata and controls
540 lines (442 loc) · 17.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
package dyfields
import (
"strconv"
"strings"
)
// DefaultGroup is where AddField puts a field when the caller does not care
// about grouping. There is always a group, so the serialized form has exactly
// one shape instead of a grouped and an ungrouped variant.
const DefaultGroup = "default"
// Builder assembles a schema step by step. It only accumulates operations —
// every consistency check happens in Build. A half-built schema is allowed to
// be inconsistent (a condition may reference a field that is not added yet),
// otherwise callers would be forced to care about the order they build in.
type Builder struct {
version int
groups []*GroupBuilder
}
// NewBuilder returns an empty builder.
func NewBuilder() *Builder { return &Builder{version: CurrentVersion} }
// FromSchema loads an existing schema back into a builder, which is what a
// form designer needs to edit a form that is already stored.
func FromSchema(s Schema) *Builder {
b := &Builder{version: s.Version}
if b.version == 0 {
b.version = CurrentVersion
}
for _, g := range s.Groups {
if g != nil {
b.groups = append(b.groups, &GroupBuilder{g: g})
}
}
return b
}
// SetVersion sets schema_version. Its meaning is the caller's; the library only
// preserves it.
func (b *Builder) SetVersion(v int) *Builder { b.version = v; return b }
// Group returns the group with this key, creating it if needed.
func (b *Builder) Group(key string) *GroupBuilder {
for _, gb := range b.groups {
if gb.g.Key == key {
return gb
}
}
gb := &GroupBuilder{g: &Group{Key: key, Mode: ModeAlways}}
b.groups = append(b.groups, gb)
return gb
}
// AddField adds a field to the default group.
func (b *Builder) AddField(fb *FieldBuilder) *Builder {
b.Group(DefaultGroup).AddField(fb)
return b
}
// RemoveField removes a top-level field by name, from whichever group holds it.
func (b *Builder) RemoveField(name string) bool {
for _, gb := range b.groups {
for i, f := range gb.g.Fields {
if f.Name == name {
gb.g.Fields = append(gb.g.Fields[:i], gb.g.Fields[i+1:]...)
return true
}
}
}
return false
}
// RemoveGroup removes a group and every field in it.
func (b *Builder) RemoveGroup(key string) bool {
for i, gb := range b.groups {
if gb.g.Key == key {
b.groups = append(b.groups[:i], b.groups[i+1:]...)
return true
}
}
return false
}
// Field returns a pointer to a top-level field so the caller can edit it in
// place.
func (b *Builder) Field(name string) (*Field, bool) {
for _, gb := range b.groups {
for _, f := range gb.g.Fields {
if f.Name == name {
return f, true
}
}
}
return nil, false
}
// FieldAt resolves a dotted path such as "brokers.port" into a nested field.
func (b *Builder) FieldAt(path string) (*Field, bool) {
parts := strings.Split(path, ".")
cur, ok := b.Field(parts[0])
if !ok {
return nil, false
}
for _, p := range parts[1:] {
next, found := (*Field)(nil), false
for _, sub := range cur.Fields {
if sub.Name == p {
next, found = sub, true
break
}
}
if !found {
return nil, false
}
cur = next
}
return cur, true
}
// RemoveFieldAt removes a field addressed by a dotted path.
func (b *Builder) RemoveFieldAt(path string) bool {
parts := strings.Split(path, ".")
if len(parts) == 1 {
return b.RemoveField(path)
}
parent, ok := b.FieldAt(strings.Join(parts[:len(parts)-1], "."))
if !ok {
return false
}
last := parts[len(parts)-1]
for i, sub := range parent.Fields {
if sub.Name == last {
parent.Fields = append(parent.Fields[:i], parent.Fields[i+1:]...)
return true
}
}
return false
}
// MoveField moves a top-level field to another group and position. A form
// designer needs this; index is clamped rather than rejected, because dragging
// past the end of a list is a normal gesture, not an error.
func (b *Builder) MoveField(name, groupKey string, idx int) bool {
var moved *Field
for _, gb := range b.groups {
for i, f := range gb.g.Fields {
if f.Name == name {
moved = f
gb.g.Fields = append(gb.g.Fields[:i], gb.g.Fields[i+1:]...)
break
}
}
if moved != nil {
break
}
}
if moved == nil {
return false
}
dst := b.Group(groupKey)
if idx < 0 || idx > len(dst.g.Fields) {
idx = len(dst.g.Fields)
}
dst.g.Fields = append(dst.g.Fields, nil)
copy(dst.g.Fields[idx+1:], dst.g.Fields[idx:])
dst.g.Fields[idx] = moved
return true
}
// Extend merges another schema in. Groups with the same key are merged;
// colliding field names are left to Build to report, in keeping with the rule
// that the builder never fails early.
func (b *Builder) Extend(other Schema) *Builder {
for _, g := range other.Groups {
if g == nil {
continue
}
dst := b.Group(g.Key)
if dst.g.Label == "" {
dst.g.Label = g.Label
}
if g.Mode != "" && dst.g.Mode == ModeAlways {
dst.g.Mode = g.Mode
}
if dst.g.Toggle == nil {
dst.g.Toggle = g.Toggle
}
if dst.g.VisibleWhen == nil {
dst.g.VisibleWhen = g.VisibleWhen
}
dst.g.ValidWhen = append(dst.g.ValidWhen, g.ValidWhen...)
dst.g.Fields = append(dst.g.Fields, g.Fields...)
}
return b
}
// Schema assembles the schema without checking it.
func (b *Builder) Schema() Schema {
s := Schema{Version: b.version}
for _, gb := range b.groups {
s.Groups = append(s.Groups, gb.g)
}
return s
}
// Build assembles and checks the schema. The error, when there is one, is a
// SchemaErrors listing every problem at once.
func (b *Builder) Build() (Schema, error) {
s := b.Schema()
if _, err := Compile(s); err != nil {
return s, err
}
return s, nil
}
// MustBuild is Build for schemas defined in code, where a failure is a bug.
func (b *Builder) MustBuild() Schema {
s, err := b.Build()
if err != nil {
panic("dyfields: " + err.Error())
}
return s
}
// Compile builds and compiles in one step.
func (b *Builder) Compile() (*Compiled, error) { return Compile(b.Schema()) }
// ---- groups ---------------------------------------------------------------
// GroupBuilder configures one group.
type GroupBuilder struct{ g *Group }
// Group exposes the underlying group for direct edits.
func (gb *GroupBuilder) Group() *Group { return gb.g }
// Label sets the display name.
func (gb *GroupBuilder) Label(s string) *GroupBuilder { gb.g.Label = s; return gb }
// LabelI18n sets translations of the display name.
func (gb *GroupBuilder) LabelI18n(m map[string]string) *GroupBuilder { gb.g.LabelI18n = m; return gb }
// Description sets the help text.
func (gb *GroupBuilder) Description(s string) *GroupBuilder { gb.g.Description = s; return gb }
// Collapsible renders the group folded by default; its contents still exist
// and are still validated.
func (gb *GroupBuilder) Collapsible(collapsed bool) *GroupBuilder {
gb.g.Mode = ModeCollapsible
gb.g.DefaultCollapsed = collapsed
return gb
}
// Toggleable puts the group behind a switch. The switch is a real boolean
// field, declared here and nowhere else; when it is off the group is not
// validated and its values are removed.
func (gb *GroupBuilder) Toggleable(field, label string, def bool) *GroupBuilder {
gb.g.Mode = ModeToggleable
gb.g.Toggle = &Toggle{Field: field, Label: label, Default: def}
return gb
}
// VisibleWhen shows the group only when the condition holds.
func (gb *GroupBuilder) VisibleWhen(c *Condition) *GroupBuilder { gb.g.VisibleWhen = c; return gb }
// Check adds a cross-field rule that must hold while the group is active.
func (gb *GroupBuilder) Check(c *Condition, code, reason string) *GroupBuilder {
gb.g.ValidWhen = append(gb.g.ValidWhen, &Rule{When: c, Code: code, Reason: reason})
return gb
}
// AddField appends a field to the group.
func (gb *GroupBuilder) AddField(fb *FieldBuilder) *GroupBuilder {
if fb != nil {
gb.g.Fields = append(gb.g.Fields, fb.f)
}
return gb
}
// ---- fields ---------------------------------------------------------------
// FieldBuilder configures one field.
type FieldBuilder struct{ f *Field }
func newField(name string, t Type) *FieldBuilder {
return &FieldBuilder{f: &Field{Name: name, Type: t}}
}
// String declares a text field.
func String(name string) *FieldBuilder { return newField(name, TypeString) }
// Int declares an integer field.
func Int(name string) *FieldBuilder { return newField(name, TypeInteger) }
// Number declares a floating-point field.
func Number(name string) *FieldBuilder { return newField(name, TypeNumber) }
// Decimal declares an exact decimal field, for money and anything else where
// binary floating point would round.
func Decimal(name string) *FieldBuilder { return newField(name, TypeDecimal) }
// Bool declares a boolean field.
func Bool(name string) *FieldBuilder { return newField(name, TypeBoolean) }
// Secret declares a field whose value lives in the secrets container and is
// never read back.
func Secret(name string) *FieldBuilder { return newField(name, TypeSecret) }
// Enum declares a choice field.
func Enum(name string) *FieldBuilder { return newField(name, TypeEnum) }
// Datetime declares a date, time or timestamp field; use Format to say which.
func Datetime(name string) *FieldBuilder { return newField(name, TypeDatetime) }
// File declares a file reference. The library validates the reference, never
// the bytes.
func File(name string) *FieldBuilder { return newField(name, TypeFile) }
// List declares a repeated scalar field.
func List(name string) *FieldBuilder { return newField(name, TypeList) }
// Map declares a string-keyed scalar map.
func Map(name string) *FieldBuilder { return newField(name, TypeMap) }
// ObjectList declares a repeated group of fields, each entry forming its own
// scope.
func ObjectList(name string) *FieldBuilder { return newField(name, TypeObjectList) }
// JSON declares an opaque JSON value. Validation stops at its boundary.
func JSON(name string) *FieldBuilder { return newField(name, TypeJSON) }
// Field exposes the underlying field for direct edits.
func (fb *FieldBuilder) Field() *Field { return fb.f }
// Label sets the display name.
func (fb *FieldBuilder) Label(s string) *FieldBuilder { fb.f.Label = s; return fb }
// LabelI18n sets translations of the display name.
func (fb *FieldBuilder) LabelI18n(m map[string]string) *FieldBuilder { fb.f.LabelI18n = m; return fb }
// Description sets the help text.
func (fb *FieldBuilder) Description(s string) *FieldBuilder { fb.f.Description = s; return fb }
// Placeholder sets the empty-state hint.
func (fb *FieldBuilder) Placeholder(s string) *FieldBuilder { fb.f.Placeholder = s; return fb }
// Required marks the field as mandatory. Containers use MinItems instead.
func (fb *FieldBuilder) Required() *FieldBuilder { fb.f.Required = true; return fb }
// ReadOnly marks the field as displayed but not editable. It is enforced in
// Apply, which is the only place a change can be detected.
func (fb *FieldBuilder) ReadOnly() *FieldBuilder { fb.f.ReadOnly = true; return fb }
// Transient marks a field that exists for validation only and is dropped from
// the document once validation passes, such as a password confirmation.
func (fb *FieldBuilder) Transient() *FieldBuilder { fb.f.Transient = true; return fb }
// Mask declares how Redact writes this field into a log or an audit record.
// It changes nothing about what is stored or validated; see Mask.
func (fb *FieldBuilder) Mask(m *Mask) *FieldBuilder { fb.f.Mask = m; return fb }
// Default sets the value used when the field is visible and has none.
func (fb *FieldBuilder) Default(v any) *FieldBuilder { fb.f.Default = v; return fb }
// Format names a semantic format such as email or hostname. Known formats are
// validated; unknown ones are passed through as a hint for the renderer.
func (fb *FieldBuilder) Format(s string) *FieldBuilder { fb.f.Format = s; return fb }
// Pattern constrains a string with a regular expression.
func (fb *FieldBuilder) Pattern(s string) *FieldBuilder { fb.f.Pattern = s; return fb }
// Length constrains the number of characters.
func (fb *FieldBuilder) Length(min, max int) *FieldBuilder {
fb.f.MinLength, fb.f.MaxLength = &min, &max
return fb
}
// MinLength sets the lower bound on characters.
func (fb *FieldBuilder) MinLength(n int) *FieldBuilder { fb.f.MinLength = &n; return fb }
// MaxLength sets the upper bound on characters.
func (fb *FieldBuilder) MaxLength(n int) *FieldBuilder { fb.f.MaxLength = &n; return fb }
// Range constrains a numeric field.
func (fb *FieldBuilder) Range(min, max float64) *FieldBuilder {
fb.f.Min, fb.f.Max = &min, &max
return fb
}
// Min sets the lower numeric bound.
func (fb *FieldBuilder) Min(n float64) *FieldBuilder { fb.f.Min = &n; return fb }
// Max sets the upper numeric bound.
func (fb *FieldBuilder) Max(n float64) *FieldBuilder { fb.f.Max = &n; return fb }
// MinDec sets the lower bound of a decimal field from its decimal string, so
// the schema can be written the way the value is written.
func (fb *FieldBuilder) MinDec(s string) *FieldBuilder {
if n, err := strconv.ParseFloat(s, 64); err == nil {
fb.f.Min = &n
}
return fb
}
// MaxDec sets the upper bound of a decimal field from its decimal string.
func (fb *FieldBuilder) MaxDec(s string) *FieldBuilder {
if n, err := strconv.ParseFloat(s, 64); err == nil {
fb.f.Max = &n
}
return fb
}
// Options sets the choices of an enum.
func (fb *FieldBuilder) Options(opts ...Option) *FieldBuilder { fb.f.Options = opts; return fb }
// Multiple lets an enum take several values.
func (fb *FieldBuilder) Multiple() *FieldBuilder { fb.f.Multiple = true; return fb }
// Items constrains the elements of a list or the values of a map.
func (fb *FieldBuilder) Items(ib *ItemsBuilder) *FieldBuilder {
if ib != nil {
fb.f.Items = ib.it
}
return fb
}
// KeyPattern constrains the keys of a map.
func (fb *FieldBuilder) KeyPattern(s string) *FieldBuilder { fb.f.KeyPattern = s; return fb }
// MinItems sets the minimum number of entries in a container.
func (fb *FieldBuilder) MinItems(n int) *FieldBuilder { fb.f.MinItems = &n; return fb }
// MaxItems sets the maximum number of entries in a container.
func (fb *FieldBuilder) MaxItems(n int) *FieldBuilder { fb.f.MaxItems = &n; return fb }
// Unique declares a combined uniqueness constraint across the entries of an
// object_list. Several names form one composite key, not several constraints.
func (fb *FieldBuilder) Unique(names ...string) *FieldBuilder { fb.f.Unique = names; return fb }
// ItemLabel is the template used to title one entry, e.g. "{host}:{port}".
func (fb *FieldBuilder) ItemLabel(s string) *FieldBuilder { fb.f.ItemLabel = s; return fb }
// Layout hints how to render an object_list, e.g. "table" or "cards".
func (fb *FieldBuilder) Layout(s string) *FieldBuilder { fb.f.Layout = s; return fb }
// AddField appends a field to an object_list entry.
func (fb *FieldBuilder) AddField(sub *FieldBuilder) *FieldBuilder {
if sub != nil {
fb.f.Fields = append(fb.f.Fields, sub.f)
}
return fb
}
// VisibleWhen shows the field only when the condition holds. An invisible
// field has no value.
func (fb *FieldBuilder) VisibleWhen(c *Condition) *FieldBuilder { fb.f.VisibleWhen = c; return fb }
// RequiredWhen makes the field mandatory only when the condition holds.
func (fb *FieldBuilder) RequiredWhen(c *Condition) *FieldBuilder { fb.f.RequiredWhen = c; return fb }
// ReadOnlyWhen locks the field against changes while the condition holds.
func (fb *FieldBuilder) ReadOnlyWhen(c *Condition) *FieldBuilder { fb.f.ReadOnlyWhen = c; return fb }
// Check adds a cross-field rule that must hold while the field is visible.
func (fb *FieldBuilder) Check(c *Condition, code, reason string) *FieldBuilder {
fb.f.ValidWhen = append(fb.f.ValidWhen, &Rule{When: c, Code: code, Reason: reason})
return fb
}
// Meta attaches renderer-specific data. The library preserves it and never
// interprets it.
func (fb *FieldBuilder) Meta(k string, v any) *FieldBuilder {
if fb.f.Metadata == nil {
fb.f.Metadata = map[string]any{}
}
fb.f.Metadata[k] = v
return fb
}
// ---- items ----------------------------------------------------------------
// ItemsBuilder configures the element constraint of a list or map.
type ItemsBuilder struct{ it *Items }
func newItems(t Type) *ItemsBuilder { return &ItemsBuilder{it: &Items{Type: t}} }
// ItemString constrains elements to text.
func ItemString() *ItemsBuilder { return newItems(TypeString) }
// ItemInt constrains elements to integers.
func ItemInt() *ItemsBuilder { return newItems(TypeInteger) }
// ItemNumber constrains elements to numbers.
func ItemNumber() *ItemsBuilder { return newItems(TypeNumber) }
// ItemDecimal constrains elements to exact decimals.
func ItemDecimal() *ItemsBuilder { return newItems(TypeDecimal) }
// ItemBool constrains elements to booleans.
func ItemBool() *ItemsBuilder { return newItems(TypeBoolean) }
// ItemDatetime constrains elements to dates or timestamps.
func ItemDatetime() *ItemsBuilder { return newItems(TypeDatetime) }
// ItemFile constrains elements to file references.
func ItemFile() *ItemsBuilder { return newItems(TypeFile) }
// ItemEnum constrains elements to a set of choices.
func ItemEnum(opts ...Option) *ItemsBuilder {
ib := newItems(TypeEnum)
ib.it.Options = opts
return ib
}
// Format names a semantic format for the elements.
func (ib *ItemsBuilder) Format(s string) *ItemsBuilder { ib.it.Format = s; return ib }
// Pattern constrains string elements with a regular expression.
func (ib *ItemsBuilder) Pattern(s string) *ItemsBuilder { ib.it.Pattern = s; return ib }
// Range constrains numeric elements.
func (ib *ItemsBuilder) Range(min, max float64) *ItemsBuilder {
ib.it.Min, ib.it.Max = &min, &max
return ib
}
// Length constrains the number of characters in string elements.
func (ib *ItemsBuilder) Length(min, max int) *ItemsBuilder {
ib.it.MinLength, ib.it.MaxLength = &min, &max
return ib
}
// Opt builds one enum choice.
func Opt(value any, label string) Option { return Option{Value: value, Label: label} }
// OptI18n builds one enum choice with translations.
func OptI18n(value any, label string, i18n map[string]string) Option {
return Option{Value: value, Label: label, LabelI18n: i18n}
}