Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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**.
Expand Down Expand Up @@ -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::

Expand Down Expand Up @@ -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
~~~~~~~~~~~~~~~~
Expand Down
15 changes: 15 additions & 0 deletions test/modules/Union.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include <ConfigDBTest.h>
#include <test-config-union.h>
#include <test-root-union.h>
#include <ConfigDB/Json/Format.h>

namespace json
Expand Down Expand Up @@ -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();
}
}
}
};

Expand Down
19 changes: 19 additions & 0 deletions test/modules/Update.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <ConfigDBTest.h>
#include <test-config-range.h>
#include <test-config-ref.h>
#include <test-root-array.h>
#include <ConfigDB/Json/Format.h>

class UpdateTest : public TestGroup
Expand Down Expand Up @@ -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);
}
}
};

Expand Down
15 changes: 15 additions & 0 deletions test/test-root-array.cfgdb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "array",
"items": {
"type": "object",
"properties": {
"code": {
"type": "integer"
},
"value": {
"type": "string"
}
}
}
}
38 changes: 38 additions & 0 deletions test/test-root-union.cfgdb
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
]
}
96 changes: 60 additions & 36 deletions tools/dbgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand All @@ -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'''
Expand Down
Loading