OpenApi: Add missing mandatory nodes to required 79/104979/29
authorPeter Suna <peter.suna@pantheon.tech>
Wed, 22 Mar 2023 08:10:45 +0000 (09:10 +0100)
committerIvan Hrasko <ivan.hrasko@pantheon.tech>
Wed, 21 Jun 2023 13:55:16 +0000 (15:55 +0200)
Add mandatory containers, lists and leaf-lists to
required JSON array in OpenApi if they are mandatory.

Based on RFC7950:
https://www.rfc-editor.org/rfc/rfc7950#page-14

JIRA: NETCONF-976
Change-Id: Ieca56ad9a325b59fdf57438594b89f34e3776e08
Signed-off-by: Peter Suna <peter.suna@pantheon.tech>
Signed-off-by: Ivan Hrasko <ivan.hrasko@pantheon.tech>
restconf/restconf-openapi/src/main/java/org/opendaylight/restconf/openapi/impl/DefinitionGenerator.java
restconf/restconf-openapi/src/test/java/org/opendaylight/restconf/openapi/impl/OpenApiGeneratorRFC8040Test.java

index be57c8d51a513eaf168e4e142fc8185f86871516..7b6a6527d03336a980dec85ec2dd0772c8eea07e 100644 (file)
@@ -44,6 +44,7 @@ import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
 import org.opendaylight.yangtools.yang.model.api.ElementCountConstraint;
+import org.opendaylight.yangtools.yang.model.api.ElementCountConstraintAware;
 import org.opendaylight.yangtools.yang.model.api.IdentitySchemaNode;
 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
