diff --git a/yang-modules/test/test-data.json b/yang-modules/test/test-data.json new file mode 100644 index 00000000..2d83b8a0 --- /dev/null +++ b/yang-modules/test/test-data.json @@ -0,0 +1,34 @@ +{ + "test:llistB": ["::1", "127.0.0.1"], + "test:leafX": 53531, + "test:contA": { + "leafB": 9, + "listA": [{ + "leafE": "C0FFEE", + "leafF": true, + "contD": { + "leafG": "foo1-bar", + "contE": { + "leafJ": [null], + "leafP": 10 + } + } + }, + { + "leafE": "ABBA", + "leafW": 9, + "leafF": false + }], + "testb:leafS": + "/test:contA/listA[leafE='C0FFEE'][leafF='true']/contD/contE/leafP", + "testb:leafR": "C0FFEE", + "testb:leafT": "test:CC-BY", + "testb:leafV": 99, + "testb:leafN": "hi!" + }, + "test:contT": { + "bits": "dos cuatro", + "decimal64": 4.50, + "enumeration": "Hearts" + } +} diff --git a/yang-modules/test/test.py b/yang-modules/test/test.py new file mode 100644 index 00000000..1b589205 --- /dev/null +++ b/yang-modules/test/test.py @@ -0,0 +1,13 @@ +import json +from yangson import DataModel +from yangson.enumerations import ContentType +import xml.etree.ElementTree as ET + +dm = DataModel.from_file( + 'yang-library.json', ['.', '../ietf']) +with open('test-data.json') as infile: + ri = json.load(infile) +inst = dm.from_raw(ri) +root = inst.to_xml() + +print(ET.tostring(root)) diff --git a/yangson/datamodel.py b/yangson/datamodel.py index bcf9d49d..739ca13f 100644 --- a/yangson/datamodel.py +++ b/yangson/datamodel.py @@ -27,11 +27,11 @@ from typing import Optional, Tuple import xml.etree.ElementTree as ET from .enumerations import ContentType -from .exceptions import BadYangLibraryData +from .exceptions import BadYangLibraryData, BadRootNode from .instance import (InstanceRoute, InstanceIdParser, ResourceIdParser, RootNode) from .schemadata import SchemaData, SchemaContext -from .schemanode import DataNode, SchemaTreeNode, RawObject, SchemaNode +from .schemanode import DataNode, SchemaTreeNode, RawObject, SchemaNode, ContainerNode from .typealiases import DataPath, SchemaPath @@ -80,7 +80,7 @@ def __init__(self, yltxt: str, mod_path: Tuple[str] = (".",), self.yang_library = json.loads(yltxt) except json.JSONDecodeError as e: raise BadYangLibraryData(str(e)) from None - self.schema_data = SchemaData(self.yang_library, mod_path) + self.schema_data = SchemaData(self.yang_library, list(mod_path)) self.schema = SchemaTreeNode(self.schema_data) self.schema._ctype = ContentType.all self._build_schema() @@ -88,6 +88,43 @@ def __init__(self, yltxt: str, mod_path: Tuple[str] = (".",), "Data model ID: " + self.yang_library["ietf-yang-library:modules-state"] ["module-set-id"]) + self.subschema = {} + + def add_submodel(self, container: ContainerNode, submodel: "DataModel"): + if container.schema_root() != self.schema: + raise BadRootNode(container.iname()) + + # update yang library + yl_modules = self.yang_library['ietf-yang-library:modules-state']['module'] + existing = list() + for module in yl_modules: + existing.append((module['name'], module['revision'])) + + sm_modules = submodel.yang_library['ietf-yang-library:modules-state']['module'] + for module in sm_modules: + if (module['name'], module['revision']) not in existing: + yl_modules.append(module) + + # update schema data + self.schema_data.add(submodel.schema_data) + + # update container + for subchild in submodel.schema.children: + container.children.append(subchild) + subchild.parent = container + + self.schema.subschema[(subchild.name, subchild.ns)] = subchild.data_path() + if subchild.mandatory: + container._mandatory_children.add(subchild) + + if self.schema.description.startswith('Data model ID: '): + self.schema.description = ( + "Data model ID: " + + self.yang_library["ietf-yang-library:modules-state"] + ["module-set-id"]) + + # rebuild schema patterns + self.schema._make_schema_patterns() def module_set_id(self) -> str: """Compute unique id of YANG modules comprising the data model. @@ -174,9 +211,25 @@ def clear_val_counters(self): self.schema.clear_val_counters() def parse_instance_id(self, text: str) -> InstanceRoute: + split = text.split('/') + ns, sep, name = split[1].partition(':') + + if (name, ns) in self.schema.subschema: + text = self.schema.subschema[(name, ns)] + if len(split) >= 2: + text = text + '/' + '/'.join(split[2:]) + return InstanceIdParser(text).parse() def parse_resource_id(self, text: str) -> InstanceRoute: + split = text.split('/') + ns, sep, name = split[1].partition(':') + + if (name, ns) in self.schema.subschema: + text = self.schema.subschema[(name, ns)] + if len(split) >= 2: + text = text + '/' + '/'.join(split[2:]) + return ResourceIdParser(text, self.schema).parse() def schema_digest(self) -> str: diff --git a/yangson/exceptions.py b/yangson/exceptions.py index 515b11b9..fd4674d4 100644 --- a/yangson/exceptions.py +++ b/yangson/exceptions.py @@ -387,6 +387,10 @@ def __str__(self) -> str: return f"{prefix}{self.name} under {super().__str__()}" +class BadRootNode(SchemaNodeException): + '''Wrong root node of schema node''' + pass + class BadSchemaNodeType(SchemaNodeException): """A schema node is of a wrong type.""" diff --git a/yangson/instance.py b/yangson/instance.py index 8fd5ff5e..f38cb8c8 100644 --- a/yangson/instance.py +++ b/yangson/instance.py @@ -48,7 +48,8 @@ __all__ = ["InstanceNode", "RootNode", "ObjectMember", "ArrayEntry", "InstanceIdParser", "ResourceIdParser", "InstanceRoute", - "InstanceException", "InstanceValueError", "NonexistentInstance"] + "InstanceException", "InstanceValueError", "NonexistentInstance", + "OutputFilter"] class OutputFilter: @@ -236,7 +237,8 @@ def ita(): return ita() if isinstance(self.value, ObjectValue): return iter(self._member_names()) - raise InstanceValueError(self.json_pointer(), + raise InstanceValueError( + self.json_pointer(), "{} is a scalar instance".format(str(type(self.value)))) def is_internal(self) -> bool: @@ -340,8 +342,8 @@ def goto(self, iroute: "InstanceRoute") -> "InstanceNode": inst = sel.goto_step(inst) return inst - def peek(self, iroute: "InstanceRoute") -> Optional[Value]: - """Return a value within the receiver's subtree. + def full_peek(self, iroute: "InstanceRoute") -> Optional[Tuple]: + """Return a value and schema within the receiver's subtree. Args: iroute: Instance route (relative to the receiver). @@ -349,10 +351,18 @@ def peek(self, iroute: "InstanceRoute") -> Optional[Value]: val = self.value sn = self.schema_node for sel in iroute: - val, sn = sel.peek_step(val, sn) if val is None: - return None - return val + return (None, None) + val, sn = sel.peek_step(val, sn) + return (val, sn) + + def peek(self, iroute: "InstanceRoute") -> Optional[Value]: + """Return a value within the receiver's subtree. + + Args: + iroute: Instance route (relative to the receiver). + """ + return self.full_peek(iroute)[0] def validate(self, scope: ValidationScope = ValidationScope.all, ctype: ContentType = ContentType.config) -> None: @@ -486,9 +496,10 @@ def to_xml(self, filter: OutputFilter = OutputFilter(), elem: ET.Element = None) if elem is None: element = ET.Element(self.schema_node.name) - module = self.schema_data.modules_by_name.get(self.schema_node.ns) + ns = self.schema_node.ns + module = self.schema_data.modules_by_name.get(ns) if not module: - raise MissingModuleNamespace(self.schema_node.ns) + raise MissingModuleNamespace(ns if ns else 'None') element.attrib['xmlns'] = module.xml_namespace else: element = elem @@ -565,9 +576,10 @@ def _member_names(self) -> List[InstanceName]: return [m for m in self.value if not m.startswith("@")] def _member(self, name: InstanceName) -> "ObjectMember": - pts = name.partition(":") - if pts[1] and pts[0] == self.namespace: - name = pts[2] + if not type(self) is RootNode: + pts = name.partition(":") + if pts[1] and pts[0] == self.namespace: + name = pts[2] sibs = self.value.copy() try: return ObjectMember( @@ -678,6 +690,7 @@ def __init__(self, value: Value, schema_node: "DataNode", schema_data: "SchemaData", timestamp: datetime): super().__init__("/", value, None, schema_node, timestamp) self.schema_data = schema_data + """Dictionary of subschema root qnames to subschema paths""" def up(self) -> None: """Override the superclass method. @@ -1005,6 +1018,20 @@ def peek_step(self, val: ObjectValue, val: Current value (object). sn: Current schema node. """ + qn = (self.name, self.namespace) + if isinstance(sn, SchemaTreeNode) and qn in sn.subschema: + path = sn.subschema.get((self.name, self.namespace)) + c_schema = sn + c_value = val + try: + for dp in path.split('/'): + name, sep, ns = dp.partition(':') + c_schema = c_schema.get_data_child(name, ns if ns else None) + c_value = c_value[c_schema.iname()] + return c_value, c_schema + except (IndexError, KeyError, TypeError): + return (None, c_schema) + cn = sn.get_data_child(self.name, self.namespace) try: return (val[cn.iname()], cn) @@ -1017,6 +1044,15 @@ def goto_step(self, inst: InstanceNode) -> InstanceNode: Args: inst: Current instance. """ + sn = inst.schema_node + qn = (self.name, self.namespace) + + if isinstance(sn, SchemaTreeNode)and qn in sn.subschema: + path = sn.subschema.get((self.name, self.namespace)) + child = inst + for dp in path[1:].split('/'): + child = child[dp] + return child return inst[self.iname()] @@ -1323,4 +1359,5 @@ def _key_predicates(self) -> EntryKeys: AnyContentNode,AnydataNode, CaseNode, ChoiceNode, DataNode, InternalNode, LeafNode, LeafListNode, ListNode, - RpcActionNode, SequenceNode, TerminalNode) + RpcActionNode, SchemaTreeNode, SequenceNode, TerminalNode, + ) diff --git a/yangson/schemadata.py b/yangson/schemadata.py index 912a1ad2..24f904eb 100644 --- a/yangson/schemadata.py +++ b/yangson/schemadata.py @@ -105,11 +105,22 @@ def __init__(self, yang_lib: Dict[str, Any], mod_path: List[str]) -> None: """Dictionary of module data.""" self.modules_by_name = {} # type: Dict[str, ModuleData] """Dictionary of module data by module name.""" - self.modules_by_ns = {} + self.modules_by_ns = {} # type: Dict[str, ModuleData] + """Dictionary of module data by xml namespace.""" self._module_sequence = [] # type: List[ModuleId] """List that defines the order of module processing.""" self._from_yang_library(yang_lib) + def add(self, child: "SchemaData"): + """Add the schemadata of a subschema to this one""" + self.identity_adjs = {**self.identity_adjs, **child.identity_adjs} + self.implement = {**self.implement, **child.implement} + self.module_search_path.extend(child.module_search_path) + self.modules = {**self.modules, **child.modules} + self.modules_by_name = {**self.modules_by_name, **child.modules_by_name} + self.modules_by_ns = {**self.modules_by_ns, **child.modules_by_ns} + self._module_sequence.extend(child._module_sequence) + def _from_yang_library(self, yang_lib: Dict[str, Any]) -> None: """Set the schema structures from YANG library data. diff --git a/yangson/schemanode.py b/yangson/schemanode.py index 27e51653..8afc14b9 100644 --- a/yangson/schemanode.py +++ b/yangson/schemanode.py @@ -467,10 +467,7 @@ def from_raw(self, rval: RawObject, jptr: JSONPointer = "", allow_nodata: bool = res[qn] = self._process_metadata(rval[qn], jptr) else: cn = self._iname2qname(qn) - if allow_nodata: - ch = self.get_child(*cn) - else: - ch = self.get_data_child(*cn) + ch = self.get_data_child(*cn) npath = jptr + "/" + qn if ch is None: raise RawMemberError(npath) @@ -799,11 +796,15 @@ def __init__(self, schemadata: "SchemaData" = None): super().__init__() self.annotations: Dict[QualName, Annotation] = {} self.schema_data = schemadata + self.subschema: Dict[QualName, "InstanceRoute"] = {} def data_parent(self) -> InternalNode: """Override the superclass method.""" return self.parent + def add_subschema(self, container: SchemaNode): + self.subschema[container.qual_name] = container.data_path() + def _annotation_stmt(self, stmt: Statement, sctx: SchemaContext) -> None: """Handle annotation statement.""" if not sctx.schema_data.if_features(stmt, sctx.text_mid): @@ -1137,6 +1138,7 @@ def _process_xmlarray_child( res.append(child) else: child = None + return child def entry_from_raw(self, rval: RawEntry,