Fix OpenApi ignoring min-elements for list 56/108456/28
authorMatej Sramcik <matej.sramcik@pantheon.tech>
Mon, 16 Oct 2023 12:37:38 +0000 (14:37 +0200)
committerIvan Hrasko <ivan.hrasko@pantheon.tech>
Fri, 3 Nov 2023 14:56:04 +0000 (14:56 +0000)
If list have min-elements parameter, it is ignored and example with
only one element is created.
Edited definitionGenerator to adhere to the amount of min-elements,
also if parameter is defined as key, unique examples are created for
string and number types.

MinItems and maxItems values are missing in list property,
so they are added.

JIRA: NETCONF-1172
Change-Id: I5411fe6dd451a87fd7bfa53eb47206685aa624e7
Signed-off-by: Matej Sramcik <matej.sramcik@pantheon.tech>
Signed-off-by: Peter Suna <peter.suna@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/DefinitionGeneratorTest.java
restconf/restconf-openapi/src/test/java/org/opendaylight/restconf/openapi/impl/OpenApiGeneratorRFC8040Test.java
restconf/restconf-openapi/src/test/java/org/opendaylight/restconf/openapi/mountpoints/MountPointOpenApiTest.java
restconf/restconf-openapi/src/test/resources/yang/test-container-childs.yang [new file with mode: 0644]

index f575b686a0e826ec75c4843999ffd6569a1f9ea5..b39116a080aa3f681e100bf618c7fc1e2d5e3da7 100644 (file)
@@ -23,9 +23,12 @@ import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import org.eclipse.jdt.annotation.NonNull;
 import org.opendaylight.restconf.openapi.model.Property;
 import org.opendaylight.restconf.openapi.model.Schema;
 import org.opendaylight.restconf.openapi.model.Xml;
+import org.opendaylight.yangtools.yang.common.AbstractQName;
 import org.opendaylight.yangtools.yang.common.Decimal64;
 import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.common.XMLNamespace;
@@ -51,6 +54,8 @@ import org.opendaylight.yangtools.yang.model.api.OperationDefinition;
 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
+import org.opendaylight.yangtools.yang.model.api.TypedDataSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.meta.ModelStatement;
 import org.opendaylight.yangtools.yang.model.api.type.BinaryTypeDefinition;
 import org.opendaylight.yangtools.yang.model.api.type.BitsTypeDefinition;
 import org.opendaylight.yangtools.yang.model.api.type.BitsTypeDefinition.Bit;
