forked from 0xPolygonHermez/zkevm-node
-
Notifications
You must be signed in to change notification settings - Fork 0
/
gen_json_schema.go
260 lines (233 loc) · 7.77 KB
/
gen_json_schema.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
package config
import (
"encoding/json"
"os"
"reflect"
"github.com/0xPolygonHermez/zkevm-node/config/types"
"github.com/0xPolygonHermez/zkevm-node/log"
"github.com/ethereum/go-ethereum/common"
"github.com/invopop/jsonschema"
"github.com/urfave/cli/v2"
)
// ConfigJsonSchemaGenerater are the parameters to generate a json-schema based on the T struct
// The parametrization of the function are used for unittest
type ConfigJsonSchemaGenerater[T any] struct {
repoName string
repoNameSuffix string
// It force to remove any required field in json-schema
cleanRequiredField bool
// It read the comments in the code and add as description in schema
addCodeCommentsToSchema bool
// Check if there are mapstructure that renames the fields
checkNoMapStructureIsRenamingFields bool
// source directories for extract comments
pathSourceCode string
// Struct with the default values to set
defaultValues *T
// NetworkConfig is read from Genesis file, so make sense to remove
// from general config file
removeNetworkConfig bool
}
// NewNodeConfigJsonSchemaGenerater returns a new class for generating json-schema of the node config file (.toml)
func NewNodeConfigJsonSchemaGenerater() ConfigJsonSchemaGenerater[Config] {
res := ConfigJsonSchemaGenerater[Config]{}
res.repoName = "github.com/0xPolygonHermez/zkevm-node"
res.repoNameSuffix = "/config/config"
res.addCodeCommentsToSchema = true
res.pathSourceCode = "./"
res.cleanRequiredField = true
res.checkNoMapStructureIsRenamingFields = true
config_default_values, err := Default()
res.defaultValues = config_default_values
if err != nil {
log.Fatal("can't create default values for config file: " + err.Error())
}
return res
}
// NewNetworkConfigJsonSchemaGenerater returns a new class for generating json-schema of the network-custom config file (.json)
func NewNetworkConfigJsonSchemaGenerater() ConfigJsonSchemaGenerater[GenesisFromJSON] {
res := ConfigJsonSchemaGenerater[GenesisFromJSON]{}
res.repoName = "github.com/0xPolygonHermez/zkevm-node"
res.repoNameSuffix = "/config/config"
res.addCodeCommentsToSchema = true
res.pathSourceCode = "./"
res.cleanRequiredField = true
res.checkNoMapStructureIsRenamingFields = false
res.defaultValues = nil
return res
}
// GenerateJsonSchema launchs the generation, and returns the schema
func (s ConfigJsonSchemaGenerater[T]) GenerateJsonSchema(cli *cli.Context) (*jsonschema.Schema, error) {
if s.checkNoMapStructureIsRenamingFields {
checkNoMapStructureIsRenamingFields(s.defaultValues)
}
r := new(jsonschema.Reflector)
repoName := s.repoName
r.Namer = func(rt reflect.Type) string {
return rt.PkgPath() + "_" + rt.Name()
}
r.ExpandedStruct = true
r.DoNotReference = true
if s.addCodeCommentsToSchema {
if err := r.AddGoComments(repoName, s.pathSourceCode); err != nil {
return nil, err
}
}
schema := r.Reflect(s.defaultValues)
schema.ID = jsonschema.ID(s.repoName + s.repoNameSuffix)
if s.cleanRequiredField {
cleanRequiredFields(schema)
}
if s.defaultValues != nil {
fillDefaultValues(schema, s.defaultValues)
}
if s.removeNetworkConfig {
schema.Properties.Delete("NetworkConfig")
}
return schema, nil
}
// SerializeJsonSchema serializes the schema in JSON to be stored
func (s ConfigJsonSchemaGenerater[T]) SerializeJsonSchema(schema *jsonschema.Schema) ([]byte, error) {
file, err := json.MarshalIndent(schema, "", "\t")
if err != nil {
return nil, err
}
return file, nil
}
// GenerateJsonSchemaAndWriteToFile generate the schema and store in `output_filename` file
func (s ConfigJsonSchemaGenerater[T]) GenerateJsonSchemaAndWriteToFile(cli *cli.Context, output_filename string) error {
schema, err := s.GenerateJsonSchema(cli)
if err != nil {
return err
}
file, err := s.SerializeJsonSchema(schema)
if err != nil {
return err
}
err = os.WriteFile(output_filename, file, 0600) //nolint:gomnd
if err != nil {
return err
}
return nil
}
// The tag `magstructure` is not supported by `jsonschema` module
// so, if you try to rename a field using that the documentation is going to incosistent
// For that reason is a good practice to check that is not present this situation in
// the config files
func checkNoMapStructureIsRenamingFields(data interface{}) {
var reflected reflect.Value
if reflect.ValueOf(data).Kind() == reflect.Ptr {
reflected = reflect.ValueOf(data).Elem()
} else {
reflected = reflect.ValueOf(data)
}
for i := 0; i < reflected.NumField(); i++ {
field := reflected.Type().Field(i)
tag := field.Tag.Get("mapstructure")
if len(tag) > 0 && tag != field.Name {
panic("field [" + field.Name + "] is renamed using mapstructure to [" + tag + "]! that is not supported")
}
if field.Type.Kind() == reflect.Struct {
checkNoMapStructureIsRenamingFields(reflected.FieldByName(field.Name).Interface())
}
}
}
func variantFieldIsSet(field *interface{}) bool {
value := reflect.ValueOf(field)
if value.Kind() == reflect.Ptr && value.IsNil() {
return false
} else {
return true
}
}
func fillDefaultValues(schema *jsonschema.Schema, default_config interface{}) {
fillDefaultValuesPartial(schema, default_config)
}
func getFieldNameFromTag(data reflect.Value, key string, tagName string) (reflect.Value, bool) {
v := data
t := v.Type()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
tag := field.Tag.Get(tagName)
if tag == key {
return v.Field(i), true
}
}
return reflect.Value{}, false
}
func getValueFromStruct(default_config interface{}, key string) reflect.Value {
var default_object reflect.Value
if reflect.ValueOf(default_config).Kind() == reflect.Ptr {
default_object = reflect.ValueOf(default_config).Elem()
} else {
default_object = reflect.ValueOf(default_config)
}
default_value := default_object.FieldByName(key)
if !default_value.IsValid() {
mappedFieldName, found := getFieldNameFromTag(default_object, key, "json")
if found {
default_value = mappedFieldName
}
}
return default_value
}
func fillDefaultValuesPartial(schema *jsonschema.Schema, default_config interface{}) {
if schema.Properties == nil {
return
}
for pair := schema.Properties.Oldest(); pair != nil; pair = pair.Next() {
key := pair.Key
value_schema := pair.Value
log.Debugf("fillDefaultValuesPartial: key: %s", key)
default_value := getValueFromStruct(default_config, key)
if default_value.IsValid() && variantFieldIsSet(&value_schema.Default) {
switch value_schema.Type {
case "array":
if default_value.Kind() == reflect.ValueOf(common.Address{}).Kind() {
if !default_value.IsZero() {
def_value := default_value.Interface()
value_schema.Default = def_value
}
} else {
if !default_value.IsZero() && !default_value.IsNil() {
def_value := default_value.Interface()
value_schema.Default = def_value
}
}
case "object":
typeObj := reflect.ValueOf(default_value.Interface()).Kind()
isPointer := typeObj == reflect.Ptr
if !isPointer || (isPointer && !default_value.IsNil()) {
fillDefaultValuesPartial(value_schema, default_value.Interface())
} else {
log.Debugf("fillDefaultValuesPartial: key: %s is nil", key)
}
default: // string, number, integer, boolean
if default_value.Type() == reflect.TypeOf(types.Duration{}) {
duration, ok := default_value.Interface().(types.Duration)
if ok {
value_schema.Default = duration.String()
}
} else {
value_schema.Default = default_value.Interface()
}
}
}
}
}
func cleanRequiredFields(schema *jsonschema.Schema) {
schema.Required = []string{}
if schema.Properties == nil {
return
}
for pair := schema.Properties.Oldest(); pair != nil; pair = pair.Next() {
value_schema := pair.Value
value_schema.Required = []string{}
switch value_schema.Type {
case "object":
cleanRequiredFields(value_schema)
case "array":
cleanRequiredFields(value_schema.Items)
}
}
}