-
Notifications
You must be signed in to change notification settings - Fork 6
/
struct.go
348 lines (296 loc) · 10.5 KB
/
struct.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
package generate
import (
"bytes"
"fmt"
"go/format"
"io"
"sort"
"strings"
"github.com/GannettDigital/jstransform/jsonschema"
)
// extractedField represents a Golang struct field as extracted from a JSON schema file. It is an intermediate format
// that is populated while parsing the JSON schema file then used when generating the Golang code for the struct.
type extractedField struct {
array bool
description string
fields extractedFields
jsonName string
jsonType string
name string
requiredFields map[string]bool
}
// write outputs the Golang representation of this field to the writer with prefix before each line.
// It handles inline structs by calling this method recursively adding a new \t to the prefix for each layer.
// If required is set to false 'omitempty' is added in the JSON struct tag for the field
func (ef *extractedField) write(w io.Writer, prefix string, required, descriptionAsStructTag, pointers bool) error {
var omitempty string
if !required {
omitempty = ",omitempty"
}
jsonTag := fmt.Sprintf(`json:"%s%s"`, ef.jsonName, omitempty)
var description string
if descriptionAsStructTag && ef.description != "" {
description = fmt.Sprintf(`description:"%s"`, strings.Split(ef.description, "\n")[0])
}
structTag := fmt.Sprintf("`%s`\n", strings.Trim(strings.Join([]string{jsonTag, description}, " "), " "))
if !descriptionAsStructTag && ef.description != "" {
for _, line := range strings.Split(ef.description, "\n") {
if _, err := w.Write([]byte(fmt.Sprintf("// %s\n", line))); err != nil {
return err
}
}
}
if ef.jsonType != "object" {
_, err := w.Write([]byte(fmt.Sprintf("%s%s\t%s\t%s", prefix, ef.name, goType(ef.jsonType, ef.array, required, pointers), structTag)))
return err
}
if _, err := w.Write([]byte(fmt.Sprintf("%s%s\t%s {\n", prefix, ef.name, goType(ef.jsonType, ef.array, required, pointers)))); err != nil {
return err
}
for _, field := range ef.fields.Sorted() {
fieldRequired := ef.requiredFields[field.jsonName]
if err := field.write(w, prefix+"\t", fieldRequired, descriptionAsStructTag, pointers); err != nil {
return fmt.Errorf("failed writing field %q: %v", field.name, err)
}
}
if _, err := w.Write([]byte(fmt.Sprintf("%s\t}\t%s", prefix, structTag))); err != nil {
return err
}
return nil
}
// extractedFields is a map of fields keyed on the field name.
type extractedFields map[string]*extractedField
// IncludeTime does a depth-first recursive search to see if any field or child field is of type "date-time"
func (efs extractedFields) IncludeTime() bool {
for _, field := range efs {
if field.fields != nil {
if field.fields.IncludeTime() {
return true
}
}
if field.jsonType == "date-time" {
return true
}
}
return false
}
// Sorted will return the fields in a sorted list. The sort is a string sort on the keys
func (efs extractedFields) Sorted() []*extractedField {
var sorted []*extractedField
var sortedKeys sort.StringSlice
fieldsByName := make(map[string]*extractedField)
for _, f := range efs {
sortedKeys = append(sortedKeys, f.name)
fieldsByName[f.name] = f
}
sortedKeys.Sort()
for _, key := range sortedKeys {
sorted = append(sorted, fieldsByName[key])
}
return sorted
}
// goFile represents the contents of a single go file to be generated based on the given JSON schema.
type goFile struct {
packageName string
args BuildArgs
rootStruct *generatedStruct
nestedStructs map[string]*generatedStruct // The key is derived from the path used by the walk function for the given struct
}
// newGeneratedGoFile creates a go file based on the given JSON schema.
// The write function can be used to write out the value of the file, which will end up with either a single struct
// or multiple depending on the presence of nested structs and the value of the NoNestedStructs build argument.
func newGeneratedGoFile(schema *jsonschema.Schema, name, packageName string, embeds []string, args BuildArgs) (*goFile, error) {
required := map[string]bool{}
for _, fname := range schema.Required {
required[fname] = true
}
gof := &goFile{
packageName: packageName,
args: args,
nestedStructs: make(map[string]*generatedStruct),
}
gof.rootStruct = gof.newGeneratedStruct(name, required)
gof.rootStruct.embededStructs = embeds
if err := jsonschema.Walk(schema, gof.walkFunc); err != nil {
return nil, fmt.Errorf("failed to walk schema for %q: %v", name, err)
}
return gof, nil
}
func (gof *goFile) newGeneratedStruct(name string, requiredFields map[string]bool) *generatedStruct {
return &generatedStruct{
extractedField: extractedField{
name: name,
fields: make(map[string]*extractedField),
requiredFields: requiredFields,
},
args: gof.args,
}
}
func (gof *goFile) structs() []*generatedStruct {
if len(gof.nestedStructs) < 1 {
return []*generatedStruct{gof.rootStruct}
}
nested := make([]*generatedStruct, len(gof.nestedStructs))
var i int
for _, s := range gof.nestedStructs {
nested[i] = s
i++
}
// order with root first and nested in a consistent following order
sort.Slice(nested, func(i, j int) bool {
return nested[i].name < nested[j].name
})
structs := []*generatedStruct{gof.rootStruct}
structs = append(structs, nested...)
return structs
}
// walkFunc is a jsonschema.WalkFunc which builds the fields for generatedStructFile within the gofile as the
// JSON schema file is walked.
func (gof *goFile) walkFunc(path string, i jsonschema.Instance) error {
gen := gof.rootStruct
if !gof.args.NoNestedStructs {
return addField(gen.fields, splitJSONPath(path), i, gen.args.FieldNameMap)
}
parts := []string{exportedName(gof.rootStruct.name)}
for _, part := range splitJSONPath(path) {
parts = append(parts, exportedName(part))
}
// Find parent struct or if none use root struct
parentKey := strings.Join(parts[:len(parts)-1], "")
name := splitJSONPath(path)[len(parts)-2]
gen = gof.nestedStructs[parentKey]
if gen == nil {
gen = gof.rootStruct
}
// If the types is an object create a new generated struct for it
if i.Type == "object" {
key := strings.Join(parts, "")
structType := key
// nullable nested structs could be pointers
if !gof.rootStruct.requiredFields[name] && gof.args.Pointers {
structType = "*" + key
}
requiredFields := make(map[string]bool)
for _, name := range i.Required {
requiredFields[name] = true
}
gof.nestedStructs[key] = gof.newGeneratedStruct(key, requiredFields)
return addField(gen.fields, []string{name}, jsonschema.Instance{Description: i.Description, Type: structType}, gen.args.FieldNameMap)
}
return addField(gen.fields, []string{name}, i, gen.args.FieldNameMap)
}
// write will write the generated file to the given io.Writer.
func (gof *goFile) write(w io.Writer) error {
buf := &bytes.Buffer{} // the formatter uses the entire output, so buffer for that
if _, err := buf.Write([]byte(fmt.Sprintf("package %s\n\n%s\n\n", gof.packageName, disclaimer))); err != nil {
return fmt.Errorf("failed writing struct: %v", err)
}
var includeTime bool
for _, s := range gof.structs() {
if s.fields.IncludeTime() {
includeTime = true
break
}
}
if includeTime {
if _, err := buf.Write([]byte("import \"time\"\n")); err != nil {
return fmt.Errorf("failed writing imports: %v", err)
}
}
for _, s := range gof.structs() {
if _, err := buf.Write([]byte("\n\n")); err != nil {
return fmt.Errorf("failed writing struct %q: %v", s.name, err)
}
if err := s.write(buf); err != nil {
return fmt.Errorf("failed writing struct %q: %v", s.name, err)
}
}
final, err := format.Source(buf.Bytes())
if err != nil {
return fmt.Errorf("failed to format source: %v", err)
}
if _, err := w.Write(final); err != nil {
return fmt.Errorf("error writing to io.Writer: %v", err)
}
return nil
}
type generatedStruct struct {
extractedField
args BuildArgs
embededStructs []string
}
// write will write the generated file to the given io.Writer.
func (gen *generatedStruct) write(w io.Writer) error {
embeds := strings.Join(gen.embededStructs, "\n")
if embeds != "" {
embeds += "\n\n"
}
if _, err := w.Write([]byte(fmt.Sprintf("type %s struct {\n%s", exportedName(gen.name), embeds))); err != nil {
return fmt.Errorf("failed writing struct: %v", err)
}
for _, field := range gen.fields.Sorted() {
req := gen.requiredFields[field.jsonName]
if err := field.write(w, "\t", req, gen.args.DescriptionAsStructTag, gen.args.Pointers); err != nil {
return fmt.Errorf("failed writing field %q: %v", field.name, err)
}
}
if _, err := w.Write([]byte("}")); err != nil {
return fmt.Errorf("failed writing struct: %v", err)
}
return nil
}
// addField will create a new field or add to an existing field in the extractedFields.
// Nested fields are handled by recursively calling this function until the leaf field is reached.
// For all fields the name and jsonType are set, for arrays the array bool is set for true and for JSON objects,
// the fields map is created and if it exists the requiredFields section populated.
// fields will be renamed if a matching entry is supplied in the fieldRenameMap
func addField(fields extractedFields, tree []string, inst jsonschema.Instance, fieldRenameMap map[string]string) error {
if len(tree) > 1 {
if f, ok := fields[tree[0]]; ok {
return addField(f.fields, tree[1:], inst, fieldRenameMap)
}
f := &extractedField{jsonName: tree[0], jsonType: "object", name: exportedName(tree[0]), fields: make(map[string]*extractedField)}
fields[tree[0]] = f
if err := addField(f.fields, tree[1:], inst, fieldRenameMap); err != nil {
return fmt.Errorf("failed field %q: %v", tree[0], err)
}
return nil
}
if len(tree) > 0 {
fieldName, ok := fieldRenameMap[tree[0]]
if !ok {
fieldName = tree[0]
}
f := &extractedField{
description: inst.Description,
name: exportedName(fieldName),
jsonName: tree[0],
jsonType: inst.Type,
}
// Second processing of an array type
if exists, ok := fields[f.jsonName]; ok {
f = exists
if f.array && f.jsonType == "" {
f.jsonType = inst.Type
} else {
return fmt.Errorf("field %q already exists but is not an array field", f.name)
}
}
if inst.Type == "string" && inst.Format == "date-time" {
f.jsonType = "date-time"
}
switch f.jsonType {
case "array":
f.jsonType = ""
f.array = true
case "object":
f.requiredFields = make(map[string]bool)
for _, name := range inst.Required {
f.requiredFields[name] = true
}
f.fields = make(map[string]*extractedField)
}
fields[tree[0]] = f
}
return nil
}