@@ -234,11 +239,19 @@ public final class DefinitionGenerator {
         final String name = filename + discriminator;
         final String ref = COMPONENTS_PREFIX + name;
 
-        if (schemaNode instanceof ListSchemaNode) {
+        if (schemaNode instanceof ListSchemaNode node) {
             dataNodeProperties.type(ARRAY_TYPE);
             final Property items = new Property.Builder().ref(ref).build();
             dataNodeProperties.items(items);
             dataNodeProperties.description(schemaNode.getDescription().orElse(""));
+            if (node.getElementCountConstraint().isPresent()) {
+                final var minElements = node.getElementCountConstraint().orElse(null).getMinElements();
+                dataNodeProperties.minItems(minElements);
+                dataNodeProperties.maxItems(node.getElementCountConstraint().orElse(null).getMaxElements());
+                if (minElements != null) {
+                    dataNodeProperties.example(createExamples(node, minElements));
+                }
+            }
         } else {
              /*
                 Description can't be added, because nothing allowed alongside $ref.
@@ -250,6 +263,66 @@ public final class DefinitionGenerator {
         return dataNodeProperties.build();
     }
 
+    private static List<Map<String, Object>> createExamples(final ListSchemaNode node,
+            @NonNull final Integer minElements) {
+        final var firstExampleMap = prepareFirstListExample(node);
+        final var examples = new ArrayList<Map<String, Object>>();
+        examples.add(firstExampleMap);
+
+        final var unqiueContraintsNameSet = node.getUniqueConstraints().stream()
+            .map(ModelStatement::argument)
+            .flatMap(uniqueSt -> uniqueSt.stream()
+                .map(schemaNI -> schemaNI.lastNodeIdentifier().getLocalName()))
+            .collect(Collectors.toSet());
+        final var keysNameSet = node.getKeyDefinition().stream()
+            .map(AbstractQName::getLocalName)
+            .collect(Collectors.toSet());
+        for (int i = 1; i < minElements; i++) {
+            final var exampleMap = new HashMap<String, Object>();
+            for (final var example : firstExampleMap.entrySet()) {
+                final Object exampleValue;
+                if (keysNameSet.contains(example.getKey()) || unqiueContraintsNameSet.contains(example.getKey())) {
+                    exampleValue = editExample(example.getValue(), i);
+                } else {
+                    exampleValue = example.getValue();
+                }
+                exampleMap.put(example.getKey(), exampleValue);
+            }
+            examples.add(exampleMap);
+        }
+        return examples;
+    }
+
+    private static HashMap<String, Object> prepareFirstListExample(final ListSchemaNode node) {
+        final var childNodes = node.getChildNodes();
+        final var firstExampleMap = new HashMap<String, Object>();
+        // Cycle for each child node
+        for (final var childNode : childNodes) {
+            if (childNode instanceof TypedDataSchemaNode leafSchemaNode) {
+                final var property = new Property.Builder();
+                processTypeDef(leafSchemaNode.getType(), leafSchemaNode, property, null);
+                final var exampleValue = property.build().example();
+                if (exampleValue != null) {
+                    firstExampleMap.put(leafSchemaNode.getQName().getLocalName(), exampleValue);
+                }
+            }
+        }
+        return firstExampleMap;
+    }
+
+    private static Object editExample(final Object exampleValue, final int edit) {
+        if (exampleValue instanceof String string) {
+            return string + "_" + edit;
+        } else if (exampleValue instanceof Integer number) {
+            return number + edit;
+        } else if (exampleValue instanceof Long number) {
+            return number + edit;
+        } else if (exampleValue instanceof Decimal64 number) {
+            return Decimal64.valueOf(BigDecimal.valueOf(number.intValue() + edit));
+        }
+        return exampleValue;
+    }
+
     private static Property processDataNodeContainer(final DataNodeContainer dataNode, final String parentName,
             final Map<String, Schema> definitions, final DefinitionNames definitionNames,
             final SchemaInferenceStack stack, final Module module, final boolean isParentConfig) throws IOException {
index a64f7a3b44b2f120cfa168c1675345eb3d40c351..3caa2ae01f3c8124df2b08c2dd7fc415db22f354 100644 (file)
@@ -15,6 +15,10 @@ import static org.mockito.Mockito.when;
 
 import java.io.IOException;
 import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -208,4 +212,99 @@ public final class DefinitionGeneratorTest {
         assertTrue(makeToast.enums().containsAll(Set.of("toast-type","white-bread", "wheat-bread", "frozen-waffle",
             "hash-brown", "frozen-bagel", "wonder-bread")));
     }
+
+    /**
+     * Test that checks if list min-elements and max-elements are present.
+     * Also checks if number of example elements meets the min-elements condition
+     */
+    @Test
+    public void testListExamples() throws IOException {
+        final var module = context.findModule("test-container-childs", Revision.of("2023-09-28")).orElseThrow();
+        final var jsonObject = DefinitionGenerator.convertToSchemas(module, context, new DefinitionNames(), true);
+        final var component = jsonObject.get("test-container-childs_root-container_nested-container");
+        assertNotNull(component);
+        assertNotNull(component.properties());
+        final var property = component.properties().get("mandatory-list");
+        assertNotNull(property);
+        assertNotNull(property.minItems());
+        assertNotNull(property.maxItems());
+        assertEquals(3, (int) property.minItems());
+        assertEquals(5, (int) property.maxItems());
+        final var example = property.example();
+        assertNotNull(example);
+        assertEquals(ArrayList.class, example.getClass());
+        assertEquals(3, ((List<?>)example).size());
+    }
+
+    /**
+     * Test that checks if list min-elements and max-elements are present.
+     * Also checks if number of example elements meets the min-elements condition
+     * and if key defined leaf have unique values.
+     */
+    @Test
+    public void testListExamplesWithNonKeyLeaf() throws IOException {
+        final var module = context.findModule("test-container-childs", Revision.of("2023-09-28")).orElseThrow();
+        final var jsonObject = DefinitionGenerator.convertToSchemas(module, context, new DefinitionNames(), true);
+        final var component = jsonObject.get("test-container-childs_root-container_nested-container");
+        assertNotNull(component);
+        assertNotNull(component.properties());
+        final var property = component.properties().get("mandatory-list");
+        assertNotNull(property);
+        assertNotNull(property.minItems());
+        assertNotNull(property.maxItems());
+        assertEquals(3, (int) property.minItems());
+        assertEquals(5, (int) property.maxItems());
+        final var example = property.example();
+        assertNotNull(example);
+        assertEquals(3, ((List<?>)example).size());
+        assertTrue(checkUniqueExample(example, "id"));
+    }
+
+    /**
+     * Test that checks if multiple key leafs have unique values.
+     * Also checks if nested container node is ignored.
+     */
+    @Test
+    public void testListExamplesWithTwoKeys() throws IOException {
+        final var module = context.findModule("test-container-childs", Revision.of("2023-09-28")).orElseThrow();
+        final var jsonObject = DefinitionGenerator.convertToSchemas(module, context, new DefinitionNames(), true);
+        final var component = jsonObject.get("test-container-childs_root-container-two-keys_nested-container-two-keys");
+        assertNotNull(component);
+        assertNotNull(component.properties());
+        final var property = component.properties().get("mandatory-list-two-keys");
+        assertNotNull(property);
+        final var example = property.example();
+        assertNotNull(example);
+        assertTrue(checkUniqueExample(example, "id"));
+        assertTrue(checkUniqueExample(example, "name"));
+        assertEquals(3, ((ArrayList<Map<?,?>>)example).get(0).size());
+    }
+
+    /**
+     * Test that checks if sets of unique defined leafs have unique combination of values.
+     */
+    @Test
+    public void testListExamplesWithUnique() throws IOException {
+        final var module = context.findModule("test-container-childs", Revision.of("2023-09-28")).orElseThrow();
+        final var jsonObject = DefinitionGenerator.convertToSchemas(module, context, new DefinitionNames(), true);
+        final var component = jsonObject.get("test-container-childs_root-container-unique_nested-container-unique");
+        assertNotNull(component);
+        assertNotNull(component.properties());
+        final var property = component.properties().get("mandatory-list-unique");
+        assertNotNull(property);
+        final var example = property.example();
+        assertNotNull(example);
+        assertTrue(checkUniqueExample(example, "id"));
+        assertTrue(checkUniqueExample(example, "name") || checkUniqueExample(example, "address"));
+    }
+
+    private static boolean checkUniqueExample(final Object examples, final String key) {
+        assertEquals(ArrayList.class, examples.getClass());
+        final var exampleValues = new HashSet<>();
+
+        for (final Map<String, Object> example : (ArrayList<Map<String, Object>>)examples) {
+            exampleValues.add(example.get(key));
+        }
+        return (exampleValues.size() == ((ArrayList<?>) examples).size());
+    }
 }
index 0322357c5099ead3743afaae437ac5eba0344d63..711dc48c00ced9ee0f66839e3277f71401f2a24c 100644 (file)
@@ -451,6 +451,83 @@ public final class OpenApiGeneratorRFC8040Test {
         assertEquals("urn:ietf:params:xml:ns:yang:test:action:types", namespace);
     }
 
+    /**
+     * Test that checks if list min-elements and max-elements are present.
+     * Also checks if number of example elements meets the min-elements condition
+     * and if key defined leaf have unique values.
+     */
+    @Test
+    public void testListExamplesWithNonKeyLeaf() {
+        final var doc = generator.getApiDeclaration("test-container-childs", "2023-09-28", uriInfo);
+        assertNotNull("Failed to find Datastore API", doc);
+        final var components = doc.components();
+        final var component = components.schemas().get("test-container-childs_root-container_nested-container");
+        assertNotNull(component);
+        assertNotNull(component.properties());
+        final var property = component.properties().get("mandatory-list");
+        assertNotNull(property);
+        assertNotNull(property.minItems());
+        assertNotNull(property.maxItems());
+        assertEquals(3, (int) property.minItems());
+        assertEquals(5, (int) property.maxItems());
+        final var example = property.example();
+        assertNotNull(example);
+        assertEquals(3, ((List<?>)example).size());
+        assertTrue(checkUniqueExample(example, "id"));
+    }
+
+    /**
+     * Test that checks if multiple key leafs have unique values.
+     * Also checks if nested container node is ignored.
+     */
+    @Test
+    public void testListExamplesWithTwoKeys() {
+        final var doc = generator.getApiDeclaration("test-container-childs", "2023-09-28", uriInfo);
+        assertNotNull("Failed to find Datastore API", doc);
+        final var components = doc.components();
+        final var component = components.schemas()
+            .get("test-container-childs_root-container-two-keys_nested-container-two-keys");
+        assertNotNull(component);
+        assertNotNull(component.properties());
+        final var property = component.properties().get("mandatory-list-two-keys");
+        assertNotNull(property);
+        final var example = property.example();
+        assertNotNull(example);
+        assertTrue(checkUniqueExample(example, "id"));
+        assertTrue(checkUniqueExample(example, "name"));
+        assertEquals(3, ((ArrayList<Map<?,?>>)example).get(0).size());
+    }
+
+    /**
+     * Test that checks if sets of unique defined leafs have unique combination of values.
+     */
+    @Test
+    public void testListExamplesWithUnique() {
+        final var doc = generator.getApiDeclaration("test-container-childs", "2023-09-28", uriInfo);
+        assertNotNull("Failed to find Datastore API", doc);
+        final var components = doc.components();
+        final var component = components.schemas()
+            .get("test-container-childs_root-container-unique_nested-container-unique");
+        assertNotNull(component);
+        assertNotNull(component.properties());
+        final var property = component.properties().get("mandatory-list-unique");
+        assertNotNull(property);
+        final var example = property.example();
+        assertNotNull(example);
+        assertTrue(checkUniqueExample(example, "id"));
+        assertTrue(checkUniqueExample(example, "name") || checkUniqueExample(example, "address"));
+    }
+
+    private static boolean checkUniqueExample(final Object examples, final String key) {
+        assertEquals(ArrayList.class, examples.getClass());
+        final var exampleValues = new HashSet<>();
+
+        for (final Map<String, Object> example : (ArrayList<Map<String, Object>>)examples) {
+            exampleValues.add(example.get(key));
+        }
+        return (exampleValues.size() == ((ArrayList<?>) examples).size());
+    }
+
     /**
      * Test that number of elements in payload is correct.
      */
index 02347da6499ef515002ccc9fe59dbc0d2597dd50..6a641950cc164a77a6f6c8a01227b918a138778c 100644 (file)
@@ -144,7 +144,7 @@ public final class MountPointOpenApiTest {
         final Map<String, Path> paths = mountPointApi.paths();
         assertNotNull(paths);
 
-        assertEquals("Unexpected api list size", 68, paths.size());
+        assertEquals("Unexpected api list size", 78, paths.size());
 
         final List<Operation> getOperations = new ArrayList<>();
         final List<Operation> postOperations = new ArrayList<>();
@@ -160,11 +160,11 @@ public final class MountPointOpenApiTest {
             Optional.ofNullable(path.getValue().delete()).ifPresent(deleteOperations::add);
         }
 
-        assertEquals("Unexpected GET paths size", 60, getOperations.size());
-        assertEquals("Unexpected POST paths size", 15, postOperations.size());
-        assertEquals("Unexpected PUT paths size", 58, putOperations.size());
-        assertEquals("Unexpected PATCH paths size", 58, patchOperations.size());
-        assertEquals("Unexpected DELETE paths size", 58, deleteOperations.size());
+        assertEquals("Unexpected GET paths size", 70, getOperations.size());
+        assertEquals("Unexpected POST paths size", 21, postOperations.size());
+        assertEquals("Unexpected PUT paths size", 68, putOperations.size());
+        assertEquals("Unexpected PATCH paths size", 68, patchOperations.size());
+        assertEquals("Unexpected DELETE paths size", 68, deleteOperations.size());
     }
 
     /**
diff --git a/restconf/restconf-openapi/src/test/resources/yang/test-container-childs.yang b/restconf/restconf-openapi/src/test/resources/yang/test-container-childs.yang
new file mode 100644 (file)
index 0000000..81854e2
--- /dev/null
@@ -0,0 +1,73 @@
+module test-container-childs {
+  yang-version 1.1;
+  namespace "http://example.com/test/container/child";
+  prefix "tcc";
+  revision 2023-09-28;
+
+  container root-container {
+    container nested-container {
+      list mandatory-list {
+        min-elements 3;
+        max-elements 5;
+        key id;
+        leaf id {
+          type uint32;
+        }
+        leaf name {
+          type string;
+        }
+        leaf address {
+          type string;
+        }
+      }
+    }
+  }
+
+  container root-container-two-keys {
+    container nested-container-two-keys {
+      list mandatory-list-two-keys {
+        min-elements 3;
+        max-elements 5;
+        key "id name";
+        leaf id {
+          type uint32;
+        }
+        leaf name {
+          type string;
+        }
+        leaf address {
+          type string;
+        }
+        container list-nested-container {
+          leaf nested-leaf {
+            type string;
+          }
+        }
+      }
+    }
+  }
+
+
+  container root-container-unique {
+    container nested-container-unique {
+      list mandatory-list-unique {
+        min-elements 3;
+        max-elements 5;
+        key id;
+        unique "name address";
+        leaf id {
+          type uint32;
+        }
+        leaf name {
+          type string;
+        }
+        leaf address {
+          type string;
+        }
+        leaf description {
+          type string;
+        }
+      }
+    }
+  }
+}