forked from puppetlabs-toy-chest/wash
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathentrySchema.go
416 lines (386 loc) · 13.6 KB
/
entrySchema.go
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
package plugin
import (
"encoding/json"
"fmt"
"reflect"
"strings"
"github.com/ekinanp/jsonschema"
"github.com/emirpasic/gods/maps/linkedhashmap"
)
const registrySchemaLabel = "mountpoint"
// TypeID returns the entry's type ID. It is needed by the API,
// so plugin authors should ignore this.
func TypeID(e Entry) string {
pluginName := pluginName(e)
rawTypeID := rawTypeID(e)
if pluginName == "" {
// e is the plugin registry or a core entry used by an external plugin
return rawTypeID
}
return namespace(pluginName, rawTypeID)
}
// SchemaGraph returns e's schema graph. This is effectively a
// map[string]plugin.EntrySchema object that preserves insertion
// order (where the string is each entry's type ID).
func SchemaGraph(e Entry) (*linkedhashmap.Map, error) {
s, err := schema(e)
if err != nil {
return nil, err
}
if s == nil {
return nil, nil
}
if s.graph == nil {
// e is a core plugin entry so fill its graph
s.graph = linkedhashmap.New()
s.fill(s.graph)
}
return s.graph, nil
}
func schema(e Entry) (*EntrySchema, error) {
switch t := e.(type) {
case externalPlugin:
graph, err := t.SchemaGraph()
if err != nil {
return nil, err
}
if graph == nil {
return nil, nil
}
s := NewEntrySchema(e, "foo")
s.graph = graph
entrySchemaV, _ := s.graph.Get(TypeID(e))
// Nodes in the graph can only set properties on entrySchema, so only copy that.
s.entrySchema = entrySchemaV.(EntrySchema).entrySchema
return s, nil
case *Registry:
// The plugin registry includes external plugins, whose schema call can
// error. Thus, it needs to be treated differently from the other core
// plugins. Here, the logic is to merge each root's schema graph with the
// registry's schema graph.
schema := NewEntrySchema(t, registrySchemaLabel).
IsSingleton().
SetDescription(registryDescription)
schema.graph = linkedhashmap.New()
typeID := TypeID(t)
// We start by putting in a stub value for s so that we preserve the insertion
// order. We'll then update this value once the "Children" array's been calculated.
schema.graph.Put(typeID, EntrySchema{})
for _, root := range t.pluginRoots {
childSchema, err := Schema(root)
if err != nil {
return nil, fmt.Errorf("failed to retrieve the %v plugin's schema: %v", root.eb().name, err)
}
if childSchema == nil {
// The plugin doesn't have a schema, which means it's an external plugin.
// Create a schema for root so that `stree <mountpoint>` can still display
// it.
childSchema = NewEntrySchema(root, CName(root))
}
childSchema.IsSingleton()
schema.Children = append(schema.Children, TypeID(childSchema.entry))
childGraph := childSchema.graph
if childGraph == nil {
// This is a core-plugin
childGraph = linkedhashmap.New()
childSchema.fill(childGraph)
}
childGraph.Each(func(key interface{}, value interface{}) {
schema.graph.Put(key, value)
})
}
// Update the graph
schema.graph.Put(typeID, schema.clone())
return schema, nil
default:
// e is a core-plugin
return e.Schema(), nil
}
}
type entrySchema struct {
Label string `json:"label"`
Description string `json:"description,omitempty"`
Singleton bool `json:"singleton"`
Signals []SignalSchema `json:"signals,omitempty"`
Actions []string `json:"actions"`
PartialMetadataSchema *JSONSchema `json:"partial_metadata_schema"`
MetadataSchema *JSONSchema `json:"metadata_schema"`
Children []string `json:"children"`
}
// EntrySchema represents an entry's schema. Use plugin.NewEntrySchema
// to create instances of these objects.
type EntrySchema struct {
// This pattern's a nice way of making JSON marshalling/unmarshalling
// easy without having to export these fields via the godocs. The latter
// is good because plugin authors should use the builders when setting them
// (so that we maintain a consistent API for e.g. metadata schemas).
//
// This pattern was obtained from https://stackoverflow.com/a/11129474
entrySchema
partialMetadataSchemaObj interface{}
metadataSchemaObj interface{}
// Store the entry so that we can compute its type ID and, if the entry's
// a core plugin entry, enumerate its child schemas when marshaling its
// schema.
entry Entry
// graph is set by external plugins
graph *linkedhashmap.Map
}
// NewEntrySchema returns a new EntrySchema object with the specified label.
func NewEntrySchema(e Entry, label string) *EntrySchema {
if len(label) == 0 {
panic("plugin.NewEntrySchema called with an empty label")
}
s := &EntrySchema{
entrySchema: entrySchema{
Label: label,
Actions: SupportedActionsOf(e),
},
// The partial metadata's empty by default
partialMetadataSchemaObj: struct{}{},
entry: e,
}
return s
}
// MarshalJSON marshals the entry's schema to JSON. It takes
// a value receiver so that the entry schema's still marshalled
// when it's referenced as an interface{} object. See
// https://stackoverflow.com/a/21394657 for more details.
//
// Note that UnmarshalJSON is not implemented since that is not
// how plugin.EntrySchema objects are meant to be used.
func (s EntrySchema) MarshalJSON() ([]byte, error) {
if s.entry == nil {
// This corresponds to an "EntrySchema" value in another entry's schema graph.
// These values directly set the undocumented fields of EntrySchema. Since graph
// and entry won't be set - and this is part of a graph already - directly serialize
// entrySchema instead of using the graph.
return json.Marshal(s.entrySchema)
}
graph := s.graph
if graph == nil {
if _, ok := s.entry.(externalPlugin); ok {
// We should never hit this code-path because external plugins with
// unknown schemas will return a nil schema. Thus, EntrySchema#MarshalJSON
// will never be invoked.
msg := fmt.Sprintf(
"s.MarshalJSON: called with a nil graph for external plugin entry %v (type ID %v)",
CName(s.entry),
TypeID(s.entry),
)
panic(msg)
}
// We're marshalling a core plugin entry's schema. Note that the reason
// we use an ordered map is to ensure that the first key in the marshalled
// schema corresponds to s.
graph = linkedhashmap.New()
s.fill(graph)
}
return graph.ToJSON()
}
// SetDescription sets the entry's description.
func (s *EntrySchema) SetDescription(description string) *EntrySchema {
s.entrySchema.Description = strings.Trim(description, "\n")
return s
}
// IsSingleton marks the entry as a singleton entry.
func (s *EntrySchema) IsSingleton() *EntrySchema {
s.entrySchema.Singleton = true
return s
}
// AddSignal adds the given signal to s' supported signals. See https://puppetlabs.github.io/wash/docs#signal
// for a list of common signal names. You should try to re-use these names if you can.
func (s *EntrySchema) AddSignal(name string, description string) *EntrySchema {
return s.addSignalSchema(name, "", description)
}
// AddSignalGroup adds the given signal group to s' supported signals
func (s *EntrySchema) AddSignalGroup(name string, regex string, description string) *EntrySchema {
if len(regex) <= 0 {
panic("s.AddSignalGroup: received empty regex")
}
return s.addSignalSchema(name, regex, description)
}
func (s *EntrySchema) addSignalSchema(name string, regex string, description string) *EntrySchema {
if len(name) <= 0 {
panic("s.addSignalSchema: received empty name")
}
if len(description) <= 0 {
panic("s.addSignalSchema: received empty description")
}
schema := SignalSchema{
signalSchema: signalSchema{
Name: name,
Regex: regex,
Description: description,
},
}
err := schema.normalize()
if err != nil {
msg := fmt.Sprintf("s.addSignalSchema: received invalid regex: %v", err)
panic(msg)
}
s.Signals = append(s.Signals, schema)
return s
}
// SetPartialMetadataSchema sets the partial metadata's schema. obj is an empty
// struct that will be marshalled into a JSON schema. SetPartialMetadataSchema
// will panic if obj is not a struct.
func (s *EntrySchema) SetPartialMetadataSchema(obj interface{}) *EntrySchema {
// We need to know if s.entry has any wrapped types in order to correctly
// compute the schema. However that information is known when s.fill() is
// called. Thus, we'll set the schema object to obj so s.fill() can properly
// calculate the schema.
s.partialMetadataSchemaObj = obj
return s
}
// SetMetadataSchema sets Entry#Metadata's schema. obj is an empty struct that will be
// marshalled into a JSON schema. SetMetadataSchema will panic if obj is not a struct.
//
// NOTE: Only use SetMetadataSchema if you're overriding Entry#Metadata. Otherwise, use
// SetPartialMetadataSchema.
func (s *EntrySchema) SetMetadataSchema(obj interface{}) *EntrySchema {
// See the comments in SetPartialMetadataSchema to understand why this line's necessary
s.metadataSchemaObj = obj
return s
}
func (s *EntrySchema) fill(graph *linkedhashmap.Map) {
// Fill-in the partial metadata + metadata schemas
var err error
if s.partialMetadataSchemaObj != nil {
s.entrySchema.PartialMetadataSchema, err = s.schemaOf(s.partialMetadataSchemaObj)
if err != nil {
s.fillPanicf("bad value passed into SetPartialMetadataSchema: %v", err)
}
}
if s.metadataSchemaObj != nil {
s.entrySchema.MetadataSchema, err = s.schemaOf(s.metadataSchemaObj)
if err != nil {
s.fillPanicf("bad value passed into SetMetadataSchema: %v", err)
}
}
typeID := TypeID(s.entry)
if !ListAction().IsSupportedOn(s.entry) {
graph.Put(typeID, s.clone())
return
}
// Fill-in the children. We start by putting in a stub value for s
// so that we preserve the insertion order and so that we can mark
// the node as visited. We'll then update this value once the "Children"
// array's been calculated.
graph.Put(typeID, EntrySchema{})
// "sParent" is read as "s.parent"
sParent := s.entry.(Parent)
children := sParent.ChildSchemas()
if children == nil {
s.fillPanicf("ChildSchemas() returned nil")
}
for _, child := range children {
if child == nil {
s.fillPanicf("found a nil child schema")
}
// The ID here is meaningless. We only set it so that TypeID can get the
// plugin name
child.entry.eb().id = s.entry.eb().id
childTypeID := TypeID(child.entry)
s.entrySchema.Children = append(s.Children, childTypeID)
if _, ok := graph.Get(childTypeID); ok {
continue
}
passAlongWrappedTypes(sParent, child.entry)
child.fill(graph)
}
// Update the graph
graph.Put(typeID, s.clone())
}
// clone returns a copy of s.entrySchema's fields as an EntrySchema
// object
func (s *EntrySchema) clone() EntrySchema {
var copy EntrySchema
copy.entrySchema = s.entrySchema
return copy
}
// This helper's used by CachedList + EntrySchema#fill(). The reason for
// the helper is because /fs/schema uses repeated calls to CachedList when
// fetching the entry, so we need to pass-along the wrapped types when
// searching for it. However, Parent#ChildSchemas uses empty Entry objects
// that do not go through CachedList (by definition). Thus, the entry found
// by /fs/schema needs to pass its wrapped types along to the children to
// determine their metadata schemas. This is done in s.fill().
func passAlongWrappedTypes(p Parent, child Entry) {
var wrappedTypes SchemaMap
if root, ok := child.(HasWrappedTypes); ok {
wrappedTypes = root.WrappedTypes()
} else {
wrappedTypes = p.eb().wrappedTypes
}
child.eb().wrappedTypes = wrappedTypes
}
// Helper that wraps the common code shared by the SetMeta*Schema methods
func (s *EntrySchema) schemaOf(obj interface{}) (*JSONSchema, error) {
typeMappings := make(map[reflect.Type]*jsonschema.Type)
for t, s := range s.entry.eb().wrappedTypes {
typeMappings[reflect.TypeOf(t)] = s.Type
}
r := jsonschema.Reflector{
AllowAdditionalProperties: false,
// Setting this option ensures that the schema's root is obj's
// schema instead of a reference to a definition containing obj's
// schema. This way, we can validate that "obj" is a JSON object's
// schema. Otherwise, the check below will always fail.
ExpandedStruct: true,
TypeMappings: typeMappings,
}
schema := r.Reflect(obj)
if schema.Type.Type != "object" {
return nil, fmt.Errorf("expected a JSON object but got %v", schema.Type.Type)
}
return schema, nil
}
// Helper for s.fill(). We make it a separate method to avoid re-creating
// closures for each recursive s.fill() call.
func (s *EntrySchema) fillPanicf(format string, a ...interface{}) {
formatStr := fmt.Sprintf("s.fill (%v): %v", TypeID(s.entry), format)
msg := fmt.Sprintf(formatStr, a...)
panic(msg)
}
func pluginName(e Entry) string {
// Using ID(e) will panic if e.id() is empty. The latter's possible
// via something like "Schema(registry) => TypeID(Root)", where
// CachedList(registry) was not yet called. This can happen if, for
// example, the user starts the Wash shell and runs `stree`.
trimmedID := strings.Trim(e.eb().id, "/")
if trimmedID == "" {
switch e.(type) {
case Root:
return CName(e)
default:
// e is an apifs entry or a core plugin entry used by an external plugin
return ""
}
}
segments := strings.SplitN(trimmedID, "/", 2)
return segments[0]
}
func rawTypeID(e Entry) string {
switch t := e.(type) {
case externalPlugin:
rawTypeID := t.RawTypeID()
if rawTypeID == "" {
rawTypeID = "unknown"
}
return rawTypeID
default:
// e is either a core plugin entry or the plugin registry itself
reflectType := unravelPtr(reflect.TypeOf(e))
return reflectType.PkgPath() + "/" + reflectType.Name()
}
}
func namespace(pluginName string, rawTypeID string) string {
return pluginName + "::" + rawTypeID
}
func unravelPtr(t reflect.Type) reflect.Type {
if t.Kind() == reflect.Ptr {
return unravelPtr(t.Elem())
}
return t
}