@@ -179,6 +180,9 @@ public class DefinitionGenerator {
             final String localName = node.getQName().getLocalName();
             if (node.isConfiguration()) {
                 if (node instanceof ContainerSchemaNode || node instanceof ListSchemaNode) {
+                    if (isSchemaNodeMandatory(node)) {
+                        required.add(localName);
+                    }
                     for (final DataSchemaNode childNode : ((DataNodeContainer) node).getChildNodes()) {
                         final ObjectNode childNodeProperties = JsonNodeFactory.instance.objectNode();
 
@@ -224,6 +228,28 @@ public class DefinitionGenerator {
         definitions.put(definitionName, definitionBuilder.build());
     }
 
+    private static boolean isSchemaNodeMandatory(final DataSchemaNode node) {
+        //    https://www.rfc-editor.org/rfc/rfc7950#page-14
+        //    mandatory node: A mandatory node is one of:
+        if (node instanceof ContainerSchemaNode containerNode) {
+            //  A container node without a "presence" statement and that has at least one mandatory node as a child.
+            if (containerNode.isPresenceContainer()) {
+                return false;
+            }
+            for (final DataSchemaNode childNode : containerNode.getChildNodes()) {
+                if (childNode instanceof MandatoryAware mandatoryAware && mandatoryAware.isMandatory()) {
+                    return true;
+                }
+            }
+        }
+        //  A list or leaf-list node with a "min-elements" statement with a value greater than zero.
+        return node instanceof ElementCountConstraintAware constraintAware
+                && constraintAware.getElementCountConstraint()
+                .map(ElementCountConstraint::getMinElements)
+                .orElse(0)
+                > 0;
+    }
+
     private void processContainersAndLists(final Module module, final Map<String, Schema> definitions,
             final DefinitionNames definitionNames, final EffectiveModelContext schemaContext)  throws IOException {
         final String moduleName = module.getName();
@@ -454,12 +480,18 @@ public class DefinitionGenerator {
 
             final ObjectNode property;
             if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
+                if (isSchemaNodeMandatory(node)) {
+                    required.add(name);
+                }
                 property = processDataNodeContainer((DataNodeContainer) node, parentName, definitions,
                         definitionNames, isConfig, stack);
                 if (!isConfig) {
                     processActionNodeContainer(node, parentName, definitions, definitionNames, stack);
                 }
             } else if (node instanceof LeafListSchemaNode leafList) {
+                if (isSchemaNodeMandatory(node)) {
+                    required.add(name);
+                }
                 property = processLeafListNode(leafList, stack, definitions, definitionNames);
 
             } else if (node instanceof ChoiceSchemaNode choice) {
index 72c67b138e945848b8a911f6cce6401315ed5bd0..e0f179e8615a549920cc259abfa8da78c1c9c97f 100644 (file)
@@ -17,9 +17,12 @@ import static org.mockito.Mockito.when;
 import static org.opendaylight.restconf.openapi.OpenApiTestUtils.getPathParameters;
 
 import com.fasterxml.jackson.databind.JsonNode;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.opendaylight.mdsal.dom.api.DOMSchemaService;
@@ -36,6 +39,13 @@ public final class OpenApiGeneratorRFC8040Test {
     private static final String TOASTER_2 = "toaster2";
     private static final String REVISION_DATE = "2009-11-20";
     private static final String MANDATORY_TEST = "mandatory-test";
+    private static final String CONFIG_ROOT_CONTAINER = "mandatory-test_config_root-container";
+    private static final String ROOT_CONTAINER = "mandatory-test_root-container";
+    private static final String CONFIG_MANDATORY_CONTAINER = "mandatory-test_root-container_config_mandatory-container";
+    private static final String MANDATORY_CONTAINER = "mandatory-test_root-container_mandatory-container";
+    private static final String CONFIG_MANDATORY_LIST = "mandatory-test_root-container_config_mandatory-list";
+    private static final String MANDATORY_LIST = "mandatory-test_root-container_mandatory-list";
+    private static final String MANDATORY_TEST_MODULE = "mandatory-test_module";
 
     private static EffectiveModelContext context;
     private static DOMSchemaService schemaService;
@@ -194,23 +204,32 @@ public final class OpenApiGeneratorRFC8040Test {
         final var doc = generator.getOpenApiSpec(module, "http", "localhost:8181", "/", "", context);
         assertNotNull(doc);
         final var schemas = doc.components().schemas();
-        //TODO: missing mandatory-container, mandatory-list
-        final var reqRootContainerElements = "[\"mandatory-root-leaf\",\"mandatory-first-choice\"]";
-        verifyRequiredField(schemas.get("mandatory-test_config_root-container"), reqRootContainerElements);
-        verifyRequiredField(schemas.get("mandatory-test_root-container"), reqRootContainerElements);
-
-        //TODO: missing leaf-list-with-min-elements
-        final var reqMandatoryContainerElements = "[\"mandatory-leaf\"]";
-        verifyRequiredField(schemas.get("mandatory-test_root-container_config_mandatory-container"),
-            reqMandatoryContainerElements);
-        verifyRequiredField(schemas.get("mandatory-test_root-container_mandatory-container"),
-            reqMandatoryContainerElements);
-
-        final var reqMandatoryListElements = "[\"mandatory-list-field\"]";
-        verifyRequiredField(schemas.get("mandatory-test_root-container_config_mandatory-list"),
-                reqMandatoryListElements);
-        verifyRequiredField(schemas.get("mandatory-test_root-container_mandatory-list"), reqMandatoryListElements);
-        //TODO: missing required field inside "mandatory-test_module" with ["root-container","root-mandatory-list"]
+        final var containersWithRequired = new ArrayList<String>();
+
+        final var reqRootContainerElements = Set.of("mandatory-root-leaf", "mandatory-container",
+            "mandatory-first-choice", "mandatory-list");
+        verifyRequiredField(schemas.get(CONFIG_ROOT_CONTAINER), reqRootContainerElements);
+        containersWithRequired.add(CONFIG_ROOT_CONTAINER);
+        verifyRequiredField(schemas.get(ROOT_CONTAINER), reqRootContainerElements);
+        containersWithRequired.add(ROOT_CONTAINER);
+
+        final var reqMandatoryContainerElements = Set.of("mandatory-leaf", "leaf-list-with-min-elements");
+        verifyRequiredField(schemas.get(CONFIG_MANDATORY_CONTAINER), reqMandatoryContainerElements);
+        containersWithRequired.add(CONFIG_MANDATORY_CONTAINER);
+        verifyRequiredField(schemas.get(MANDATORY_CONTAINER), reqMandatoryContainerElements);
+        containersWithRequired.add(MANDATORY_CONTAINER);
+
+        final var reqMandatoryListElements = Set.of("mandatory-list-field");
+        verifyRequiredField(schemas.get(CONFIG_MANDATORY_LIST), reqMandatoryListElements);
+        containersWithRequired.add(CONFIG_MANDATORY_LIST);
+        verifyRequiredField(schemas.get(MANDATORY_LIST), reqMandatoryListElements);
+        containersWithRequired.add(MANDATORY_LIST);
+
+        final var testModuleMandatoryArray = Set.of("root-container", "root-mandatory-list");
+        verifyRequiredField(schemas.get(MANDATORY_TEST_MODULE), testModuleMandatoryArray);
+        containersWithRequired.add(MANDATORY_TEST_MODULE);
+
+        verifyThatOthersNodeDoesNotHaveRequiredField(containersWithRequired, schemas);
     }
 
     /**
@@ -403,11 +422,40 @@ public final class OpenApiGeneratorRFC8040Test {
         assertEquals(expectedXmlRef, postXmlRef.textValue());
     }
 
-    private static void verifyRequiredField(final Schema rootContainer, final String expected) {
+    private static void verifyThatOthersNodeDoesNotHaveRequiredField(final List<String> expected,
+            final Map<String, Schema> definitions) {
+        for (final var value : definitions.values()) {
+            final var properties = value.properties();
+            if (properties != null) {
+                verifyRecursivelyThatPropertyDoesNotHaveRequired(expected, properties);
+            }
+        }
+    }
+
+    private static void verifyRecursivelyThatPropertyDoesNotHaveRequired(final List<String> expected,
+            final JsonNode definitions) {
+        final var fields = definitions.fields();
+        while (fields.hasNext()) {
+            final var next = fields.next();
+            final var nodeName = next.getKey();
+            final var jsonNode = next.getValue();
+            if (expected.contains(nodeName) || !jsonNode.isContainerNode()) {
+                continue;
+            }
+            assertNull("Json node " + nodeName + " should not have 'required' field in body",
+                jsonNode.get("required"));
+            verifyRecursivelyThatPropertyDoesNotHaveRequired(expected, jsonNode);
+        }
+    }
+
+    private static void verifyRequiredField(final Schema rootContainer, final Set<String> expected) {
         assertNotNull(rootContainer);
-        final var requiredNode = rootContainer.required();
-        assertNotNull(requiredNode);
-        assertTrue(requiredNode.isArray());
-        assertEquals(expected, requiredNode.toString());
+        final var required = rootContainer.required();
+        assertNotNull(required);
+        assertTrue(required.isArray());
+        final var actualContainerArray = StreamSupport.stream(required.spliterator(), false)
+            .map(JsonNode::textValue)
+            .collect(Collectors.toSet());
+        assertEquals(expected, actualContainerArray);
     }
 }