Repository navigation
Expand file tree
/
Copy pathexample_test.go
More file actions
171 lines (152 loc) · 6.3 KB
/
Copy pathexample_test.go
File metadata and controls
171 lines (152 loc) · 6.3 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
package dyfields_test
import (
"encoding/json"
"fmt"
"log"
"sort"
"github.com/BrobridgeOrg/dyfields"
)
// Example is the quickstart from the README, kept here so it cannot rot.
func Example() {
b := dyfields.NewBuilder()
b.AddField(dyfields.String("cluster").Label("Cluster").Required())
b.AddField(dyfields.Enum("env").Label("Environment").Required().Default("dev").
Options(dyfields.Opt("dev", "Development"), dyfields.Opt("prod", "Production")))
brokers := dyfields.ObjectList("brokers").Label("Broker").
MinItems(1).MaxItems(16).Unique("host").ItemLabel("{host}:{port}")
brokers.AddField(dyfields.String("host").Format("hostname").Required())
brokers.AddField(dyfields.Int("port").Range(1, 65535).Default(9092))
brokers.AddField(dyfields.Bool("use_tls").Default(false))
brokers.AddField(dyfields.Secret("password").VisibleWhen(dyfields.IsTrue("use_tls")))
b.AddField(brokers)
c, err := b.Compile()
if err != nil {
panic(err)
}
doc := dyfields.ValueDocument{
Values: map[string]any{
"cluster": "orders",
"brokers": []any{
map[string]any{"$id": "b1", "host": "a.internal", "use_tls": true},
},
},
Secrets: map[string]string{"brokers.b1.password": "s3cret"},
}
out, errs := c.Validate(doc)
fmt.Println("errors:", errs.Len())
fmt.Println("env:", out.Values["env"])
fmt.Println("port:", out.Values["brokers"].([]any)[0].(map[string]any)["port"])
// Now break it: no cluster, a host that is not a hostname, and a password
// on a broker that does not use TLS.
bad := dyfields.ValueDocument{
Values: map[string]any{
"brokers": []any{map[string]any{"$id": "b1", "host": "not a host"}},
},
Secrets: map[string]string{"brokers.b1.password": "s3cret"},
}
fixed, errs := c.Validate(bad)
for _, e := range errs {
fmt.Printf("%s: %s: %s\n", e.Path, e.Code, e.Reason)
}
// use_tls defaults to false, so the password field is hidden and its
// secret is dropped rather than kept around.
fmt.Println("secrets left:", len(fixed.Secrets))
// Output:
// errors: 0
// env: dev
// port: 9092
// brokers[0].host: format: must be a valid hostname
// cluster: required: Cluster is required
// secrets left: 0
}
// Example_service is the shape dyfields takes inside another Go project: the
// schema is stored as JSON, compiled once at start-up, and every request is
// checked against that one compiled value. It is the README's "Using it from a
// service" section, kept here so it cannot rot.
func Example_service() {
// The definition, as it would come out of a file or a database column.
const schemaJSON = `{
"schema_version": 1,
"groups": [{
"key": "delivery",
"label": "Delivery",
"fields": [
{"name": "channel", "type": "enum", "label": "Channel", "required": true,
"options": [{"value": "email"}, {"value": "webhook"}]},
{"name": "address", "type": "string", "format": "email", "label": "Address",
"required": true,
"visible_when": {"field": "channel", "equals": "email"}},
{"name": "endpoint", "type": "string", "format": "uri", "label": "Endpoint",
"required": true,
"visible_when": {"field": "channel", "equals": "webhook"}},
{"name": "signing_token", "type": "secret", "label": "Signing token",
"visible_when": {"field": "channel", "equals": "webhook"}}
]
}]
}`
// Compile once. A *Compiled is immutable and safe to share across goroutines,
// so this belongs at start-up, not in the handler. A schema that does not
// compile is a bug in your own deployment, not a bad request.
c, err := dyfields.Load([]byte(schemaJSON))
if err != nil {
log.Fatalf("bad schema: %v", err)
}
// What the service has stored for this tenant today.
stored := dyfields.ValueDocument{
Values: map[string]any{"channel": "email", "address": "ops@example.com"},
Secrets: map[string]string{},
}
// Handing the current state to a browser. Redact first: it swaps every secret
// for the list of paths that have one set, which is exactly what the form
// needs to render "already configured" without ever shipping the value.
public := c.Redact(stored)
body, _ := json.Marshal(public)
fmt.Println("GET /settings ->", string(body))
// An edit arrives. A patch is not a document: an absent secret means "leave
// it", "" also means "leave it" (an untouched password field submits exactly
// that), and null means "clear it".
patch, err := dyfields.ParsePatch([]byte(`{
"values": {"channel": "webhook", "endpoint": "not a url"},
"secrets": {"signing_token": "whsec_123"}
}`))
if err != nil {
fmt.Println("PATCH /settings -> 400", err)
return
}
// Ask what the edit would cost before making it. Switching the channel hides
// the email address, and a field you cannot see holds no value -- so the
// address is about to be dropped. This is the warning the UI shows.
if lost := c.ImpactOf(stored, patch); len(lost) > 0 {
fmt.Println("would clear:", lost)
}
// Apply merges and validates in one step. Use it rather than Validate for
// edits: readonly is only enforceable here, because only Apply can see what
// the value used to be.
next, errs := c.Apply(stored, patch)
if errs.Len() > 0 {
// Sorted so this example prints the same thing every run; a real handler
// would hand the slice over as it is.
sort.Slice(errs, func(i, j int) bool { return errs[i].Path < errs[j].Path })
out, _ := json.Marshal(map[string]any{"errors": errs})
fmt.Println("PATCH /settings -> 422", string(out))
}
// Fix the endpoint and try again.
patch.Values["endpoint"] = "https://hooks.example.com/ingest"
next, errs = c.Apply(stored, patch)
if errs.Len() > 0 {
log.Fatalf("unexpected: %v", errs)
}
// next is what to persist, and it is the checker's own output rather than
// anything the handler assembled: defaults filled in, invisible fields
// removed, transient fields dropped.
stored = next
saved, _ := json.Marshal(stored.Values)
fmt.Println("stored values:", string(saved))
fmt.Println("stored secrets:", stored.Secrets)
// Output:
// GET /settings -> {"values":{"address":"ops@example.com","channel":"email"}}
// would clear: [address]
// PATCH /settings -> 422 {"errors":[{"path":"endpoint","field":"endpoint","group":"delivery","code":"format","reason":"must be an absolute URI"}]}
// stored values: {"channel":"webhook","endpoint":"https://hooks.example.com/ingest"}
// stored secrets: map[signing_token:whsec_123]
}