From 72c80a961813adff51b1b86806b5e3b0a4ca757a Mon Sep 17 00:00:00 2001 From: M S Madhavan Iyer Date: Thu, 1 Oct 2026 16:49:50 +0530 Subject: [PATCH 1/3] fix(data-impl): validate unique in lists without schema paths Derive unique leaf paths from schema ancestors when a list has no absolute schema path. Add payload-list regression tests for duplicate and distinct nested unique values. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../impl/model/YangAbstractDataContainer.java | 20 ++++++++++- ...AnydataValidationOptionsJsonCodecTest.java | 33 +++++++++++++++++++ .../payload/yang/payload-anydata.yang | 14 ++++++++ 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/yangkit-data-impl/src/main/java/org/yangcentral/yangkit/data/impl/model/YangAbstractDataContainer.java b/yangkit-data-impl/src/main/java/org/yangcentral/yangkit/data/impl/model/YangAbstractDataContainer.java index c0c8b753..d8ca1386 100644 --- a/yangkit-data-impl/src/main/java/org/yangcentral/yangkit/data/impl/model/YangAbstractDataContainer.java +++ b/yangkit-data-impl/src/main/java/org/yangcentral/yangkit/data/impl/model/YangAbstractDataContainer.java @@ -434,7 +434,10 @@ public YangData removeDataChild(DataIdentifier identifier) { private boolean matchUnique(Unique unique,List> uniqueData,ListData listData){ List> matchedUniqueData = new ArrayList<>(); for(Leaf leaf:unique.getUniqueNodes()){ - List steps = listData.getSchemaNode().getSchemaPath().getRelativeSchemaPath(leaf.getSchemaPath()); + SchemaPath.Absolute listSchemaPath = listData.getSchemaNode().getSchemaPath(); + List steps = listSchemaPath == null + ? relativeSchemaSteps(listData.getSchemaNode(), leaf) + : listSchemaPath.getRelativeSchemaPath(leaf.getSchemaPath()); SchemaPath.Descendant descendant = new DescendantSchemaPath(steps,listData.getSchemaNode()); List> matched = YangDataUtil.search(listData,descendant); if(matched.isEmpty()){ @@ -455,6 +458,21 @@ private boolean matchUnique(Unique unique,List> uniqueData,ListData return true; } + private List relativeSchemaSteps(YangList list, Leaf leaf) { + LinkedList steps = new LinkedList<>(); + SchemaNode current = leaf; + while (current != null && current != list) { + steps.addFirst(current.getIdentifier()); + SchemaNodeContainer parent = current.getParentSchemaNode(); + current = parent instanceof SchemaNode ? (SchemaNode) parent : null; + } + if (current == null) { + throw new IllegalArgumentException("Unique leaf " + leaf.getIdentifier() + + " is not a descendant of list " + list.getIdentifier()); + } + return steps; + } + private ValidatorResult checkUniques(YangList list, List> matchedData){ ValidatorResultBuilder validatorResultBuilder = new ValidatorResultBuilder(); if(list.getUniques().isEmpty() || matchedData.isEmpty()){ diff --git a/yangkit-data-json-codec/src/test/java/org/yangcentral/yangkit/data/codec/json/test/AnydataValidationOptionsJsonCodecTest.java b/yangkit-data-json-codec/src/test/java/org/yangcentral/yangkit/data/codec/json/test/AnydataValidationOptionsJsonCodecTest.java index dcf8365a..92718c84 100644 --- a/yangkit-data-json-codec/src/test/java/org/yangcentral/yangkit/data/codec/json/test/AnydataValidationOptionsJsonCodecTest.java +++ b/yangkit-data-json-codec/src/test/java/org/yangcentral/yangkit/data/codec/json/test/AnydataValidationOptionsJsonCodecTest.java @@ -5,6 +5,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.yangcentral.yangkit.common.api.QName; +import org.yangcentral.yangkit.common.api.exception.ErrorAppTag; import org.yangcentral.yangkit.common.api.exception.ErrorTag; import org.yangcentral.yangkit.common.api.validate.ValidatorRecord; import org.yangcentral.yangkit.common.api.validate.ValidatorResult; @@ -249,4 +250,36 @@ public void deserializeNullAnydataReportsBadElement() throws Exception { assertNotNull(document); assertEquals(ErrorTag.BAD_ELEMENT, validator.build().getRecords().get(0).getErrorTag()); } + + @Test + public void duplicateUniqueValueInPayloadListReportsValidationError() throws Exception { + ValidatorResult result = validatePayloadWithItemNames("shared", "shared"); + + assertFalse(result.isOk()); + assertTrue(result.getRecords().stream().anyMatch(record -> + ErrorAppTag.DATA_NOT_UNIQUE.getName().equals(record.getErrorAppTag()))); + } + + @Test + public void distinctUniqueValuesInPayloadListValidate() throws Exception { + assertTrue(validatePayloadWithItemNames("first", "second").isOk()); + } + + private ValidatorResult validatePayloadWithItemNames(String firstName, String secondName) throws Exception { + String json = "{\"outer-anydata:anydata-wrapper\":{\"payload-holder\":{" + + "\"payload-anydata:payload-root\":{\"item\":[" + + "{\"id\":\"one\",\"details\":{\"name\":\"" + firstName + "\"}}," + + "{\"id\":\"two\",\"details\":{\"name\":\"" + secondName + "\"}}]}}}}"; + ValidatorResultBuilder parseResult = new ValidatorResultBuilder(); + YangDataDocument document = new YangDataDocumentJsonCodec(outerSchemaContext).deserialize( + new ObjectMapper().readTree(json), parseResult, + new AnydataValidationOptions().registerSchemaContext( + PAYLOAD_HOLDER_QNAME, payloadSchemaContext)); + assertTrue(parseResult.build().isOk()); + AnyDataData anydata = extractAnydata(document); + assertNotNull(anydata.getValue()); + assertEquals(2, anydata.getValue().getDataChildren("item").size()); + assertNull(anydata.getValue().getDataChildren("item").get(0).getSchemaNode().getSchemaPath()); + return anydata.getValue().validate(); + } } diff --git a/yangkit-data-json-codec/src/test/resources/anydata-validation/payload/yang/payload-anydata.yang b/yangkit-data-json-codec/src/test/resources/anydata-validation/payload/yang/payload-anydata.yang index 219b82cb..72b73fbf 100644 --- a/yangkit-data-json-codec/src/test/resources/anydata-validation/payload/yang/payload-anydata.yang +++ b/yangkit-data-json-codec/src/test/resources/anydata-validation/payload/yang/payload-anydata.yang @@ -34,5 +34,19 @@ module payload-anydata { path "../target"; } } + + list item { + key "id"; + unique "details/name"; + + leaf id { + type string; + } + container details { + leaf name { + type string; + } + } + } } } From 9665d19082bec4f8e6634f85aaccc02a4161fccd Mon Sep 17 00:00:00 2001 From: M S Madhavan Iyer Date: Fri, 9 Oct 2026 12:56:30 +0530 Subject: [PATCH 2/3] fix(model): restore schema path build lifecycle Delegate schema build phases to SchemaNodeSupport after the composition refactor. Restore structure and generated operation child paths, remove the unique ancestor fallback, and record unresolved unique paths as validation errors. Cover lifecycle paths and unique validation regressions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../impl/model/YangAbstractDataContainer.java | 40 ++--- ...AnydataValidationOptionsJsonCodecTest.java | 146 +++++++++++++++++- .../json/test/SchemaPathLifecycleTest.java | 80 ++++++++++ .../schema-path/yang/schema-path.yang | 33 ++++ .../structure/yang/schema-path-augment.yang | 12 ++ .../model/impl/stmt/DataDefinitionImpl.java | 9 +- .../yangkit/model/impl/stmt/InputImpl.java | 2 +- .../model/impl/stmt/NotificationImpl.java | 2 +- .../model/impl/stmt/OperationImpl.java | 10 +- .../yangkit/model/impl/stmt/OutputImpl.java | 2 +- .../model/impl/stmt/SchemaNodeSupport.java | 8 +- .../impl/stmt/ext/AugmentStructureImpl.java | 2 +- .../impl/stmt/ext/YangDataStructureImpl.java | 4 +- 13 files changed, 314 insertions(+), 36 deletions(-) create mode 100644 yangkit-data-json-codec/src/test/java/org/yangcentral/yangkit/data/codec/json/test/SchemaPathLifecycleTest.java create mode 100644 yangkit-data-json-codec/src/test/resources/schema-path/yang/schema-path.yang create mode 100644 yangkit-data-json-codec/src/test/resources/structure/yang/schema-path-augment.yang diff --git a/yangkit-data-impl/src/main/java/org/yangcentral/yangkit/data/impl/model/YangAbstractDataContainer.java b/yangkit-data-impl/src/main/java/org/yangcentral/yangkit/data/impl/model/YangAbstractDataContainer.java index d8ca1386..014244ab 100644 --- a/yangkit-data-impl/src/main/java/org/yangcentral/yangkit/data/impl/model/YangAbstractDataContainer.java +++ b/yangkit-data-impl/src/main/java/org/yangcentral/yangkit/data/impl/model/YangAbstractDataContainer.java @@ -431,14 +431,29 @@ public YangData removeDataChild(DataIdentifier identifier) { return dataChild; } - private boolean matchUnique(Unique unique,List> uniqueData,ListData listData){ + private boolean matchUnique(Unique unique,List> uniqueData,ListData listData, + ValidatorResultBuilder validatorResultBuilder){ List> matchedUniqueData = new ArrayList<>(); + List descendants = new ArrayList<>(); for(Leaf leaf:unique.getUniqueNodes()){ SchemaPath.Absolute listSchemaPath = listData.getSchemaNode().getSchemaPath(); - List steps = listSchemaPath == null - ? relativeSchemaSteps(listData.getSchemaNode(), leaf) + List steps = listSchemaPath == null ? null : listSchemaPath.getRelativeSchemaPath(leaf.getSchemaPath()); - SchemaPath.Descendant descendant = new DescendantSchemaPath(steps,listData.getSchemaNode()); + if (steps == null || steps.isEmpty()) { + ValidatorRecordBuilder> recordBuilder = new ValidatorRecordBuilder<>(); + recordBuilder.setErrorTag(ErrorTag.OPERATION_FAILED); + recordBuilder.setErrorPath(listData.getPath()); + recordBuilder.setBadElement(listData); + recordBuilder.setErrorMessage(new ErrorMessage("Cannot resolve unique constraint '" + + unique.getArgStr() + "' leaf " + leaf.getIdentifier().getQualifiedName() + + " beneath list " + listData.getSchemaNode().getIdentifier().getQualifiedName() + + ": missing schema path or invalid ancestry.")); + validatorResultBuilder.addRecord(recordBuilder.build()); + return false; + } + descendants.add(new DescendantSchemaPath(steps,listData.getSchemaNode())); + } + for (SchemaPath.Descendant descendant : descendants) { List> matched = YangDataUtil.search(listData,descendant); if(matched.isEmpty()){ return false; @@ -458,21 +473,6 @@ private boolean matchUnique(Unique unique,List> uniqueData,ListData return true; } - private List relativeSchemaSteps(YangList list, Leaf leaf) { - LinkedList steps = new LinkedList<>(); - SchemaNode current = leaf; - while (current != null && current != list) { - steps.addFirst(current.getIdentifier()); - SchemaNodeContainer parent = current.getParentSchemaNode(); - current = parent instanceof SchemaNode ? (SchemaNode) parent : null; - } - if (current == null) { - throw new IllegalArgumentException("Unique leaf " + leaf.getIdentifier() - + " is not a descendant of list " + list.getIdentifier()); - } - return steps; - } - private ValidatorResult checkUniques(YangList list, List> matchedData){ ValidatorResultBuilder validatorResultBuilder = new ValidatorResultBuilder(); if(list.getUniques().isEmpty() || matchedData.isEmpty()){ @@ -485,7 +485,7 @@ private ValidatorResult checkUniques(YangList list, List> matchedDat YangData previous = null; for( YangData dataItem:matchedData){ if(dataItem instanceof ListData){ - if(matchUnique(unique,uniqueData, (ListData) dataItem)){ + if(matchUnique(unique,uniqueData, (ListData) dataItem, validatorResultBuilder)){ matchCount++; } if(matchCount == 1){ diff --git a/yangkit-data-json-codec/src/test/java/org/yangcentral/yangkit/data/codec/json/test/AnydataValidationOptionsJsonCodecTest.java b/yangkit-data-json-codec/src/test/java/org/yangcentral/yangkit/data/codec/json/test/AnydataValidationOptionsJsonCodecTest.java index 92718c84..89b8765e 100644 --- a/yangkit-data-json-codec/src/test/java/org/yangcentral/yangkit/data/codec/json/test/AnydataValidationOptionsJsonCodecTest.java +++ b/yangkit-data-json-codec/src/test/java/org/yangcentral/yangkit/data/codec/json/test/AnydataValidationOptionsJsonCodecTest.java @@ -20,15 +20,24 @@ import org.yangcentral.yangkit.data.api.model.YangDataDocument; import org.yangcentral.yangkit.data.codec.json.YangDataDocumentJsonCodec; import org.yangcentral.yangkit.model.api.schema.YangSchemaContext; +import org.yangcentral.yangkit.model.api.stmt.Leaf; +import org.yangcentral.yangkit.model.api.stmt.SchemaNodeContainer; +import org.yangcentral.yangkit.model.api.stmt.Unique; +import org.yangcentral.yangkit.model.api.stmt.YangList; import org.yangcentral.yangkit.parser.YangParserException; import org.yangcentral.yangkit.parser.YangYinParser; import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Proxy; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; public class AnydataValidationOptionsJsonCodecTest { @@ -265,6 +274,141 @@ public void distinctUniqueValuesInPayloadListValidate() throws Exception { assertTrue(validatePayloadWithItemNames("first", "second").isOk()); } + @Test + public void payloadSchemaPathsAreBuiltBeforeDeserialization() { + YangList list = payloadList(); + Leaf leaf = list.getUniques().get(0).getUniqueNodes().get(0); + assertNotNull(list.getSchemaPath()); + assertNotNull(leaf.getSchemaPath()); + assertEquals("/payload:payload-root/payload:item", list.getSchemaPath().toString()); + assertEquals("/payload:payload-root/payload:item/payload:details/payload:name", + leaf.getSchemaPath().toString()); + } + + @Test + public void ordinaryListUniqueValidationUsesBuiltSchemaPaths() throws Exception { + ValidatorResultBuilder parseResult = new ValidatorResultBuilder(); + YangDataDocument document = new YangDataDocumentJsonCodec(payloadSchemaContext).deserialize( + new ObjectMapper().readTree("{\"payload-anydata:payload-root\":{\"item\":[" + + "{\"id\":\"one\",\"details\":{\"name\":\"shared\"}}," + + "{\"id\":\"two\",\"details\":{\"name\":\"shared\"}}]}}"), parseResult); + assertTrue(parseResult.build().isOk()); + ValidatorResult result = document.validate(); + assertFalse(result.isOk()); + assertTrue(result.getRecords().stream().anyMatch(record -> + ErrorAppTag.DATA_NOT_UNIQUE.getName().equals(record.getErrorAppTag()))); + } + + @Test + public void omittedUniqueLeafDoesNotCountAsDuplicate() throws Exception { + ValidatorResultBuilder parseResult = new ValidatorResultBuilder(); + YangDataDocument document = new YangDataDocumentJsonCodec(payloadSchemaContext).deserialize( + new ObjectMapper().readTree("{\"payload-anydata:payload-root\":{\"item\":[" + + "{\"id\":\"one\"},{\"id\":\"two\"}]}}"), parseResult); + assertTrue(parseResult.build().isOk()); + assertTrue(document.validate().isOk()); + } + + @Test + public void uniqueLeafOutsideListReportsValidationError() throws Exception { + SchemaNodeContainer root = (SchemaNodeContainer) payloadSchemaContext.getTreeNodeChild( + new QName("urn:test:payload-anydata", "payload-root")); + assertInvalidUniqueLeaf((Leaf) root.getTreeNodeChild( + new QName("urn:test:payload-anydata", "value"))); + } + + @Test + public void uniqueLeafWithoutSchemaPathReportsValidationError() throws Exception { + Leaf original = payloadList().getUniques().get(0).getUniqueNodes().get(0); + Leaf missingPath = (Leaf) Proxy.newProxyInstance(Leaf.class.getClassLoader(), + new Class[]{Leaf.class}, (proxy, method, args) -> { + if ("getSchemaPath".equals(method.getName())) { + return null; + } + try { + return method.invoke(original, args); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + }); + assertInvalidUniqueLeaf(missingPath); + } + + @Test + public void listWithoutSchemaPathReportsValidationError() throws Exception { + YangList list = payloadList(); + Field supportField = org.yangcentral.yangkit.model.impl.stmt.DataDefinitionImpl.class + .getDeclaredField("schemaNodeSupport"); + supportField.setAccessible(true); + Object support = supportField.get(list); + Field pathField = support.getClass().getDeclaredField("schemaPath"); + pathField.setAccessible(true); + Object originalPath = pathField.get(support); + try { + pathField.set(support, null); + assertInvalidUniqueLeaf(list.getUniques().get(0).getUniqueNodes().get(0)); + } finally { + pathField.set(support, originalPath); + } + } + + @Test + public void invalidUniqueConstraintDoesNotPreventLaterConstraintValidation() throws Exception { + YangList list = payloadList(); + Unique unique = list.getUniques().get(0); + Leaf original = unique.getUniqueNodes().get(0); + SchemaNodeContainer root = (SchemaNodeContainer) payloadSchemaContext.getTreeNodeChild( + new QName("urn:test:payload-anydata", "payload-root")); + Unique later = new org.yangcentral.yangkit.model.impl.stmt.UniqueImpl("details/name"); + later.addUniqueNode(original); + try { + unique.removeUniqueNodes(); + unique.addUniqueNode((Leaf) root.getTreeNodeChild( + new QName("urn:test:payload-anydata", "value"))); + list.getUniques().add(later); + ValidatorResult result = assertDoesNotThrow( + () -> validatePayloadWithItemNames("shared", "shared")); + assertFalse(result.isOk()); + assertTrue(result.getRecords().stream().anyMatch(record -> + ErrorTag.OPERATION_FAILED.equals(record.getErrorTag()) + && record.getErrorMsg().getMessage().contains("Cannot resolve unique"))); + assertTrue(result.getRecords().stream().anyMatch(record -> + ErrorAppTag.DATA_NOT_UNIQUE.getName().equals(record.getErrorAppTag()))); + } finally { + list.getUniques().remove(later); + unique.removeUniqueNodes(); + unique.addUniqueNode(original); + } + } + + private void assertInvalidUniqueLeaf(Leaf leaf) throws Exception { + Unique unique = payloadList().getUniques().get(0); + Leaf original = unique.getUniqueNodes().get(0); + try { + unique.removeUniqueNodes(); + assertTrue(unique.addUniqueNode(leaf)); + ValidatorResult result = assertDoesNotThrow( + () -> validatePayloadWithItemNames("shared", "shared")); + assertFalse(result.isOk()); + assertTrue(result.getRecords().stream().anyMatch(record -> + ErrorTag.OPERATION_FAILED.equals(record.getErrorTag()) + && record.getErrorPath() != null + && record.getBadElement() != null + && record.getErrorMsg().getMessage().contains("details/name"))); + assertFalse(result.getRecords().stream().anyMatch(record -> + ErrorAppTag.DATA_NOT_UNIQUE.getName().equals(record.getErrorAppTag()))); + } finally { + unique.removeUniqueNodes(); + unique.addUniqueNode(original); + } + } + + private YangList payloadList() { + SchemaNodeContainer root = (SchemaNodeContainer) payloadSchemaContext.getTreeNodeChild( + new QName("urn:test:payload-anydata", "payload-root")); + return (YangList) root.getTreeNodeChild(new QName("urn:test:payload-anydata", "item")); + } + private ValidatorResult validatePayloadWithItemNames(String firstName, String secondName) throws Exception { String json = "{\"outer-anydata:anydata-wrapper\":{\"payload-holder\":{" + "\"payload-anydata:payload-root\":{\"item\":[" @@ -279,7 +423,7 @@ private ValidatorResult validatePayloadWithItemNames(String firstName, String se AnyDataData anydata = extractAnydata(document); assertNotNull(anydata.getValue()); assertEquals(2, anydata.getValue().getDataChildren("item").size()); - assertNull(anydata.getValue().getDataChildren("item").get(0).getSchemaNode().getSchemaPath()); + assertSame(payloadList(), anydata.getValue().getDataChildren("item").get(0).getSchemaNode()); return anydata.getValue().validate(); } } diff --git a/yangkit-data-json-codec/src/test/java/org/yangcentral/yangkit/data/codec/json/test/SchemaPathLifecycleTest.java b/yangkit-data-json-codec/src/test/java/org/yangcentral/yangkit/data/codec/json/test/SchemaPathLifecycleTest.java new file mode 100644 index 00000000..0a37d2a2 --- /dev/null +++ b/yangkit-data-json-codec/src/test/java/org/yangcentral/yangkit/data/codec/json/test/SchemaPathLifecycleTest.java @@ -0,0 +1,80 @@ +package org.yangcentral.yangkit.data.codec.json.test; + +import org.junit.jupiter.api.Test; +import org.yangcentral.yangkit.common.api.QName; +import org.yangcentral.yangkit.model.api.schema.YangSchemaContext; +import org.yangcentral.yangkit.model.api.stmt.Module; +import org.yangcentral.yangkit.model.api.stmt.Operation; +import org.yangcentral.yangkit.model.api.stmt.SchemaNode; +import org.yangcentral.yangkit.model.api.stmt.SchemaNodeContainer; +import org.yangcentral.yangkit.model.api.stmt.VirtualSchemaNode; +import org.yangcentral.yangkit.model.api.stmt.YangUnknown; +import org.yangcentral.yangkit.model.api.stmt.ext.YangStructure; +import org.yangcentral.yangkit.parser.YangYinParser; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class SchemaPathLifecycleTest { + @Test + public void schemaTreeBuildsDataOperationAndNotificationPaths() throws Exception { + YangSchemaContext context = YangYinParser.parse(getClass().getClassLoader() + .getResource("schema-path/yang").getFile()); + assertTrue(context.validate().isOk()); + for (SchemaNode node : context.getSchemaNodeChildren()) { + assertPaths(node, new ArrayList<>()); + } + Module module = context.getModules().get(0); + Operation empty = (Operation) module.getSchemaNodeChild(new QName("urn:test:schema-path", "empty")); + assertNotNull(empty); + SchemaNode input = empty.getSchemaNodeChild(new QName("urn:test:schema-path", "input")); + SchemaNode output = empty.getSchemaNodeChild(new QName("urn:test:schema-path", "output")); + assertNotNull(input); + assertNotNull(output); + assertEquals("/sp:empty/sp:input", input.getSchemaPath().toString()); + assertEquals("/sp:empty/sp:output", output.getSchemaPath().toString()); + } + + @Test + public void structureAndAugmentedChildrenHaveSchemaPaths() throws Exception { + YangSchemaContext context = YangYinParser.parse(getClass().getClassLoader() + .getResource("structure/yang").getFile()); + assertTrue(context.validate().isOk()); + int structures = 0; + SchemaNode augmentedLeaf = null; + for (Module module : context.getModules()) { + for (YangUnknown unknown : module.getUnknowns()) { + if (unknown instanceof YangStructure) { + structures++; + assertPaths((YangStructure) unknown, new ArrayList<>()); + SchemaNodeContainer payload = (SchemaNodeContainer) ((YangStructure) unknown) + .getTreeNodeChild(new QName("urn:example:test-structure", "payload")); + if (payload != null) { + augmentedLeaf = payload.getTreeNodeChild(new QName("urn:test:schema-path-augment", "extra")); + } + } + } + } + assertTrue(structures > 0); + assertNotNull(augmentedLeaf); + assertEquals("/ts:message/ts:payload/spa:extra", augmentedLeaf.getSchemaPath().toString()); + } + + private void assertPaths(SchemaNode node, List ancestors) { + List expected = new ArrayList<>(ancestors); + if (node instanceof VirtualSchemaNode) { + assertNull(node.getSchemaPath(), node.getArgStr()); + } else { + expected.add(node.getIdentifier()); + assertNotNull(node.getSchemaPath(), node.getArgStr()); + assertEquals(expected, node.getSchemaPath().getPath(), node.getArgStr()); + } + if (node instanceof SchemaNodeContainer) { + for (SchemaNode child : ((SchemaNodeContainer) node).getSchemaNodeChildren()) { + assertPaths(child, expected); + } + } + } +} diff --git a/yangkit-data-json-codec/src/test/resources/schema-path/yang/schema-path.yang b/yangkit-data-json-codec/src/test/resources/schema-path/yang/schema-path.yang new file mode 100644 index 00000000..5707b516 --- /dev/null +++ b/yangkit-data-json-codec/src/test/resources/schema-path/yang/schema-path.yang @@ -0,0 +1,33 @@ +module schema-path { + yang-version 1.1; + namespace "urn:test:schema-path"; + prefix sp; + + container root { + list item { + key "id"; + leaf id { type string; } + choice selection { + case named { + leaf name { type string; } + } + } + } + action inspect { + input { leaf request { type string; } } + output { leaf response { type string; } } + } + action reset; + notification changed { + leaf value { type string; } + } + } + rpc run { + input { leaf request { type string; } } + output { leaf response { type string; } } + } + rpc empty; + notification event { + leaf value { type string; } + } +} diff --git a/yangkit-data-json-codec/src/test/resources/structure/yang/schema-path-augment.yang b/yangkit-data-json-codec/src/test/resources/structure/yang/schema-path-augment.yang new file mode 100644 index 00000000..617a8928 --- /dev/null +++ b/yangkit-data-json-codec/src/test/resources/structure/yang/schema-path-augment.yang @@ -0,0 +1,12 @@ +module schema-path-augment { + yang-version 1.1; + namespace "urn:test:schema-path-augment"; + prefix spa; + + import ietf-yang-structure-ext { prefix sx; } + import test-structure { prefix ts; } + + sx:augment-structure "/ts:message/ts:payload" { + leaf extra { type string; } + } +} diff --git a/yangkit-model-impl/src/main/java/org/yangcentral/yangkit/model/impl/stmt/DataDefinitionImpl.java b/yangkit-model-impl/src/main/java/org/yangcentral/yangkit/model/impl/stmt/DataDefinitionImpl.java index 0531a0a5..4d75d9cf 100644 --- a/yangkit-model-impl/src/main/java/org/yangcentral/yangkit/model/impl/stmt/DataDefinitionImpl.java +++ b/yangkit-model-impl/src/main/java/org/yangcentral/yangkit/model/impl/stmt/DataDefinitionImpl.java @@ -1,5 +1,6 @@ package org.yangcentral.yangkit.model.impl.stmt; +import org.yangcentral.yangkit.base.BuildPhase; import org.yangcentral.yangkit.base.ErrorCode; import org.yangcentral.yangkit.base.YangBuiltinKeyword; import org.yangcentral.yangkit.base.YangContext; @@ -30,6 +31,13 @@ public DataDefinitionImpl(String argStr) { super(argStr); } + @Override + protected ValidatorResult buildSelf(BuildPhase phase) { + ValidatorResultBuilder validatorResultBuilder = new ValidatorResultBuilder(super.buildSelf(phase)); + validatorResultBuilder.merge(schemaNodeSupport.buildSelf(phase)); + return validatorResultBuilder.build(); + } + public void setContext(YangContext context) { super.setContext(context); this.ifFeatureSupport.setYangContext(context); @@ -189,4 +197,3 @@ public List getEffectiveSubStatements() { @Override public org.yangcentral.yangkit.model.api.stmt.SchemaNodeContainer getSchemaTreeRoot() { return schemaNodeSupport.getSchemaTreeRoot(); } } - diff --git a/yangkit-model-impl/src/main/java/org/yangcentral/yangkit/model/impl/stmt/InputImpl.java b/yangkit-model-impl/src/main/java/org/yangcentral/yangkit/model/impl/stmt/InputImpl.java index 908bc52e..0488dd69 100644 --- a/yangkit-model-impl/src/main/java/org/yangcentral/yangkit/model/impl/stmt/InputImpl.java +++ b/yangkit-model-impl/src/main/java/org/yangcentral/yangkit/model/impl/stmt/InputImpl.java @@ -277,6 +277,7 @@ protected ValidatorResult validateSelf() { protected ValidatorResult buildSelf(BuildPhase phase) { ValidatorResultBuilder validatorResultBuilder = new ValidatorResultBuilder(super.buildSelf(phase)); + validatorResultBuilder.merge(schemaNodeSupport.buildSelf(phase)); switch (phase) { case SCHEMA_BUILD: @@ -345,4 +346,3 @@ public List getEffectiveSubStatements() { @Override public org.yangcentral.yangkit.model.api.stmt.SchemaNodeContainer getSchemaTreeRoot() { return schemaNodeSupport.getSchemaTreeRoot(); } } - diff --git a/yangkit-model-impl/src/main/java/org/yangcentral/yangkit/model/impl/stmt/NotificationImpl.java b/yangkit-model-impl/src/main/java/org/yangcentral/yangkit/model/impl/stmt/NotificationImpl.java index d7b6c156..c9e23695 100644 --- a/yangkit-model-impl/src/main/java/org/yangcentral/yangkit/model/impl/stmt/NotificationImpl.java +++ b/yangkit-model-impl/src/main/java/org/yangcentral/yangkit/model/impl/stmt/NotificationImpl.java @@ -301,6 +301,7 @@ protected ValidatorResult validateSelf() { protected ValidatorResult buildSelf(BuildPhase phase) { ValidatorResultBuilder validatorResultBuilder = new ValidatorResultBuilder(super.buildSelf(phase)); + validatorResultBuilder.merge(schemaNodeSupport.buildSelf(phase)); switch (phase) { case SCHEMA_BUILD: for (DataDefinition dataDefinition : this.getDataDefChildren()) { @@ -369,4 +370,3 @@ public List getEffectiveSubStatements() { @Override public org.yangcentral.yangkit.model.api.stmt.SchemaNodeContainer getSchemaTreeRoot() { return schemaNodeSupport.getSchemaTreeRoot(); } } - diff --git a/yangkit-model-impl/src/main/java/org/yangcentral/yangkit/model/impl/stmt/OperationImpl.java b/yangkit-model-impl/src/main/java/org/yangcentral/yangkit/model/impl/stmt/OperationImpl.java index 6a39d6d6..373b383e 100644 --- a/yangkit-model-impl/src/main/java/org/yangcentral/yangkit/model/impl/stmt/OperationImpl.java +++ b/yangkit-model-impl/src/main/java/org/yangcentral/yangkit/model/impl/stmt/OperationImpl.java @@ -244,6 +244,7 @@ protected ValidatorResult initSelf() { protected ValidatorResult buildSelf(BuildPhase phase) { ValidatorResultBuilder validatorResultBuilder = new ValidatorResultBuilder(super.buildSelf(phase)); + validatorResultBuilder.merge(schemaNodeSupport.buildSelf(phase)); switch (phase) { case SCHEMA_BUILD: //this.setSchemaTreeType(SchemaTreeType.RPCTREE); @@ -255,8 +256,8 @@ protected ValidatorResult buildSelf(BuildPhase phase) { input.setElementPosition(this.getElementPosition()); input.setParentStatement(this); input.init(); - input.build(); - this.schemaNodeContainer.addSchemaNodeChild(input); + validatorResultBuilder.merge(this.schemaNodeContainer.addSchemaNodeChild(input)); + validatorResultBuilder.merge(input.build()); } if (this.output != null) { @@ -267,8 +268,8 @@ protected ValidatorResult buildSelf(BuildPhase phase) { output.setElementPosition(this.getElementPosition()); output.setParentStatement(this); output.init(); - output.build(); - this.schemaNodeContainer.addSchemaNodeChild(output); + validatorResultBuilder.merge(this.schemaNodeContainer.addSchemaNodeChild(output)); + validatorResultBuilder.merge(output.build()); } default: return validatorResultBuilder.build(); @@ -335,4 +336,3 @@ public List getEffectiveSubStatements() { @Override public org.yangcentral.yangkit.model.api.stmt.SchemaNodeContainer getSchemaTreeRoot() { return schemaNodeSupport.getSchemaTreeRoot(); } } - diff --git a/yangkit-model-impl/src/main/java/org/yangcentral/yangkit/model/impl/stmt/OutputImpl.java b/yangkit-model-impl/src/main/java/org/yangcentral/yangkit/model/impl/stmt/OutputImpl.java index 2894d261..cbcb18aa 100644 --- a/yangkit-model-impl/src/main/java/org/yangcentral/yangkit/model/impl/stmt/OutputImpl.java +++ b/yangkit-model-impl/src/main/java/org/yangcentral/yangkit/model/impl/stmt/OutputImpl.java @@ -251,6 +251,7 @@ protected ValidatorResult validateSelf() { protected ValidatorResult buildSelf(BuildPhase phase) { ValidatorResultBuilder validatorResultBuilder = new ValidatorResultBuilder(super.buildSelf(phase)); + validatorResultBuilder.merge(schemaNodeSupport.buildSelf(phase)); if (Objects.requireNonNull(phase) == BuildPhase.SCHEMA_BUILD) { for (DataDefinition dataDefinition : this.getDataDefChildren()) { validatorResultBuilder.merge(this.addSchemaNodeChild(dataDefinition)); @@ -336,4 +337,3 @@ public List getEffectiveSubStatements() { @Override public org.yangcentral.yangkit.model.api.stmt.SchemaNodeContainer getSchemaTreeRoot() { return schemaNodeSupport.getSchemaTreeRoot(); } } - diff --git a/yangkit-model-impl/src/main/java/org/yangcentral/yangkit/model/impl/stmt/SchemaNodeSupport.java b/yangkit-model-impl/src/main/java/org/yangcentral/yangkit/model/impl/stmt/SchemaNodeSupport.java index 6262435b..592469c4 100644 --- a/yangkit-model-impl/src/main/java/org/yangcentral/yangkit/model/impl/stmt/SchemaNodeSupport.java +++ b/yangkit-model-impl/src/main/java/org/yangcentral/yangkit/model/impl/stmt/SchemaNodeSupport.java @@ -27,6 +27,8 @@ * SchemaNode (a role/contract) no longer sits in the ontology inheritance * chain. Classes that implement SchemaNode compose this class instead of * extending SchemaNodeImpl. + * Owners must delegate buildSelf as well as SchemaNode methods so that + * SCHEMA_TREE assigns their absolute paths. */ public class SchemaNodeSupport { private final SchemaNode owner; @@ -192,14 +194,14 @@ private SchemaPath.Absolute getSchemaPath(SchemaNode schemaNode) { SchemaNodeContainer schemaNodeParent = schemaNode.getParentSchemaNode(); if (!(schemaNodeParent instanceof SchemaNode)) { SchemaPath.Absolute path = new AbsoluteSchemaPath(); - path.addStep(owner.getIdentifier()); + path.addStep(schemaNode.getIdentifier()); return path; } SchemaNode parentSchemaNode = getRealSchemaNode((SchemaNode) schemaNodeParent); if (null == parentSchemaNode) { SchemaPath.Absolute path = new AbsoluteSchemaPath(); - path.addStep(owner.getIdentifier()); + path.addStep(schemaNode.getIdentifier()); return path; } SchemaPath parentSchemaPath = parentSchemaNode.getSchemaPath(); @@ -207,7 +209,7 @@ private SchemaPath.Absolute getSchemaPath(SchemaNode schemaNode) { parentSchemaPath = getSchemaPath(parentSchemaNode); } SchemaPath.Absolute path = new AbsoluteSchemaPath(parentSchemaPath.getPath()); - path.addStep(owner.getIdentifier()); + path.addStep(schemaNode.getIdentifier()); return path; } diff --git a/yangkit-model-impl/src/main/java/org/yangcentral/yangkit/model/impl/stmt/ext/AugmentStructureImpl.java b/yangkit-model-impl/src/main/java/org/yangcentral/yangkit/model/impl/stmt/ext/AugmentStructureImpl.java index 4fcaad05..4a9acea1 100644 --- a/yangkit-model-impl/src/main/java/org/yangcentral/yangkit/model/impl/stmt/ext/AugmentStructureImpl.java +++ b/yangkit-model-impl/src/main/java/org/yangcentral/yangkit/model/impl/stmt/ext/AugmentStructureImpl.java @@ -239,6 +239,7 @@ protected ValidatorResult initSelf() { protected ValidatorResult buildSelf(BuildPhase phase) { ValidatorResultBuilder validatorResultBuilder = new ValidatorResultBuilder(super.buildSelf(phase)); + validatorResultBuilder.merge(schemaNodeSupport.buildSelf(phase)); Iterator iterator; SchemaNode child; switch (phase) { @@ -421,4 +422,3 @@ public List getEffectiveSchemaNodeChildren(boolean ignoreNamespace) @Override public org.yangcentral.yangkit.model.api.stmt.SchemaNodeContainer getSchemaTreeRoot() { return schemaNodeSupport.getSchemaTreeRoot(); } } - diff --git a/yangkit-model-impl/src/main/java/org/yangcentral/yangkit/model/impl/stmt/ext/YangDataStructureImpl.java b/yangkit-model-impl/src/main/java/org/yangcentral/yangkit/model/impl/stmt/ext/YangDataStructureImpl.java index 2651c3ea..6f5775df 100644 --- a/yangkit-model-impl/src/main/java/org/yangcentral/yangkit/model/impl/stmt/ext/YangDataStructureImpl.java +++ b/yangkit-model-impl/src/main/java/org/yangcentral/yangkit/model/impl/stmt/ext/YangDataStructureImpl.java @@ -26,7 +26,7 @@ public class YangDataStructureImpl extends EntityImpl implements YangStructure { public static void register(){ YangUnknownParserPolicy unknownParserPolicy = new YangUnknownParserPolicy(YANG_KEYWORD, YangDataStructureImpl.class, - Arrays.asList(BuildPhase.GRAMMAR,BuildPhase.SCHEMA_BUILD)); + Arrays.asList(BuildPhase.GRAMMAR,BuildPhase.SCHEMA_BUILD,BuildPhase.SCHEMA_TREE)); YangStatementDef yangStatementDef = new YangStatementDef(YANG_KEYWORD,"name",true); yangStatementDef.addSubStatementInfo(new YangSubStatementInfo(YangBuiltinKeyword.MUST.getQName(),new Cardinality())); yangStatementDef.addSubStatementInfo(new YangSubStatementInfo(YangBuiltinKeyword.STATUS.getQName(),new Cardinality(0,1))); @@ -314,6 +314,7 @@ protected ValidatorResult initSelf() { protected ValidatorResult buildSelf(BuildPhase phase) { ValidatorResultBuilder validatorResultBuilder = new ValidatorResultBuilder(); validatorResultBuilder.merge(super.buildSelf(phase)); + validatorResultBuilder.merge(schemaNodeSupport.buildSelf(phase)); switch (phase) { case SCHEMA_BUILD: @@ -381,4 +382,3 @@ public QName getIdentifier() { @Override public org.yangcentral.yangkit.model.api.stmt.SchemaNodeContainer getSchemaTreeRoot() { return schemaNodeSupport.getSchemaTreeRoot(); } } - From 527ac902cde122d6d98d45069bd99f5b5099533c Mon Sep 17 00:00:00 2001 From: M S Madhavan Iyer Date: Fri, 9 Oct 2026 12:58:22 +0530 Subject: [PATCH 3/3] test: reconcile unique fixture with upstream anydata constraints Combine the unique details leaf with upstream's existing item list, preserving mandatory and cardinality constraints. Supply valid required fields in unique regression inputs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...AnydataValidationOptionsJsonCodecTest.java | 16 ++++++++-------- .../payload/yang/payload-anydata.yang | 19 ++++++------------- 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/yangkit-data-json-codec/src/test/java/org/yangcentral/yangkit/data/codec/json/test/AnydataValidationOptionsJsonCodecTest.java b/yangkit-data-json-codec/src/test/java/org/yangcentral/yangkit/data/codec/json/test/AnydataValidationOptionsJsonCodecTest.java index 89b8765e..26f60fd1 100644 --- a/yangkit-data-json-codec/src/test/java/org/yangcentral/yangkit/data/codec/json/test/AnydataValidationOptionsJsonCodecTest.java +++ b/yangkit-data-json-codec/src/test/java/org/yangcentral/yangkit/data/codec/json/test/AnydataValidationOptionsJsonCodecTest.java @@ -289,9 +289,9 @@ public void payloadSchemaPathsAreBuiltBeforeDeserialization() { public void ordinaryListUniqueValidationUsesBuiltSchemaPaths() throws Exception { ValidatorResultBuilder parseResult = new ValidatorResultBuilder(); YangDataDocument document = new YangDataDocumentJsonCodec(payloadSchemaContext).deserialize( - new ObjectMapper().readTree("{\"payload-anydata:payload-root\":{\"item\":[" - + "{\"id\":\"one\",\"details\":{\"name\":\"shared\"}}," - + "{\"id\":\"two\",\"details\":{\"name\":\"shared\"}}]}}"), parseResult); + new ObjectMapper().readTree("{\"payload-anydata:payload-root\":{\"value\":\"abc\",\"item\":[" + + "{\"id\":\"one\",\"name\":\"first\",\"details\":{\"name\":\"shared\"}}," + + "{\"id\":\"two\",\"name\":\"second\",\"details\":{\"name\":\"shared\"}}]}}"), parseResult); assertTrue(parseResult.build().isOk()); ValidatorResult result = document.validate(); assertFalse(result.isOk()); @@ -303,8 +303,8 @@ public void ordinaryListUniqueValidationUsesBuiltSchemaPaths() throws Exception public void omittedUniqueLeafDoesNotCountAsDuplicate() throws Exception { ValidatorResultBuilder parseResult = new ValidatorResultBuilder(); YangDataDocument document = new YangDataDocumentJsonCodec(payloadSchemaContext).deserialize( - new ObjectMapper().readTree("{\"payload-anydata:payload-root\":{\"item\":[" - + "{\"id\":\"one\"},{\"id\":\"two\"}]}}"), parseResult); + new ObjectMapper().readTree("{\"payload-anydata:payload-root\":{\"value\":\"abc\",\"item\":[" + + "{\"id\":\"one\",\"name\":\"first\"},{\"id\":\"two\",\"name\":\"second\"}]}}"), parseResult); assertTrue(parseResult.build().isOk()); assertTrue(document.validate().isOk()); } @@ -411,9 +411,9 @@ private YangList payloadList() { private ValidatorResult validatePayloadWithItemNames(String firstName, String secondName) throws Exception { String json = "{\"outer-anydata:anydata-wrapper\":{\"payload-holder\":{" - + "\"payload-anydata:payload-root\":{\"item\":[" - + "{\"id\":\"one\",\"details\":{\"name\":\"" + firstName + "\"}}," - + "{\"id\":\"two\",\"details\":{\"name\":\"" + secondName + "\"}}]}}}}"; + + "\"payload-anydata:payload-root\":{\"value\":\"abc\",\"item\":[" + + "{\"id\":\"one\",\"name\":\"first\",\"details\":{\"name\":\"" + firstName + "\"}}," + + "{\"id\":\"two\",\"name\":\"second\",\"details\":{\"name\":\"" + secondName + "\"}}]}}}}"; ValidatorResultBuilder parseResult = new ValidatorResultBuilder(); YangDataDocument document = new YangDataDocumentJsonCodec(outerSchemaContext).deserialize( new ObjectMapper().readTree(json), parseResult, diff --git a/yangkit-data-json-codec/src/test/resources/anydata-validation/payload/yang/payload-anydata.yang b/yangkit-data-json-codec/src/test/resources/anydata-validation/payload/yang/payload-anydata.yang index 72b73fbf..2dd1d081 100644 --- a/yangkit-data-json-codec/src/test/resources/anydata-validation/payload/yang/payload-anydata.yang +++ b/yangkit-data-json-codec/src/test/resources/anydata-validation/payload/yang/payload-anydata.yang @@ -12,6 +12,7 @@ module payload-anydata { list item { key "id"; + unique "details/name"; min-elements 1; max-elements 2; @@ -23,6 +24,11 @@ module payload-anydata { type string; mandatory true; } + container details { + leaf name { + type string; + } + } } leaf target { @@ -35,18 +41,5 @@ module payload-anydata { } } - list item { - key "id"; - unique "details/name"; - - leaf id { - type string; - } - container details { - leaf name { - type string; - } - } - } } }