Asterisk - The Open Source Telephony Project GIT-master-66c01d8
All Data Structures Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Macros Modules Pages
swagger_model.py
Go to the documentation of this file.
2# Asterisk -- An open source telephony toolkit.
3#
4# Copyright (C) 2013, Digium, Inc.
5#
6# David M. Lee, II <dlee@digium.com>
7#
8# See http://www.asterisk.org for more information about
9# the Asterisk project. Please do not directly contact
10# any of the maintainers of this project for assistance;
11# the project provides a web site, mailing lists and IRC
12# channels for your use.
13#
14# This program is free software, distributed under the terms of
15# the GNU General Public License Version 2. See the LICENSE file
16# at the top of the source tree.
17#
18
19"""Swagger data model objects.
20
21These objects should map directly to the Swagger api-docs, without a lot of
22additional fields. In the process of translation, it should also validate the
23model for consistency against the Swagger spec (i.e., fail if fields are
24missing, or have incorrect values).
25
26See https://github.com/wordnik/swagger-core/wiki/API-Declaration for the spec.
27"""
28
29from __future__ import print_function
30import json
31import os.path
32import pprint
33import re
34import sys
35import traceback
36
37# We don't fully support Swagger 1.2, but we need it for subtyping
38SWAGGER_VERSIONS = ["1.1", "1.2"]
39
40SWAGGER_PRIMITIVES = [
41 'void',
42 'string',
43 'boolean',
44 'number',
45 'int',
46 'long',
47 'double',
48 'float',
49 'Date',
50]
51
52
53class Stringify(object):
54 """Simple mix-in to make the repr of the model classes more meaningful.
55 """
56 def __repr__(self):
57 return "%s(%s)" % (self.__class__, pprint.saferepr(self.__dict__))
58
59
60def compare_versions(lhs, rhs):
61 '''Performs a lexicographical comparison between two version numbers.
62
63 This properly handles simple major.minor.whatever.sure.why.not version
64 numbers, but fails miserably if there's any letters in there.
65
66 For reference:
67 1.0 == 1.0
68 1.0 < 1.0.1
69 1.2 < 1.10
70
71 @param lhs Left hand side of the comparison
72 @param rhs Right hand side of the comparison
73 @return < 0 if lhs < rhs
74 @return == 0 if lhs == rhs
75 @return > 0 if lhs > rhs
76 '''
77 lhs = [int(v) for v in lhs.split('.')]
78 rhs = [int(v) for v in rhs.split('.')]
79 return (lhs > rhs) - (lhs < rhs)
80
81
82class ParsingContext(object):
83 """Context information for parsing.
84
85 This object is immutable. To change contexts (like adding an item to the
86 stack), use the next() and next_stack() functions to build a new one.
87 """
88
89 def __init__(self, swagger_version, stack):
90 self.__swagger_version = swagger_version
91 self.__stack = stack
92
93 def __repr__(self):
94 return "ParsingContext(swagger_version=%s, stack=%s)" % (
95 self.swagger_version, self.stack)
96
98 return self.__swagger_version
99
100 def get_stack(self):
101 return self.__stack
102
103 swagger_version = property(get_swagger_version)
104
105 stack = property(get_stack)
106
107 def version_less_than(self, ver):
108 return compare_versions(self.swagger_version, ver) < 0
109
110 def next_stack(self, json, id_field):
111 """Returns a new item pushed to the stack.
112
113 @param json: Current JSON object.
114 @param id_field: Field identifying this object.
115 @return New context with additional item in the stack.
116 """
117 if not id_field in json:
118 raise SwaggerError("Missing id_field: %s" % id_field, self)
119 new_stack = self.stack + ['%s=%s' % (id_field, str(json[id_field]))]
120 return ParsingContext(self.swagger_version, new_stack)
121
122 def next(self, version=None, stack=None):
123 if version is None:
124 version = self.version
125 if stack is None:
126 stack = self.stack
127 return ParsingContext(version, stack)
128
129
130class SwaggerError(Exception):
131 """Raised when an error is encountered mapping the JSON objects into the
132 model.
133 """
134
135 def __init__(self, msg, context, cause=None):
136 """Ctor.
137
138 @param msg: String message for the error.
139 @param context: ParsingContext object
140 @param cause: Optional exception that caused this one.
141 """
142 super(Exception, self).__init__(msg, context, cause)
143
144
146 """Post processing interface for model objects. This processor can add
147 fields to model objects for additional information to use in the
148 templates.
149 """
150 def process_resource_api(self, resource_api, context):
151 """Post process a ResourceApi object.
152
153 @param resource_api: ResourceApi object.
154 @param context: Current context in the API.
155 """
156 pass
157
158 def process_api(self, api, context):
159 """Post process an Api object.
160
161 @param api: Api object.
162 @param context: Current context in the API.
163 """
164 pass
165
166 def process_operation(self, operation, context):
167 """Post process a Operation object.
168
169 @param operation: Operation object.
170 @param context: Current context in the API.
171 """
172 pass
173
174 def process_parameter(self, parameter, context):
175 """Post process a Parameter object.
176
177 @param parameter: Parameter object.
178 @param context: Current context in the API.
179 """
180 pass
181
182 def process_model(self, model, context):
183 """Post process a Model object.
184
185 @param model: Model object.
186 @param context: Current context in the API.
187 """
188 pass
189
190 def process_property(self, property, context):
191 """Post process a Property object.
192
193 @param property: Property object.
194 @param context: Current context in the API.
195 """
196 pass
197
198 def process_type(self, swagger_type, context):
199 """Post process a SwaggerType object.
200
201 @param swagger_type: ResourceListing object.
202 @param context: Current context in the API.
203 """
204 pass
205
206 def process_resource_listing(self, resource_listing, context):
207 """Post process the overall ResourceListing object.
208
209 @param resource_listing: ResourceListing object.
210 @param context: Current context in the API.
211 """
212 pass
213
214
216 """Model of a allowableValues of type RANGE
217
218 See https://github.com/wordnik/swagger-core/wiki/datatypes#complex-types
219 """
220 def __init__(self, min_value, max_value):
221 self.min_value = min_value
222 self.max_value = max_value
223
224 def to_wiki(self):
225 return "Allowed range: Min: {0}; Max: {1}".format(self.min_value, self.max_value)
226
227
229 """Model of a allowableValues of type LIST
230
231 See https://github.com/wordnik/swagger-core/wiki/datatypes#complex-types
232 """
233 def __init__(self, values):
234 self.values = values
235
236 def to_wiki(self):
237 return "Allowed values: {0}".format(", ".join(self.values))
238
239
240def load_allowable_values(json, context):
241 """Parse a JSON allowableValues object.
242
243 This returns None, AllowableList or AllowableRange, depending on the
244 valueType in the JSON. If the valueType is not recognized, a SwaggerError
245 is raised.
246 """
247 if not json:
248 return None
249
250 if not 'valueType' in json:
251 raise SwaggerError("Missing valueType field", context)
252
253 value_type = json['valueType']
254
255 if value_type == 'RANGE':
256 if not 'min' in json and not 'max' in json:
257 raise SwaggerError("Missing fields min/max", context)
258 return AllowableRange(json.get('min'), json.get('max'))
259 if value_type == 'LIST':
260 if not 'values' in json:
261 raise SwaggerError("Missing field values", context)
262 return AllowableList(json['values'])
263 raise SwaggerError("Unkown valueType %s" % value_type, context)
264
265
267 """Model of an operation's parameter.
268
269 See https://github.com/wordnik/swagger-core/wiki/parameters
270 """
271
272 required_fields = ['name', 'paramType', 'dataType']
273
274 def __init__(self):
275 self.param_type = None
276 self.name = None
277 self.description = None
278 self.data_type = None
279 self.required = None
281 self.allow_multiple = None
282
283 def load(self, parameter_json, processor, context):
284 context = context.next_stack(parameter_json, 'name')
285 validate_required_fields(parameter_json, self.required_fields, context)
286 self.name = parameter_json.get('name')
287 self.param_type = parameter_json.get('paramType')
288 self.description = parameter_json.get('description') or ''
289 self.data_type = parameter_json.get('dataType')
290 self.required = parameter_json.get('required') or False
291 self.default_value = parameter_json.get('defaultValue')
293 parameter_json.get('allowableValues'), context)
294 self.allow_multiple = parameter_json.get('allowMultiple') or False
295 processor.process_parameter(self, context)
296 if parameter_json.get('allowedValues'):
297 raise SwaggerError(
298 "Field 'allowedValues' invalid; use 'allowableValues'",
299 context)
300 return self
301
302 def is_type(self, other_type):
303 return self.param_type == other_type
304
305
307 """Model of an error response.
308
309 See https://github.com/wordnik/swagger-core/wiki/errors
310 """
311
312 required_fields = ['code', 'reason']
313
314 def __init__(self):
315 self.code = None
316 self.reason = None
317
318 def load(self, err_json, processor, context):
319 context = context.next_stack(err_json, 'code')
320 validate_required_fields(err_json, self.required_fields, context)
321 self.code = err_json.get('code')
322 self.reason = err_json.get('reason')
323 return self
324
325
327 """Model of a data type.
328 """
329
330 def __init__(self):
331 self.name = None
333 self.is_list = None
334 self.singular_name = None
336 self.is_primitive = None
337 self.is_binary = None
338
339 def load(self, type_name, processor, context):
340 # Some common errors
341 if type_name == 'integer':
342 raise SwaggerError("The type for integer should be 'int'", context)
343
344 self.name = type_name
345 type_param = get_list_parameter_type(self.name)
346 self.is_list = type_param is not None
347 if self.is_list:
348 self.singular_name = type_param
349 self.lc_singular_name = type_param.lower()
350 else:
351 self.singular_name = self.name
352 self.lc_singular_name = self.name.lower()
353 self.is_primitive = self.singular_name in SWAGGER_PRIMITIVES
354 self.is_binary = (self.singular_name == 'binary')
355 processor.process_type(self, context)
356 return self
357
358
360 """Model of an operation on an API
361
362 See https://github.com/wordnik/swagger-core/wiki/API-Declaration#apis
363 """
364
365 required_fields = ['httpMethod', 'nickname', 'responseClass', 'summary']
366
367 def __init__(self):
368 self.http_method = None
369 self.nickname = None
370 self.nickname_lc = None
371 self.response_class = None
372 self.parameters = []
373 self.summary = None
374 self.notes = None
376 self.since = []
377
378 def load(self, op_json, processor, context):
379 context = context.next_stack(op_json, 'nickname')
380 validate_required_fields(op_json, self.required_fields, context)
381 self.http_method = op_json.get('httpMethod')
382 self.nickname = op_json.get('nickname')
383 self.nickname_lc = self.nickname.lower()
384 response_class = op_json.get('responseClass')
385 self.response_class = response_class and SwaggerType().load(
386 response_class, processor, context)
387 since = op_json.get('since') or []
388 self.since = ", ".join(since)
389
390 # Specifying WebSocket URL's is our own extension
391 self.is_websocket = op_json.get('upgrade') == 'websocket'
392 self.is_req = not self.is_websocket
393
394 if self.is_websocket:
395 self.websocket_protocol = op_json.get('websocketProtocol')
396 if self.http_method != 'GET':
397 raise SwaggerError(
398 "upgrade: websocket is only valid on GET operations",
399 context)
400
401 params_json = op_json.get('parameters') or []
402 self.parameters = [
403 Parameter().load(j, processor, context) for j in params_json]
405 p for p in self.parameters if p.is_type('query')]
408 p for p in self.parameters if p.is_type('path')]
411 p for p in self.parameters if p.is_type('header')]
415 self.is_binary_response = self.response_class.is_binary
416
417 # Body param is different, since there's at most one
419 p for p in self.parameters if p.is_type('body')]
420 if len(self.body_parameter) > 1:
421 raise SwaggerError("Cannot have more than one body param", context)
422 self.body_parameter = self.body_parameter and self.body_parameter[0]
424
425 self.summary = op_json.get('summary')
426 self.notes = op_json.get('notes')
427 err_json = op_json.get('errorResponses') or []
428 self.error_responses = [
429 ErrorResponse().load(j, processor, context) for j in err_json]
431 processor.process_operation(self, context)
432 return self
433
434
436 """Model of a single API in an API declaration.
437
438 See https://github.com/wordnik/swagger-core/wiki/API-Declaration
439 """
440
441 required_fields = ['path', 'operations']
442
443 def __init__(self,):
444 self.path = None
445 self.description = None
446 self.operations = []
447
448 def load(self, api_json, processor, context):
449 context = context.next_stack(api_json, 'path')
450 validate_required_fields(api_json, self.required_fields, context)
451 self.path = api_json.get('path')
452 self.description = api_json.get('description')
453 op_json = api_json.get('operations')
454 self.operations = [
455 Operation().load(j, processor, context) for j in op_json]
456 self.has_websocket = any(op.is_websocket for op in self.operations)
457 processor.process_api(self, context)
458 return self
459
460
461def get_list_parameter_type(type_string):
462 """Returns the type parameter if the given type_string is List[].
463
464 @param type_string: Type string to parse
465 @returns Type parameter of the list, or None if not a List.
466 """
467 list_match = re.match(r'^List\[(.*)\]$', type_string)
468 return list_match and list_match.group(1)
469
470
472 """Model of a Swagger property.
473
474 See https://github.com/wordnik/swagger-core/wiki/datatypes
475 """
476
477 required_fields = ['type']
478
479 def __init__(self, name):
480 self.name = name
481 self.type = None
482 self.description = None
483 self.required = None
484
485 def load(self, property_json, processor, context):
486 validate_required_fields(property_json, self.required_fields, context)
487 # Bit of a hack, but properties do not self-identify
488 context = context.next_stack({'name': self.name}, 'name')
489 self.description = property_json.get('description') or ''
490 self.required = property_json.get('required') or False
491
492 type = property_json.get('type')
493 self.type = type and SwaggerType().load(type, processor, context)
494
495 processor.process_property(self, context)
496 return self
497
498
500 """Model of a Swagger model.
501
502 See https://github.com/wordnik/swagger-core/wiki/datatypes
503 """
504
505 required_fields = ['description', 'properties']
506
507 def __init__(self):
508 self.id = None
509 self.id_lc = None
510 self.subtypes = []
511 self.__subtype_types = []
512 self.notes = None
513 self.description = None
514 self.__properties = None
515 self.__discriminator = None
516 self.__extends_type = None
517
518 def load(self, id, model_json, processor, context):
519 context = context.next_stack(model_json, 'id')
520 validate_required_fields(model_json, self.required_fields, context)
521 # The duplication of the model's id is required by the Swagger spec.
522 self.id = model_json.get('id')
523 self.id_lc = self.id.lower()
524 if id != self.id:
525 raise SwaggerError("Model id doesn't match name", context)
526 self.subtypes = model_json.get('subTypes') or []
527 if self.subtypes and context.version_less_than("1.2"):
528 raise SwaggerError("Type extension support added in Swagger 1.2",
529 context)
530 self.description = model_json.get('description')
531 props = model_json.get('properties').items() or []
532 self.__properties = [
533 Property(k).load(j, processor, context) for (k, j) in props]
534 self.__properties = sorted(self.__properties, key=lambda p: p.name)
535
536 discriminator = model_json.get('discriminator')
537
538 if discriminator:
539 if context.version_less_than("1.2"):
540 raise SwaggerError("Discriminator support added in Swagger 1.2",
541 context)
542
543 discr_props = [p for p in self.__properties if p.name == discriminator]
544 if not discr_props:
545 raise SwaggerError(
546 "Discriminator '%s' does not name a property of '%s'" % (
547 discriminator, self.id),
548 context)
549
550 self.__discriminator = discr_props[0]
551
552 self.model_json = json.dumps(model_json,
553 indent=2, separators=(',', ': '))
554
555 processor.process_model(self, context)
556 return self
557
558 def extends(self):
559 return self.__extends_type and self.__extends_type.id
560
561 def extends_lc(self):
562 return self.__extends_type and self.__extends_type.id_lc
563
564 def set_extends_type(self, extends_type):
565 self.__extends_type = extends_type
566
567 def set_subtype_types(self, subtype_types):
568 self.__subtype_types = subtype_types
569
570 def discriminator(self):
571 """Returns the discriminator, digging through base types if needed.
572 """
573 return self.__discriminator or \
575
576 def properties(self):
577 base_props = []
578 if self.__extends_type:
579 base_props = self.__extends_type.properties()
580 return base_props + self.__properties
581
582 def has_properties(self):
583 return len(self.properties()) > 0
584
585 def all_subtypes(self):
586 """Returns the full list of all subtypes, including sub-subtypes.
587 """
588 res = self.__subtype_types + \
589 [subsubtypes for subtype in self.__subtype_types
590 for subsubtypes in subtype.all_subtypes()]
591 return sorted(res, key=lambda m: m.id)
592
593 def has_subtypes(self):
594 """Returns True if type has any subtypes.
595 """
596 return len(self.subtypes) > 0
597
598
600 """Model class for an API Declaration.
601
602 See https://github.com/wordnik/swagger-core/wiki/API-Declaration
603 """
604
605 required_fields = [
606 'swaggerVersion', '_author', '_copyright', 'apiVersion', 'basePath',
607 'resourcePath', 'apis', 'models'
608 ]
609
610 def __init__(self):
611 self.swagger_version = None
612 self.author = None
613 self.copyright = None
614 self.api_version = None
615 self.base_path = None
616 self.resource_path = None
617 self.since = []
618 self.apis = []
619 self.models = []
620
621 def load_file(self, api_declaration_file, processor):
622 context = ParsingContext(None, [api_declaration_file])
623 try:
624 return self.__load_file(api_declaration_file, processor, context)
625 except SwaggerError:
626 raise
627 except Exception as e:
628 print("Error: ", traceback.format_exc(), file=sys.stderr)
629 raise SwaggerError(
630 "Error loading %s" % api_declaration_file, context, e)
631
632 def __load_file(self, api_declaration_file, processor, context):
633 with open(api_declaration_file) as fp:
634 self.load(json.load(fp), processor, context)
635
636 expected_resource_path = '/api-docs/' + \
637 os.path.basename(api_declaration_file) \
638 .replace(".json", ".{format}")
639
640 if self.resource_path != expected_resource_path:
641 print("%s != %s" % (self.resource_path, expected_resource_path),
642 file=sys.stderr)
643 raise SwaggerError("resourcePath has incorrect value", context)
644
645 return self
646
647 def load(self, api_decl_json, processor, context):
648 """Loads a resource from a single Swagger resource.json file.
649 """
650 # If the version doesn't match, all bets are off.
651 self.swagger_version = api_decl_json.get('swaggerVersion')
652 context = context.next(version=self.swagger_version)
653 if not self.swagger_version in SWAGGER_VERSIONS:
654 raise SwaggerError(
655 "Unsupported Swagger version %s" % self.swagger_version, context)
656
657 validate_required_fields(api_decl_json, self.required_fields, context)
658
659 self.author = api_decl_json.get('_author')
660 self.copyright = api_decl_json.get('_copyright')
661 self.api_version = api_decl_json.get('apiVersion')
662 self.base_path = api_decl_json.get('basePath')
663 self.resource_path = api_decl_json.get('resourcePath')
664 self.requires_modules = api_decl_json.get('requiresModules') or []
665 since = api_decl_json.get('since') or []
666 self.since = ", ".join(since)
667 api_json = api_decl_json.get('apis') or []
668 self.apis = [
669 Api().load(j, processor, context) for j in api_json]
670 paths = set()
671 for api in self.apis:
672 if api.path in paths:
673 raise SwaggerError("API with duplicated path: %s" % api.path, context)
674 paths.add(api.path)
675 self.has_websocket = any(api.has_websocket for api in self.apis)
676 models = api_decl_json.get('models').items() or []
677 self.models = [Model().load(id, json, processor, context)
678 for (id, json) in models]
679 self.models = sorted(self.models, key=lambda m: m.id)
680 # Now link all base/extended types
681 model_dict = dict((m.id, m) for m in self.models)
682 for m in self.models:
683 def link_subtype(name):
684 res = model_dict.get(name)
685 if not res:
686 raise SwaggerError("%s has non-existing subtype %s",
687 m.id, name)
688 res.set_extends_type(m)
689 return res;
690 if m.subtypes:
691 m.set_subtype_types([
692 link_subtype(subtype) for subtype in m.subtypes])
693 return self
694
695
697 """Model of an API listing in the resources.json file.
698 """
699
700 required_fields = ['path', 'description']
701
702 def __init__(self):
703 self.path = None
704 self.description = None
705 self.api_declaration = None
706
707 def load(self, api_json, processor, context):
708 context = context.next_stack(api_json, 'path')
709 validate_required_fields(api_json, self.required_fields, context)
710 self.path = api_json['path'].replace('{format}', 'json')
711 self.description = api_json['description']
712
713 if not self.path or self.path[0] != '/':
714 raise SwaggerError("Path must start with /", context)
715 processor.process_resource_api(self, context)
716 return self
717
718 def load_api_declaration(self, base_dir, processor):
719 self.file = (base_dir + self.path)
720 self.api_declaration = ApiDeclaration().load_file(self.file, processor)
721 processor.process_resource_api(self, [self.file])
722
723
725 """Model of Swagger's resources.json file.
726 """
727
728 required_fields = ['apiVersion', 'basePath', 'apis']
729
730 def __init__(self):
731 self.swagger_version = None
732 self.api_version = None
733 self.base_path = None
734 self.apis = None
735
736 def load_file(self, resource_file, processor):
737 context = ParsingContext(None, [resource_file])
738 try:
739 return self.__load_file(resource_file, processor, context)
740 except SwaggerError:
741 raise
742 except Exception as e:
743 print("Error: ", traceback.format_exc(), file=sys.stderr)
744 raise SwaggerError(
745 "Error loading %s" % resource_file, context, e)
746
747 def __load_file(self, resource_file, processor, context):
748 with open(resource_file) as fp:
749 return self.load(json.load(fp), processor, context)
750
751 def load(self, resources_json, processor, context):
752 # If the version doesn't match, all bets are off.
753 self.swagger_version = resources_json.get('swaggerVersion')
754 if not self.swagger_version in SWAGGER_VERSIONS:
755 raise SwaggerError(
756 "Unsupported Swagger version %s" % self.swagger_version, context)
757
758 validate_required_fields(resources_json, self.required_fields, context)
759 self.api_version = resources_json['apiVersion']
760 self.base_path = resources_json['basePath']
761 apis_json = resources_json['apis']
762 self.apis = [
763 ResourceApi().load(j, processor, context) for j in apis_json]
764 processor.process_resource_listing(self, context)
765 return self
766
767
768def validate_required_fields(json, required_fields, context):
769 """Checks a JSON object for a set of required fields.
770
771 If any required field is missing, a SwaggerError is raised.
772
773 @param json: JSON object to check.
774 @param required_fields: List of required fields.
775 @param context: Current context in the API.
776 """
777 missing_fields = [f for f in required_fields if not f in json]
778
779 if missing_fields:
780 raise SwaggerError(
781 "Missing fields: %s" % ', '.join(missing_fields), context)
const char * str
Definition: app_jack.c:150
def __init__(self, values)
def __init__(self, min_value, max_value)
def __load_file(self, api_declaration_file, processor, context)
def load(self, api_decl_json, processor, context)
def load_file(self, api_declaration_file, processor)
def load(self, api_json, processor, context)
def load(self, err_json, processor, context)
def set_subtype_types(self, subtype_types)
def set_extends_type(self, extends_type)
def load(self, id, model_json, processor, context)
def has_properties(self)
def load(self, op_json, processor, context)
def is_type(self, other_type)
def load(self, parameter_json, processor, context)
def version_less_than(self, ver)
def next(self, version=None, stack=None)
def next_stack(self, json, id_field)
def __init__(self, swagger_version, stack)
def __init__(self, name)
def load(self, property_json, processor, context)
def load_api_declaration(self, base_dir, processor)
def load(self, api_json, processor, context)
def load_file(self, resource_file, processor)
def load(self, resources_json, processor, context)
def __load_file(self, resource_file, processor, context)
def __init__(self, msg, context, cause=None)
def process_resource_listing(self, resource_listing, context)
def process_resource_api(self, resource_api, context)
def process_operation(self, operation, context)
def process_type(self, swagger_type, context)
def process_model(self, model, context)
def process_parameter(self, parameter, context)
def process_property(self, property, context)
def process_api(self, api, context)
def load(self, type_name, processor, context)
static int set(struct ast_channel *chan, const char *cmd, char *data, char *buf, size_t len)
Definition: func_logic.c:266
static int replace(struct ast_channel *chan, const char *cmd, char *data, struct ast_str **buf, ssize_t len)
Definition: func_strings.c:980
static int len(struct ast_channel *chan, const char *cmd, char *data, char *buf, size_t buflen)
def get_list_parameter_type(type_string)
def load_allowable_values(json, context)
def compare_versions(lhs, rhs)
def validate_required_fields(json, required_fields, context)
static int load_file(const char *filename, char **ret)
Read a TEXT file into a string and return the length.