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
Original file line number Diff line number Diff line change
Expand Up @@ -431,11 +431,29 @@ public YangData<?> removeDataChild(DataIdentifier identifier) {
return dataChild;
}

private boolean matchUnique(Unique unique,List<YangData<?>> uniqueData,ListData listData){
private boolean matchUnique(Unique unique,List<YangData<?>> uniqueData,ListData listData,
ValidatorResultBuilder validatorResultBuilder){
List<YangData<?>> matchedUniqueData = new ArrayList<>();
List<SchemaPath.Descendant> descendants = new ArrayList<>();
for(Leaf leaf:unique.getUniqueNodes()){
List<QName> steps = listData.getSchemaNode().getSchemaPath().getRelativeSchemaPath(leaf.getSchemaPath());
SchemaPath.Descendant descendant = new DescendantSchemaPath(steps,listData.getSchemaNode());
SchemaPath.Absolute listSchemaPath = listData.getSchemaNode().getSchemaPath();
List<QName> steps = listSchemaPath == null ? null
: listSchemaPath.getRelativeSchemaPath(leaf.getSchemaPath());
if (steps == null || steps.isEmpty()) {
ValidatorRecordBuilder<AbsolutePath, YangData<?>> 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<YangData<?>> matched = YangDataUtil.search(listData,descendant);
if(matched.isEmpty()){
return false;
Expand Down Expand Up @@ -467,7 +485,7 @@ private ValidatorResult checkUniques(YangList list, List<YangData<?>> 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){
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -19,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 {
Expand Down Expand Up @@ -249,4 +259,171 @@ 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());
}

@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\":{\"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());
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\":{\"value\":\"abc\",\"item\":["
+ "{\"id\":\"one\",\"name\":\"first\"},{\"id\":\"two\",\"name\":\"second\"}]}}"), 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\":{\"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,
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());
assertSame(payloadList(), anydata.getValue().getDataChildren("item").get(0).getSchemaNode());
return anydata.getValue().validate();
}
}
Original file line number Diff line number Diff line change
@@ -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<QName> ancestors) {
List<QName> 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);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module payload-anydata {

list item {
key "id";
unique "details/name";
min-elements 1;
max-elements 2;

Expand All @@ -23,6 +24,11 @@ module payload-anydata {
type string;
mandatory true;
}
container details {
leaf name {
type string;
}
}
}

leaf target {
Expand All @@ -34,5 +40,6 @@ module payload-anydata {
path "../target";
}
}

}
}
Original file line number Diff line number Diff line change
@@ -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; }
}
}
Loading
Loading