-
Notifications
You must be signed in to change notification settings - Fork 0
/
schema.config
290 lines (271 loc) · 11.2 KB
/
schema.config
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
import org.yaml.snakeyaml.Yaml
includeConfig "./custom_schema_types.config"
/**
* This schema namespace is used to valide the params defined in config file(s) using a schema
* YAML file. This config script should be included and called through `schema.validate()` at the
* very bottom.
*/
schema {
type_map = [
'Integer' : Integer,
'String' : [String, GString],
'Number' : Number,
'List' : List,
'Bool' : Boolean,
'Namespace': Map,
'Path' : [String, GString]
]
required_properties = ['type']
path_types = ['Path']
custom_types = [:]
single_choice_types = [
'String',
'Path',
'Number',
'Integer'
]
/**
* Load the schema file.
*/
load_schema = { String file_path ->
def schema_file = new File(file_path)
Yaml parser = new Yaml()
def yaml = parser.load(schema_file.text)
return yaml
}
/**
* Check that given directory either exists and is writeable or is createable.
* @throws IOException when directory cannot be written to or created.
*/
check_write_permission = { File dir ->
if (dir.exists()) {
if (!dir.canWrite()) {
throw new IOException(" ### ERROR ### The directory ${dir} is not writeable. Please verify and try again.")
}
} else {
while (!dir.exists()) {
dir = dir.getParentFile()
}
if (!dir.canWrite()) {
throw new IOException(" ### ERROR ### The directory ${dir} cannot be created. The closest existing parent directory ${dir.toString()} is not writable. Please verify permissions or change the input parameter.")
}
}
}
/**
* Check type for paths (writeable or readable). Accepts path as String for validation.
* @throws FileNotFoundException when file does not exist.
* @throws IOException when file cannot be read.
*/
check_path = { String p, String mode ->
def File file = new File(p)
file = file.getAbsoluteFile()
if (mode == 'w') {
schema.check_write_permission(file)
return
}
if (!(file.exists())) {
throw new FileNotFoundException("${file} does not exist.")
}
if (!(file.canRead())) {
throw new IOException("${file} is not readable.")
}
}
/**
* Set default properties from the schema.
*/
set_default_properties = { Map properties ->
if (!properties.containsKey('help')) {
properties['help'] = ''
}
if (!properties.containsKey('required')) {
properties['required'] = false
}
if (properties['type'] in schema.path_types && !properties.containsKey('mode')) {
properties['mode'] = 'r'
}
if (properties['type'] in schema.type_map['String'] && !properties.containsKey('allow_empty')) {
properties['allow_empty'] = false
}
if (properties['type'] in schema.type_map['List'] && !properties.containsKey('allow_empty')) {
properties['allow_empty'] = false
}
}
/**
* Check whether the required properties are defined for the corresponding parameter from the
* schema yaml file.
*
* @throws IllegalArgumentException when config file is missing required properties.
*/
check_required_properties = { String name, Map properties ->
schema.required_properties.each { property ->
if (!properties.containsKey(property)) {
throw new IllegalArgumentException("Config file invalid. Parameter ${name} is missing property ${property}.")
}
}
}
/**
* Check whether the required parameter is set from config file(s).
* @throws IllegalArgumentException when config file is missing required properties.
*/
check_required = { Map options, String name, Map properties ->
if (properties['required'] && !options.containsKey(name)) {
throw new IllegalArgumentException("Config file invalid. Required parameter ${name} is missing.")
}
}
/**
* Check whether the parameter is set in the correct type, e.g. string, integer, etc.
* @throws IllegalArgumentException when config file is missing required properties.
*/
check_type = { Map options, String name, String type ->
if (! options.containsKey(name)) {
return false
}
if (schema.type_map.containsKey(type)) {
return schema.primitive_check_type(options, name, type)
} else if (! schema.custom_types.containsKey(type)) {
throw new IllegalArgumentException("Invalid parameter type ${type} found from schema.")
}
return true
}
/**
* Check type of values
* @throws IllegalStateException when parameters are not the required type.
*/
primitive_check_type = { Map options, String name, String type ->
if (!(schema.type_map[type].any{ options[name] in it })) {
throw new IllegalStateException("Invalid parameter type for parameter ${name}. Requires ${schema.type_map[type]} but received ${options[name].getClass()}.")
}
return true
}
/**
* Check if string/list is empty if necessary
* @throws IllegalStateException when required non-empty parameters are empty.
*/
check_non_empty = { Map options, String name, Boolean allow_empty ->
if (allow_empty) {
return
}
if (options[name].isEmpty()) {
throw new Exception("Parameter ${name} of type String or List is empty. Please enter a value.")
}
}
/**
* Check whether the parameter is set within the valid choices.
* @throws IllegalStateException when invalid choice is used for parameter.
*/
check_choices_singular = { Map options, String name, List choices ->
if (!(options[name] in choices)) {
throw new Exception("Invalid parameter ${name}. Valid values: ${choices}.")
}
}
/**
* Check whether list contains only valid choices
* @throws IllegalStateException when invalid choice is used for parameter in a list.
*/
check_choices_list = { Map options, String name, List choices ->
for (elem in options[name]) {
if (!(elem in choices)) {
throw new Exception("Invalid parameter ${name}. Valid values: ${choices}.")
}
}
}
/**
* Check default for given property
*/
check_default = { Map options, String name, Map properties ->
if (properties.containsKey('default')) {
options[name] = properties.default
}
}
/**
* For a given parameter, check whether the value is set properly from config file(s).
*/
validate_parameter = { Map options, String name, Map properties ->
// type is required
schema.check_required_properties(name, properties)
schema.set_default_properties(properties)
if (schema.check_type(options, name, properties.type)) {
if (properties.type == 'Namespace' && properties.containsKey('elements')) {
properties.elements.each { key, val ->
schema.validate_parameter(options[name], key, val)
}
} else if (properties.type == 'List') {
schema.check_non_empty(options, name, properties.allow_empty)
if (properties.containsKey('choices')) {
schema.check_choices_list(options, name, properties.choices)
}
} else if (properties.type == 'String') {
schema.check_non_empty(options, name, properties.allow_empty)
} else if (properties.type in schema.path_types) {
schema.check_path(options[name], properties.mode)
} else if (schema.custom_types.containsKey(properties.type)) {
schema.custom_types[properties.type](options, name, properties)
}
if (properties.type in schema.single_choice_types && properties.containsKey('choices')) {
schema.check_choices_singular(options, name, properties.choices)
}
} else {
schema.check_default(options, name, properties)
schema.check_required(options, name, properties)
}
}
/**
* Load custom types and validation methods.
* @throws IllegalStateException when invalid choice is used for parameter.
* @throws FileNotFoundException when custom_schema_types.config path is not valid.
*/
load_custom_types = { String custom_types_path=null, Boolean purge_existing_custom_types=false ->
if (custom_types_path != null) {
includeConfig "${custom_types_path}"
if (! custom_schema_types.containsKey('types')) {
throw new IllegalStateException("Failed to load custom types. Custom config must define namespace 'custom_schema_types' containing a Map 'types' defining the custom types as key and the corresponding function for validation as value.")
}
if(purge_existing_custom_types == true){
schema.custom_types = custom_schema_types.types
} else {
if(schema.custom_types.isEmpty()){
schema.custom_types = custom_schema_types.types
} else {
schema.custom_types = schema.custom_types + custom_schema_types.types
}
}
} else {
throw new FileNotFoundException("Failed to load custom types. Custom schema type config is not found.")
}
}
/**
* Main validation to call, to validate the params from config file(s) against the schema.
*/
validate = { String file_path="${projectDir}/config/schema.yaml" ->
def params_schema = schema.load_schema(file_path)
params_schema.each { key, val ->
try {
schema.validate_parameter(params, key, val)
} catch (Exception ex) {
System.out.println "Failed to validate parameter key: ${val}"
throw ex
}
}
}
/**
* Fine-grained validation entrypoint; to be used for validating specific namespaces with certain parameters excluded
* schema_to_validate: path to schema YAML to be used for validation
* params_to_validate: Map of parameters to validate against schema
* keys_to_exclude: params to skip during validation
* @throws IllegalArgumentException when invalid format of schema is provided
*/
validate_specific = { Object schema_to_validate, Map params_to_validate, List keys_to_exclude=[] ->
def params_schema;
if (custom_schema_types.is_string(schema_to_validate)) {
params_schema = schema.load_schema(schema_to_validate)
} else if (schema_to_validate in Map) {
params_schema = schema_to_validate
} else {
throw new IllegalArgumentException("The given schema must be a path to the schema YAML or a Map, received `${schema_to_validate.getClass()}` instead.")
}
params_schema.removeAll{ key, val -> keys_to_exclude.contains(key) }
params_schema.each { key, val ->
schema.validate_parameter(params_to_validate, key, val)
}
}
}