Skip to content
Snippets Groups Projects
Commit a89ce6e8 authored by Ladislav Lhotka's avatar Ladislav Lhotka
Browse files

Split validation into syntax and semantics.

parent ce7fac7e
No related branches found
No related tags found
No related merge requests found
......@@ -449,10 +449,24 @@ __ http://www.sphinx-doc.org/en/stable/ext/doctest.html
>>> e2inst.value['example-2:bag']['foo'][1]['in-words'] # changed!
'tres'
.. method:: validate(content: ContentType = ContentType.config) -> None
.. method:: validate(scope: ValidationScope = ValidationScope.all, \
content: ContentType = ContentType.config) -> None
Perform schema validation on the receiver's value. The value of
the *content* argument belongs to the
Perform validation on the receiver's value. The *scope* argument
determines the validation scope. The options are as follows:
* ``ValidationScope.syntax`` – verifies schema constraints
(taking into account **if-feature** and **when** statements,
if present) and data types.
* ``ValidationScope.semantics`` – verifies **must** constraints,
uniqueness of list keys, **unique** constraints in list nodes,
and integrity of **leafref** references.
* ``ValidationScope.all`` – performs all checks from both items
above.
The value of the *content* argument belongs to the
:data:`~.enumerations.ContentType` enumeration and specifies
whether the receiver's value is to be validated as configuration
(``Content.config``) or as both configuration and state data
......
......@@ -485,7 +485,7 @@ def test_edits(data_model, instance):
llb1.update("2001::2::1", raw=True)
def test_validation(instance):
assert instance.validate(ContentType.all) is None
assert instance.validate(ctype=ContentType.all) is None
inst2 = instance.put_member("testb:leafQ", "ABBA").top()
with pytest.raises(SchemaError):
inst2.validate(ContentType.all)
inst2.validate(ctype=ContentType.all)
......@@ -42,7 +42,7 @@ from typing import Any, Callable, List, Tuple, Union
from urllib.parse import unquote
from .exceptions import YangsonException
from .context import Context
from .enumerations import ContentType
from .enumerations import ContentType, ValidationScope
from .instvalue import *
from .parser import EndOfInput, Parser, UnexpectedInput
from .typealiases import *
......@@ -326,17 +326,19 @@ class InstanceNode:
if val is None: return None
return val
def validate(self, ctype: ContentType = ContentType.config) -> None:
"""Perform schema validation of the receiver's value.
def validate(self, scope: ValidationScope = ValidationScope.all,
ctype: ContentType = ContentType.config) -> None:
"""Validate the receiver's value.
Args:
scope: Scope of the validation (syntax, semantics or all).
ctype: Receiver's content type.
Raises:
SchemaError: If the value doesn't conform to the schema.
SemanticError: If the value violates a semantic constraint.
"""
self.schema_node._validate(self, ctype)
self.schema_node._validate(self, scope, ctype)
def add_defaults(self, ctype: ContentType = None) -> "InstanceNode":
"""Return the receiver with defaults added recursively to its value.
......
......@@ -53,7 +53,7 @@ from .exceptions import YangsonException
from .context import Context
from .datatype import (DataType, LeafrefType, LinkType,
RawScalar, IdentityrefType)
from .enumerations import Axis, ContentType, DefaultDeny
from .enumerations import Axis, ContentType, DefaultDeny, ValidationScope
from .instvalue import ArrayValue, EntryValue, ObjectValue, Value
from .schpattern import *
from .statement import Statement, WrongArgument
......@@ -133,11 +133,13 @@ class SchemaNode:
"""
raise NotImplementedError
def _validate(self, inst: "InstanceNode", ctype: ContentType) -> None:
def _validate(self, inst: "InstanceNode", scope: ValidationScope,
ctype: ContentType) -> None:
"""Validate instance against the receiver.
Args:
inst: Instance node to be validated.
scope: Scope of the validation (syntax, semantics or all)
ctype: Content type of the instance.
Returns:
......@@ -363,11 +365,13 @@ class InternalNode(SchemaNode):
res[ch.iname()] = ch.from_raw(rval[qn])
return res
def _validate(self, inst: "InstanceNode", ctype: ContentType) -> None:
def _validate(self, inst: "InstanceNode", scope: ValidationScope,
ctype: ContentType) -> None:
"""Extend the superclass method."""
self._check_schema_pattern(inst, ctype)
for m in inst.value:
inst._member(m).validate(ctype)
if ctype.value & ValidationScope.syntax.value: # schema
self._check_schema_pattern(inst, ctype)
for m in inst.value: # all members
inst._member(m).validate(scope, ctype)
def _add_child(self, node: SchemaNode) -> None:
node.parent = self
......@@ -379,15 +383,6 @@ class InternalNode(SchemaNode):
def _check_schema_pattern(self, inst: "InstanceNode",
ctype: ContentType) -> None:
"""Match instance value against receiver's schema pattern.
Args:
inst: Instance node to be chancked.
ctype: Content type of the instance.
Raises:
SchemaError: if `inst` doesn't match the schema pattern.
"""
p = self.schema_pattern
p._eval_when(inst)
for m in inst.value:
......@@ -589,10 +584,12 @@ class DataNode(SchemaNode):
super().__init__()
self.default_deny = DefaultDeny.none # type: "DefaultDeny"
def _validate(self, inst: "InstanceNode", ctype: ContentType) -> None:
def _validate(self, inst: "InstanceNode", scope: ValidationScope,
ctype: ContentType) -> None:
"""Extend the superclass method."""
self._check_must(inst)
super()._validate(inst, ctype)
if scope.value & ValidationScope.semantics.value:
self._check_must(inst) # must expressions
super()._validate(inst, scope, ctype)
def _default_instance(self, pnode: "InstanceNode", ctype: ContentType,
lazy: bool = False) -> "InstanceNode":
......@@ -606,14 +603,6 @@ class DataNode(SchemaNode):
return pnode
def _check_must(self, inst: "InstanceNode") -> None:
"""Check that all receiver's "must" constraints for the instance.
Args:
inst: Instance node to be checked.
Raises:
SemanticError: If a "must" expression evaluates to ``False``.
"""
for mex in self.must:
if not mex[0].evaluate(inst):
msg = "'must' expression is false" if mex[1] is None else mex[1]
......@@ -654,31 +643,22 @@ class TerminalNode(SchemaNode):
"""Override the superclass method."""
return self.type.from_raw(rval)
def _validate(self, inst: "InstanceNode", ctype: ContentType) -> None:
def _validate(self, inst: "InstanceNode", scope: ValidationScope,
ctype: ContentType) -> None:
"""Extend the superclass method."""
self._check_type(inst)
if (scope.value & ValidationScope.syntax.value and
not self.type.contains(inst.value)): # data type
raise SchemaError(inst, "invalid type: " + repr(inst.value))
if (isinstance(self.type, LinkType) and # referential integrity
scope.value & ValidationScope.semantics.value and
self.type.require_instance and not inst._deref()):
raise SemanticError(inst, "required instance missing")
def _default_value(self, inst: "InstanceNode", ctype: ContentType,
lazy: bool) -> "InstanceNode":
inst.value = self.default
return inst
def _check_type(self, inst: "InstanceNode"):
"""Check whether receiver's type matches the instance value.
Args:
inst: Instance node to be checked.
Raises:
SchemaError: If the instance value doesn't match the type.
SemanticError: If the instance violates referential integrity.
"""
if not self.type.contains(inst.value):
raise SchemaError(inst, "invalid type: " + repr(inst.value))
if (isinstance(self.type, LinkType) and self.type.require_instance and
not inst._deref()):
raise SemanticError(inst, "required instance missing")
def _post_process(self) -> None:
super()._post_process()
if isinstance(self.type, LeafrefType):
......@@ -759,25 +739,19 @@ class SequenceNode(DataNode):
"""Is the receiver a mandatory node?"""
return self.min_elements > 0
def _validate(self, inst: "InstanceNode", ctype: ContentType) -> None:
def _validate(self, inst: "InstanceNode", scope: ValidationScope,
ctype: ContentType) -> None:
"""Extend the superclass method."""
if isinstance(inst, ArrayEntry):
super()._validate(inst, ctype)
super()._validate(inst, scope, ctype)
else:
self._check_list_props(inst)
self._check_cardinality(inst)
if scope.value & ValidationScope.semantics.value:
self._check_list_props(inst)
self._check_cardinality(inst)
for e in inst:
super()._validate(e, ctype)
super()._validate(e, scope, ctype)
def _check_cardinality(self, inst: "InstanceNode") -> None:
"""Check that the instance satisfies cardinality constraints.
Args:
inst: Instance node to be checked.
Raises:
SchemaError: It the cardinality of `inst` isn't correct.
"""
if len(inst.value) < self.min_elements:
raise SemanticError(inst,
"number of entries < min-elements ({})".format(
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment