1
2# =====================================
3# Copyright 2017-2023, Andrew Lindesay
4# Distributed under the terms of the MIT License.
5# =====================================
6
7# common material related to generation of schema-generated artifacts.
8
9import datetime
10
11
12# The possible JSON types
13JSON_TYPE_STRING = "string"
14JSON_TYPE_OBJECT = "object"
15JSON_TYPE_ARRAY = "array"
16JSON_TYPE_BOOLEAN = "boolean"
17JSON_TYPE_INTEGER = "integer"
18JSON_TYPE_NUMBER = "number"
19
20
21# The possible C++ types
22CPP_TYPE_STRING = "BString"
23CPP_TYPE_ARRAY = "BObjectList"
24CPP_TYPE_BOOLEAN = "bool"
25CPP_TYPE_INTEGER = "int64"
26CPP_TYPE_NUMBER = "double"
27
28
29# The possible C++ default values
30CPP_DEFAULT_STRING = "NULL"
31CPP_DEFAULT_OBJECT = "NULL"
32CPP_DEFAULT_ARRAY = "NULL"
33CPP_DEFAULT_BOOLEAN = "false"
34CPP_DEFAULT_INTEGER = "0"
35CPP_DEFAULT_NUMBER = "0.0"
36
37
38def uniondicts(d1, d2):
39    d = dict(d1)
40    d.update(d2)
41    return d
42
43
44def javatypetocppname(javaname):
45    return javaname[javaname.rindex('.')+1:]
46
47
48def propnametocppname(propname):
49    return propname[0:1].upper() + propname[1:]
50
51
52def propnametocppmembername(propname):
53    return "f" + propnametocppname(propname)
54
55def propmetadatatocppdefaultvalue(propmetadata):
56    type = propmetadata['type']
57
58    if type == JSON_TYPE_STRING:
59        return CPP_DEFAULT_STRING
60    if type == JSON_TYPE_BOOLEAN:
61        return CPP_DEFAULT_BOOLEAN
62    if type == JSON_TYPE_INTEGER:
63        return CPP_DEFAULT_INTEGER
64    if type == JSON_TYPE_NUMBER:
65        return CPP_DEFAULT_NUMBER
66    if type == JSON_TYPE_OBJECT:
67        return CPP_DEFAULT_OBJECT
68    if type == JSON_TYPE_ARRAY:
69        return CPP_DEFAULT_ARRAY
70
71    raise Exception('unknown json-schema type [' + type + ']')
72
73def propmetadatatocpptypename(propmetadata):
74    type = propmetadata['type']
75
76    if type == JSON_TYPE_STRING:
77        return CPP_TYPE_STRING
78    if type == JSON_TYPE_BOOLEAN:
79        return CPP_TYPE_BOOLEAN
80    if type == JSON_TYPE_INTEGER:
81        return CPP_TYPE_INTEGER
82    if type == JSON_TYPE_NUMBER:
83        return CPP_TYPE_NUMBER
84    if type == JSON_TYPE_OBJECT:
85        javatype = propmetadata['javaType']
86
87        if not javatype or 0 == len(javatype):
88            raise Exception('missing "javaType" field')
89
90        return javatypetocppname(javatype)
91
92    if type == JSON_TYPE_ARRAY:
93        itemsmetadata = propmetadata['items']
94        itemstype = itemsmetadata['type']
95
96        if not itemstype or 0 == len(itemstype):
97            raise Exception('missing "type" field')
98
99        if itemstype == JSON_TYPE_OBJECT:
100            itemsjavatype = itemsmetadata['javaType']
101            if not itemsjavatype or 0 == len(itemsjavatype):
102                raise Exception('missing "javaType" field')
103            return "%s<%s>" % (CPP_TYPE_ARRAY, javatypetocppname(itemsjavatype))
104
105        if itemstype == JSON_TYPE_STRING:
106            return "%s<%s>" % (CPP_TYPE_ARRAY, CPP_TYPE_STRING)
107
108        raise Exception('unsupported type [%s]' % itemstype)
109
110    raise Exception('unknown json-schema type [' + type + ']')
111
112
113def propmetadatatypeisscalar(propmetadata):
114    type = propmetadata['type']
115    return type == JSON_TYPE_BOOLEAN or type == JSON_TYPE_INTEGER or type == JSON_TYPE_NUMBER
116
117
118def writetopcomment(f, inputfilename, variant):
119    f.write((
120                '/*\n'
121                ' * Generated %s Object\n'
122                ' * source json-schema : %s\n'
123                ' * generated at : %s\n'
124                ' */\n'
125            ) % (variant, inputfilename, datetime.datetime.now().isoformat()))
126
127def collect_all_objects(schema: dict[str, any]) -> list[dict[str, any]]:
128    assembly = dict[str, dict[str, any]]()
129
130    def accumulate_all_objects(obj: dict[str, any]) -> None:
131        assembly[obj["cppname"]] = obj
132
133        for prop_name, prop in obj['properties'].items():
134            if JSON_TYPE_ARRAY == prop["type"]:
135                array_items = prop['items']
136
137                if JSON_TYPE_OBJECT == array_items["type"] and not array_items["cppname"] in assembly:
138                    accumulate_all_objects(array_items)
139
140            if JSON_TYPE_OBJECT == prop["type"] and not prop["cppname"] in assembly:
141                accumulate_all_objects(prop)
142
143    accumulate_all_objects(schema)
144    result = list(assembly.values())
145    result.sort(key= lambda v: v["cppname"])
146
147    return result
148
149
150def augment_schema(schema: dict[str, any]) -> None:
151    """This function will take the data from the JSON schema and will overlay any
152    specific information required for rendering the templates.
153    """
154
155    def derive_cpp_classname(obj: dict[str, any]) -> str:
156        obj_type = obj['type']
157
158        if obj_type != JSON_TYPE_OBJECT:
159            raise Exception('expecting object, but was [' + obj_type + ']')
160
161        java_type = obj['javaType']
162
163        return javatypetocppname(java_type)
164
165    def augment_property(prop: dict[str, any]) -> None:
166        prop_type = prop['type']
167
168        prop["cpptype"] = propmetadatatocpptypename(prop)
169
170        is_scalar_type = propmetadatatypeisscalar(prop)
171        is_array_type = (prop_type == JSON_TYPE_ARRAY)
172
173        prop["iscppscalartype"] = is_scalar_type
174        prop["isarray"] = is_array_type
175        prop["isstring"] = JSON_TYPE_STRING == prop_type
176        prop["isboolean"] = JSON_TYPE_BOOLEAN == prop_type
177        prop["isnumber"] = JSON_TYPE_NUMBER == prop_type
178        prop["isinteger"] = JSON_TYPE_INTEGER == prop_type
179        prop["isobject"] = JSON_TYPE_OBJECT == prop_type
180        prop["iscppnonscalarnoncollectiontype"] = not is_scalar_type and not is_array_type
181        prop["toplevelcppname"] = top_level_cpp_name
182        prop["cppdefaultvalue"] = propmetadatatocppdefaultvalue(prop)
183
184        if not prop_type or 0 == len(prop_type):
185            raise Exception('missing "type" field')
186
187        if JSON_TYPE_ARRAY == prop_type:
188            array_items = prop['items']
189            array_items_type = array_items["type"]
190
191            if array_items_type == JSON_TYPE_OBJECT:
192                augment_object(array_items)
193            else:
194                augment_property(array_items)
195
196        if JSON_TYPE_OBJECT == prop_type:
197            augment_object(prop)
198
199    def augment_object(obj: dict[str, any]) -> None:
200
201        def collect_referenced_class_names() -> list[str]:
202            result = set()
203            properties = obj['properties'].items()
204
205            if len(properties) > 15:
206                raise RuntimeError("an object has more than 15 properties"
207                                   " which is not allowed")
208
209            for _, prop in obj['properties'].items():
210                if prop['type'] == JSON_TYPE_ARRAY:
211                    array_items = prop['items']
212                    if array_items['type'] == JSON_TYPE_OBJECT:
213                        result.add(array_items['cppname'])
214                if prop['type'] == JSON_TYPE_OBJECT:
215                    result.add(prop['cppname'])
216
217            result_ordered = list(result)
218            result_ordered.sort()
219            return result_ordered
220
221        def has_any_list_properties() -> bool:
222            for _, prop in obj['properties'].items():
223                if prop['type'] == JSON_TYPE_ARRAY:
224                    return True
225            return False
226
227        obj_cpp_classname = derive_cpp_classname(obj)
228        obj["cppname"] = obj_cpp_classname
229        obj["cppnameupper"] = obj_cpp_classname.upper()
230        obj["cpptype"] = obj_cpp_classname
231        obj["hasanylistproperties"] = has_any_list_properties()
232
233        # allows the object to know the top level object that contains it
234        obj["toplevelcppname"] = top_level_cpp_name
235
236        properties = obj['properties'].items()
237
238        for prop_name, prop in properties:
239            prop["cppname"] = propnametocppname(prop_name)
240            prop["cppmembername"] = propnametocppmembername(prop_name)
241            augment_property(prop)
242
243        # mustache is not able to iterate over a dictionary; it can only
244        # iterate over a list where each item in the list has some properties.
245
246        property_array = sorted(
247            [{
248                "name": k,
249                "property": v,
250                "cppobjectname": obj_cpp_classname
251                    # ^ this is the name of the containing object
252            } for k,v in properties],
253            key= lambda item: item["name"]
254        )
255
256        for i in range(len(property_array)):
257            property_array[i]["cppbitmaskexpression"] = "(1 << %d)" % i
258
259        if 0 != len(property_array):
260            property_array[0]["isfirst"] = True
261            property_array[len(property_array) - 1]["islast"] = True
262
263        obj["propertyarray"] = property_array
264        obj["referencedclasscpptypes"] = collect_referenced_class_names()
265
266    top_level_cpp_name = derive_cpp_classname(schema)
267    augment_object(schema)