diff --git a/README.rst b/README.rst index 65c36f65..ac01a13d 100644 --- a/README.rst +++ b/README.rst @@ -67,7 +67,7 @@ Schema rules See the :sample:`Basic_Config` sample schema. The test application contains further examples. -- Root object is always a :cpp:class:`ConfigDB::Database` +- Each schema is managed by a single :cpp:class:`ConfigDB::Database` instance - A database is always rooted in a directory - An optional **include** array annotation can be added to specify additional header files required for custom types used in the schema. - Contains one or more stores. The root (un-named) object is the primary store, with the filename **_root.json**. @@ -191,7 +191,7 @@ ConfigDB uses the **array** schema keyword to implement both *simple* arrays (co Simple arrays are accessed via the :cpp:class:`ConfigDB::Array` class. All elements must be of the same type. A **default** value may be specified which is applied automatically for uninitialised stores. The :cpp:func:`ConfigDB::Object::loadArrayDefaults` method may also be used during updates to load these default definitions. -The :cpp:class:`ConfigDB::ObjectArray` type can be used for arrays of objects or unions. Default values are not currently supported for these. +The :cpp:class:`ConfigDB::ObjectArray` type can be used for arrays of objects or unions. Default values are not supported for these. .. important:: @@ -229,6 +229,41 @@ The code generator produces an **asXXX** method for each type of object which ca The corresponding Union Updater class has a :cpp:func:`ConfigDB::Union::setTag` method. This changes the stored object type and initialises it to default values. This is done even if the tag value doesn't change so can be used to 'reset' an object to defaults. The code generator produces a **toXXX** method which sets the tag and returns the appropriate object type. +Note that the root database object may also be a union. For example:: + + { + "$schema": "http://json-schema.org/draft-07/schema#", + "oneOf": [ + { + "type": "object", + "title": "request", + "properties": { + "method": { + "type": "string" + }, + "args": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + { + "type": "object", + "title": "response", + "properties": { + "code": { + "type": "integer" + }, + "message": { + "type": "string" + } + } + } + ] + } + Re-using objects ~~~~~~~~~~~~~~~~ diff --git a/test/modules/Union.cpp b/test/modules/Union.cpp index 5d3012a3..5c47dba3 100644 --- a/test/modules/Union.cpp +++ b/test/modules/Union.cpp @@ -4,6 +4,7 @@ #include #include +#include #include namespace json @@ -160,6 +161,20 @@ class UnionTest : public TestGroup } Serial << "unionStore: " << us << endl; } + + TEST_CASE("Root Union") + { + DEFINE_FSTR_LOCAL(expectedRequest, "{\"request\":{\"args\":[\"all\"],\"method\":\"query\"}}") + DEFINE_FSTR_LOCAL(expectedResponse, "{\"response\":{\"code\":-1,\"message\":\"undefined\"}}") + TestRootUnion db("dummy"); + TestRootUnion::Root root(db); + REQUIRE_EQ(exportObject(root), expectedRequest); + if(auto update = root.update()) { + update.toResponse(); + REQUIRE_EQ(exportObject(root), expectedResponse); + root.clearDirty(); + } + } } }; diff --git a/test/modules/Update.cpp b/test/modules/Update.cpp index 4368bbf6..4e33ad5c 100644 --- a/test/modules/Update.cpp +++ b/test/modules/Update.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include class UpdateTest : public TestGroup @@ -239,6 +240,24 @@ class UpdateTest : public TestGroup } Serial << "arrayStore: " << arrayStore << endl; } + + TEST_CASE("Root Array") + { + DEFINE_FSTR_LOCAL( + expected, "[{\"code\":100,\"value\":\"This is for value 100\"},{\"code\":50,\"value\":\"Value 50.\"}]") + TestRootArray db("root-array"); + TestRootArray::Root root(db); + if(auto update = root.update()) { + auto item = update.addItem(); + item.setCode(100); + item.setValue("This is for value 100"); + item = update.addItem(); + item.setCode(50); + item.setValue("Value 50."); + update.clearDirty(); + } + REQUIRE_EQ(exportObject(root), expected); + } } }; diff --git a/test/test-root-array.cfgdb b/test/test-root-array.cfgdb new file mode 100644 index 00000000..c8406082 --- /dev/null +++ b/test/test-root-array.cfgdb @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "array", + "items": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "value": { + "type": "string" + } + } + } +} \ No newline at end of file diff --git a/test/test-root-union.cfgdb b/test/test-root-union.cfgdb new file mode 100644 index 00000000..e3a2e442 --- /dev/null +++ b/test/test-root-union.cfgdb @@ -0,0 +1,38 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "oneOf": [ + { + "type": "object", + "title": "request", + "properties": { + "method": { + "type": "string", + "default": "query" + }, + "args": { + "type": "array", + "items": { + "type": "string" + }, + "default": [ + "all" + ] + } + } + }, + { + "type": "object", + "title": "response", + "properties": { + "code": { + "type": "integer", + "default": -1 + }, + "message": { + "type": "string", + "default": "undefined" + } + } + } + ] +} \ No newline at end of file diff --git a/tools/dbgen.py b/tools/dbgen.py index 5a647cd0..668ebe04 100644 --- a/tools/dbgen.py +++ b/tools/dbgen.py @@ -719,9 +719,44 @@ def calculate_props(props: dict, path: str): return db -def parse_properties(path: str, parent_prop: Property, properties: dict): +def parse_properties(path: str, parent_prop: Property, parent_fields: dict): + properties = parent_fields.get('properties', {}) for key, fields in properties.items(): - parse_property(f'{path}/{key}', parent_prop, key, fields) + parse_property(f'{path}/properties/{key}', parent_prop, key, fields) + + +def parse_oneof(path: str, union_prop: ObjectProperty, fields: dict): + if 'default' in fields: + raise ValueError('Union default not supported') + if 'properties' in fields: + raise ValueError('Union may not have properties') + for i, opt in enumerate(fields['oneOf']): + prop = parse_property(f'{path}/oneOf/{i}', union_prop, opt.get('title'), opt) + if not prop.obj: + raise ValueError(f'Union "{union_prop.name}" option type must be *object*') + if not prop.id or not prop.name or not prop.obj.typename: + raise ValueError(f'Union "{union_prop.name}" option requires title or $ref') + if union_prop.obj.max_object_size == 0: + raise ValueError('Union contains only empty objects') + prop = Property(union_prop, 'tag', { + 'type': 'integer', + 'minimum': 0, + 'maximum': len(union_prop.obj.object_properties) - 1 + }) + union_prop.obj.properties.append(prop) + + +def parse_array(path: str, array_prop: ObjectProperty, fields: dict): + items = fields.get('items') + if not items: + raise ValueError(f'Missing "items" property for array') + items_prop = parse_property(f'{path}/items', array_prop, f'{array_prop.typename}Item', items) + if items_prop.ctype_override: + items_prop.name = make_identifier(items_prop.ctype_override) + if array_prop.obj.is_object_array: + if 'default' in fields: + raise ValueError('ObjectArray default not supported') + array_prop.obj.default = items_prop.validate_type(fields.get('default'), 'default[]') def parse_property(path: str, parent_prop: Property, key: str, fields: dict) -> Property: @@ -834,41 +869,17 @@ def create_object_and_property(Class) -> Property: if 'default' in fields: raise ValueError('Object default not supported (use default on properties)') object_prop = create_object_and_property(Object) - parse_properties(f'{path}/properties', object_prop, fields.get('properties', {})) + parse_properties(path, object_prop, fields) return object_prop if prop_type == 'array': - items = fields.get('items') - if not items: - raise ValueError(f'Missing "items" property for array') array_prop = create_object_and_property(Array) - items_prop = parse_property(f'{path}/items', array_prop, f'{array_prop.typename}Item', items) - if items_prop.ctype_override: - items_prop.name = make_identifier(items_prop.ctype_override) - if array_prop.obj.is_object_array: - if 'default' in fields: - raise ValueError('ObjectArray default not supported') - array_prop.obj.default = items_prop.validate_type(fields.get('default'), 'default[]') + parse_array(path, array_prop, fields) return array_prop if prop_type == 'union': - if 'default' in fields: - raise ValueError('Union default not supported') union_prop = create_object_and_property(Union) - for i, opt in enumerate(fields['oneOf']): - prop = parse_property(f'{path}/oneOf/{i}', union_prop, opt.get('title'), opt) - if not prop.obj: - raise ValueError(f'Union "{union_prop.name}" option type must be *object*') - if not prop.id or not prop.name or not prop.obj.typename: - raise ValueError(f'Union "{union_prop.name}" option requires title or $ref') - if union_prop.obj.max_object_size == 0: - raise ValueError('Union contains only empty objects') - prop = Property(union_prop, 'tag', { - 'type': 'integer', - 'minimum': 0, - 'maximum': len(union_prop.obj.object_properties) - 1 - }) - union_prop.obj.properties.append(prop) + parse_oneof(path, union_prop, fields) return union_prop raise ValueError('Bad type ' + prop_type) @@ -878,13 +889,26 @@ def create_object_and_property(Class) -> Property: def parse_database(database: Database): '''Validate and parse schema into python objects''' - database.include = database.schema.get('include', set()) - root_obj = Object(database, '', None, database.schema_id) - database.schema['object'] = root_obj - root = ObjectProperty(database, '', {}, root_obj) - database.object_properties.append(root) - root.is_store = True - parse_properties(f'/{database.name}/properties', root, database.schema.get('properties', {})) + + path = f'/{database.name}' + try: + database.include = database.schema.get('include', set()) + if {'oneOf', 'type'} & database.schema.keys(): + ptype = get_ptype(database.schema) + ObjectType, parser = { + 'union': (Union, parse_oneof), + 'array': (Array, parse_array), + 'object': (Object, parse_properties), + } [ptype] + root_obj = ObjectType(database, '', None, database.schema_id) + database.schema['object'] = root_obj + root = ObjectProperty(database, '', {}, root_obj) + database.object_properties.append(root) + root.is_store = True + parser(path, root, database.schema) + except ValueError as e: + raise RuntimeError(path) from e + def generate_database(db: Database) -> CodeLines: '''Generate content for entire database'''