From ced5567f2597861114e9a13e7303c43e0563147d Mon Sep 17 00:00:00 2001 From: mikee47 Date: Wed, 12 Aug 2026 08:20:42 +0100 Subject: [PATCH 1/2] Support root Union --- README.rst | 37 +++++++++++++++++++++++- test/modules/Union.cpp | 15 ++++++++++ test/test-root-union.cfgdb | 38 +++++++++++++++++++++++++ tools/dbgen.py | 58 ++++++++++++++++++++++++-------------- 4 files changed, 126 insertions(+), 22 deletions(-) create mode 100644 test/test-root-union.cfgdb diff --git a/README.rst b/README.rst index 65c36f65..b075ec73 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**. @@ -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/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..6fe10757 100644 --- a/tools/dbgen.py +++ b/tools/dbgen.py @@ -719,9 +719,31 @@ 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_property(path: str, parent_prop: Property, key: str, fields: dict) -> Property: @@ -834,7 +856,7 @@ 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': @@ -852,23 +874,8 @@ def create_object_and_property(Class) -> Property: 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) @@ -879,12 +886,21 @@ 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) + ObjectType = Union if 'oneOf' in database.schema else Object + 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 - parse_properties(f'/{database.name}/properties', root, database.schema.get('properties', {})) + path = f'/{database.name}' + try: + if ObjectType is Union: + parse_oneof(path, root, database.schema) + else: + parse_properties(path, root, database.schema) + except ValueError as e: + raise RuntimeError(path) from e + def generate_database(db: Database) -> CodeLines: '''Generate content for entire database''' From 770af9082be5cd4526bb9ef55da063a618e6d5a8 Mon Sep 17 00:00:00 2001 From: mikee47 Date: Wed, 12 Aug 2026 13:08:47 +0100 Subject: [PATCH 2/2] Support root array --- README.rst | 2 +- test/modules/Update.cpp | 19 +++++++++++++++ test/test-root-array.cfgdb | 15 ++++++++++++ tools/dbgen.py | 50 ++++++++++++++++++++++---------------- 4 files changed, 64 insertions(+), 22 deletions(-) create mode 100644 test/test-root-array.cfgdb diff --git a/README.rst b/README.rst index b075ec73..ac01a13d 100644 --- a/README.rst +++ b/README.rst @@ -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:: 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/tools/dbgen.py b/tools/dbgen.py index 6fe10757..668ebe04 100644 --- a/tools/dbgen.py +++ b/tools/dbgen.py @@ -746,6 +746,19 @@ def parse_oneof(path: str, union_prop: ObjectProperty, fields: dict): 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: '''If property contains a reference, deal with it @@ -860,17 +873,8 @@ def create_object_and_property(Class) -> Property: 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': @@ -885,19 +889,23 @@ 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()) - ObjectType = Union if 'oneOf' in database.schema else Object - 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 + path = f'/{database.name}' try: - if ObjectType is Union: - parse_oneof(path, root, database.schema) - else: - parse_properties(path, root, database.schema) + 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