Use first child in POST payloads 74/107474/1
authorYaroslav Lastivka <yaroslav.lastivka@pantheon.tech>
Fri, 4 Aug 2023 07:40:33 +0000 (10:40 +0300)
committerYaroslav Lastivka <yaroslav.lastivka@pantheon.tech>
Wed, 23 Aug 2023 08:39:11 +0000 (11:39 +0300)
Enhanced the logic to generate POST payloads by choosing one
child for both container and list types.
Introduced unit tests to validate the correctness of the
request body generated by this new logic.

JIRA: NETCONF-1054
Change-Id: I8568f570fe77a9ef0b7f20122feef6a85758ccef
Signed-off-by: Yaroslav Lastivka <yaroslav.lastivka@pantheon.tech>
Signed-off-by: Ivan Hrasko <ivan.hrasko@pantheon.tech>
(cherry picked from commit 257accd8ae1427e5e23df77fcde2cc58ac1a454d)

restconf/sal-rest-docgen/src/main/java/org/opendaylight/netconf/sal/rest/doc/impl/BaseYangSwaggerGenerator.java
restconf/sal-rest-docgen/src/main/java/org/opendaylight/netconf/sal/rest/doc/model/builder/OperationBuilder.java
restconf/sal-rest-docgen/src/test/java/org/opendaylight/netconf/sal/rest/doc/impl/ApiDocGeneratorRFC8040Test.java
restconf/sal-rest-docgen/src/test/java/org/opendaylight/netconf/sal/rest/doc/impl/PostPayloadTest.java [new file with mode: 0644]
restconf/sal-rest-docgen/src/test/resources/yang/container-test@2023-07-31.yang [new file with mode: 0644]
restconf/sal-rest-docgen/src/test/resources/yang/list-test@2023-07-31.yang [new file with mode: 0644]

index 9f8edb7630d1e14b445fb298db4f974534e53017..5d2f0a1662790b848a233d4293ade70004250d0e 100644 (file)
@@ -383,7 +383,7 @@ public abstract class BaseYangSwaggerGenerator {
             final ObjectNode post = JsonNodeFactory.instance.objectNode();
             final String moduleName = module.getName();
             final String name = moduleName + MODULE_NAME_SUFFIX;
-            post.set("post", buildPost("", name, "", moduleName, deviceName,
+            post.set("post", buildPost(null, "", name, "", moduleName, deviceName,
                     module.getDescription().orElse(""), pathParams, oaversion));
             paths.set(resourcePath, post);
         }
@@ -513,7 +513,7 @@ public abstract class BaseYangSwaggerGenerator {
             final ObjectNode delete = buildDelete(node, moduleName, deviceName, pathParams, oaversion);
             operations.put("delete", delete);
 
-            operations.put("post", buildPost(parentName, nodeName, discriminator, moduleName, deviceName,
+            operations.put("post", buildPost(node, parentName, nodeName, discriminator, moduleName, deviceName,
                     node.getDescription().orElse(""), pathParams, oaversion));
         }
         return operations;
index 85eb90692fb37e8ce5a3024192681fb52606c6e2..15501eea7ba291f8f0f8978a10b7e19cb2ee38d1 100644 (file)
@@ -24,8 +24,11 @@ import javax.ws.rs.core.Response;
 import org.opendaylight.netconf.sal.rest.doc.impl.ApiDocServiceImpl.OAversion;
 import org.opendaylight.netconf.sal.rest.doc.impl.DefinitionNames;
 import org.opendaylight.netconf.sal.rest.doc.util.JsonUtil;
+import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.InputSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.OperationDefinition;
 import org.opendaylight.yangtools.yang.model.api.OutputSchemaNode;
 
@@ -72,10 +75,10 @@ public final class OperationBuilder {
 
     }
 
-    public static ObjectNode buildPost(final String parentName, final String nodeName, final String discriminator,
-                                       final String moduleName, final Optional<String> deviceName,
-                                       final String description, final ArrayNode pathParams,
-                                       final OAversion oaversion) {
+    public static ObjectNode buildPost(final DataSchemaNode node, final String parentName, final String nodeName,
+                                       final String discriminator, final String moduleName,
+                                       final Optional<String> deviceName, final String description,
+                                       final ArrayNode pathParams, final OAversion oaversion) {
         final ObjectNode value = JsonNodeFactory.instance.objectNode();
         value.put(DESCRIPTION_KEY, description);
         value.put(SUMMARY_KEY, buildSummaryValue(HttpMethod.POST, moduleName, deviceName, nodeName));
@@ -86,7 +89,17 @@ public final class OperationBuilder {
         final String defName = cleanDefName + discriminator;
         final String xmlDefName = cleanDefName + XML_SUFFIX + discriminator;
         ref.put(REF_KEY, getAppropriateModelPrefix(oaversion) + defName);
-        insertRequestBodyParameter(parameters, value, defName, xmlDefName, nodeName + CONFIG, oaversion);
+        final DataSchemaNode childNode = getListOrContainerChildNode(Optional.ofNullable(node));
+        if (childNode != null && childNode.isConfiguration()) {
+            final String childNodeName = childNode.getQName().getLocalName();
+            final String cleanChildDefName = parentName + "_" + nodeName + CONFIG + "_" + childNodeName + POST_SUFFIX;
+            final String childDefName = cleanChildDefName + discriminator;
+            final String childXmlDefName = cleanChildDefName + XML_SUFFIX + discriminator;
+            insertPostRequestBodyParameter(childNode, parameters, value, childDefName, childXmlDefName, childNodeName,
+                oaversion);
+        } else {
+            insertRequestBodyParameter(parameters, value, defName, xmlDefName, nodeName + CONFIG, oaversion);
+        }
         value.set(PARAMETERS_KEY, parameters);
 
         final ObjectNode responses = JsonNodeFactory.instance.objectNode();
@@ -283,6 +296,40 @@ public final class OperationBuilder {
         }
     }
 
+    private static void insertPostRequestBodyParameter(final DataSchemaNode childNode, final ArrayNode parameters,
+            final ObjectNode operation, final String defName, final String xmlDefName, final String name,
+            final OAversion oaversion) {
+        final ObjectNode payload = JsonNodeFactory.instance.objectNode();
+        if (oaversion.equals(OAversion.V3_0)) {
+            final ObjectNode content = JsonNodeFactory.instance.objectNode();
+            final ObjectNode properties = JsonNodeFactory.instance.objectNode();
+            if (childNode instanceof ListSchemaNode) {
+                final ObjectNode list = JsonNodeFactory.instance.objectNode();
+                final ObjectNode listValue = JsonNodeFactory.instance.objectNode();
+                listValue.put(TYPE_KEY, "array");
+                listValue.set("items", buildRefSchema(defName, oaversion));
+                list.set(name, listValue);
+                properties.set(PROPERTIES_KEY, list);
+            } else {
+                final ObjectNode container = JsonNodeFactory.instance.objectNode();
+                container.set(name, buildRefSchema(defName, oaversion));
+                properties.set(PROPERTIES_KEY, container);
+            }
+            final ObjectNode jsonSchema = JsonNodeFactory.instance.objectNode();
+            jsonSchema.set(SCHEMA_KEY, properties);
+            content.set(MediaType.APPLICATION_JSON, jsonSchema);
+            content.set(MediaType.APPLICATION_XML, buildMimeTypeValue(xmlDefName));
+            payload.set(CONTENT_KEY, content);
+            payload.put(DESCRIPTION_KEY, name);
+            operation.set(REQUEST_BODY_KEY, payload);
+        } else {
+            payload.put(IN_KEY, BODY);
+            payload.put(NAME_KEY, name);
+            payload.set(SCHEMA_KEY, buildRefSchema(defName, OAversion.V2_0));
+            parameters.add(payload);
+        }
+    }
+
     private static ObjectNode buildRefSchema(final String defName, final OAversion oaversion) {
         final ObjectNode schema = JsonNodeFactory.instance.objectNode();
         schema.put(REF_KEY, getAppropriateModelPrefix(oaversion) + defName);
@@ -372,4 +419,10 @@ public final class OperationBuilder {
         }
         return parameter;
     }
+
+    private static DataSchemaNode getListOrContainerChildNode(final Optional<DataSchemaNode> node) {
+        return node.flatMap(schemaNode -> ((DataNodeContainer) schemaNode).getChildNodes().stream()
+            .filter(n -> n instanceof ListSchemaNode || n instanceof ContainerSchemaNode)
+            .findFirst()).orElse(null);
+    }
 }
index bf03c0be3e7201a199d4a9584cc8452bdf2289c5..155df2bed471f61c3cb16d8b61349c2137f4b30d 100644 (file)
@@ -47,6 +47,8 @@ public final class ApiDocGeneratorRFC8040Test extends AbstractApiDocTest {
     private static final String MANDATORY_TEST_MODULE = "mandatory-test_module";
     private static final String CHOICE_TEST_MODULE = "choice-test";
     private static final String PROPERTIES = "properties";
+    private static final String CONTAINER = "container";
+    private static final String LIST = "list";
 
     private final ApiDocGeneratorRFC8040 generator = new ApiDocGeneratorRFC8040(SCHEMA_SERVICE);
 
@@ -335,17 +337,18 @@ public final class ApiDocGeneratorRFC8040Test extends AbstractApiDocTest {
         final var doc = (OpenApiObject) generator.getApiDeclaration(NAME, REVISION_DATE, URI_INFO,
             ApiDocServiceImpl.OAversion.V3_0);
         final var jsonNodeToaster = doc.getPaths().get("/rests/data/toaster2:toaster");
-        verifyRequestRef(jsonNodeToaster.path("post"), "#/components/schemas/toaster2_config_toaster_post",
-                "#/components/schemas/toaster2_config_toaster_post_xml");
+        verifyPostRequestRef(jsonNodeToaster.path("post"),
+            "#/components/schemas/toaster2_toaster_config_toasterSlot_post",
+            "#/components/schemas/toaster2_toaster_config_toasterSlot_post_xml", LIST);
         verifyRequestRef(jsonNodeToaster.path("put"), "#/components/schemas/toaster2_config_toaster_TOP",
                 "#/components/schemas/toaster2_config_toaster");
         verifyRequestRef(jsonNodeToaster.path("get"), "#/components/schemas/toaster2_toaster_TOP",
                 "#/components/schemas/toaster2_toaster");
 
         final var jsonNodeToasterSlot = doc.getPaths().get("/rests/data/toaster2:toaster/toasterSlot={slotId}");
-        verifyRequestRef(jsonNodeToasterSlot.path("post"),
-                "#/components/schemas/toaster2_toaster_config_toasterSlot_post",
-                "#/components/schemas/toaster2_toaster_config_toasterSlot_post_xml");
+        verifyPostRequestRef(jsonNodeToasterSlot.path("post"),
+                "#/components/schemas/toaster2_toaster_toasterSlot_config_slotInfo_post",
+                "#/components/schemas/toaster2_toaster_toasterSlot_config_slotInfo_post_xml", CONTAINER);
         verifyRequestRef(jsonNodeToasterSlot.path("put"),
                 "#/components/schemas/toaster2_toaster_config_toasterSlot_TOP",
                 "#/components/schemas/toaster2_toaster_config_toasterSlot");
@@ -364,8 +367,8 @@ public final class ApiDocGeneratorRFC8040Test extends AbstractApiDocTest {
                 "#/components/schemas/toaster2_toaster_toasterSlot_slotInfo");
 
         final var jsonNodeLst = doc.getPaths().get("/rests/data/toaster2:lst");
-        verifyRequestRef(jsonNodeLst.path("post"), "#/components/schemas/toaster2_config_lst_post",
-                "#/components/schemas/toaster2_config_lst_post_xml");
+        verifyPostRequestRef(jsonNodeLst.path("post"), "#/components/schemas/toaster2_lst_config_cont1_post",
+                "#/components/schemas/toaster2_lst_config_cont1_post_xml", CONTAINER);
         verifyRequestRef(jsonNodeLst.path("put"), "#/components/schemas/toaster2_config_lst_TOP",
                 "#/components/schemas/toaster2_config_lst");
         verifyRequestRef(jsonNodeLst.path("get"), "#/components/schemas/toaster2_lst_TOP",
@@ -418,4 +421,27 @@ public final class ApiDocGeneratorRFC8040Test extends AbstractApiDocTest {
         assertNotNull(postXmlRef);
         assertEquals(expectedXmlRef, postXmlRef.textValue());
     }
+
+    private static void verifyPostRequestRef(final JsonNode path, final String expectedJsonRef,
+        final String expectedXmlRef, String nodeType) {
+        final JsonNode postContent;
+        if (path.get("requestBody") != null) {
+            postContent = path.get("requestBody").get("content");
+        } else {
+            postContent = path.get("responses").get("200").get("content");
+        }
+        assertNotNull(postContent);
+        final String postJsonRef;
+        if (nodeType.equals(CONTAINER)) {
+            postJsonRef = postContent.path("application/json").path("schema").path("properties").elements().next()
+                .path("$ref").textValue();
+        } else {
+            postJsonRef = postContent.path("application/json").path("schema").path("properties").elements().next()
+                .path("items").path("$ref").textValue();
+        }
+        assertEquals(expectedJsonRef, postJsonRef);
+        final var postXmlRef = postContent.get("application/xml").get("schema").get("$ref");
+        assertNotNull(postXmlRef);
+        assertEquals(expectedXmlRef, postXmlRef.textValue());
+    }
 }
diff --git a/restconf/sal-rest-docgen/src/test/java/org/opendaylight/netconf/sal/rest/doc/impl/PostPayloadTest.java b/restconf/sal-rest-docgen/src/test/java/org/opendaylight/netconf/sal/rest/doc/impl/PostPayloadTest.java
new file mode 100644 (file)
index 0000000..388db76
--- /dev/null
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2023 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.sal.rest.doc.impl;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.opendaylight.netconf.sal.rest.doc.impl.ApiDocServiceImpl.OAversion;
+import org.opendaylight.netconf.sal.rest.doc.swagger.OpenApiObject;
+
+/**
+ * These tests are designed to verify that the specified path contains corresponding references in post requests
+ * which contains lists and containers.
+ *
+ * <p>
+ * The purpose of this test is to ensure that the specified path in the document contains the necessary references
+ * in its post requests for both JSON and XML content types. It verifies that the expected structure is in place,
+ * and the application can successfully retrieve the corresponding references for further processing.
+ */
+public class PostPayloadTest extends AbstractApiDocTest {
+    private static final String CONTENT_KEY = "content";
+    private static final String SCHEMA_KEY = "schema";
+
+    private static OpenApiObject containerDoc;
+    private static OpenApiObject listDoc;
+
+    @BeforeClass
+    public static void startUp() {
+        final var generator = new ApiDocGeneratorRFC8040(SCHEMA_SERVICE);
+        containerDoc = (OpenApiObject) generator.getApiDeclaration("container-test", "2023-07-31", URI_INFO,
+            OAversion.V3_0);
+        assertNotNull(containerDoc);
+        listDoc = (OpenApiObject) generator.getApiDeclaration("list-test", "2023-07-31", URI_INFO, OAversion.V3_0);
+        assertNotNull(listDoc);
+    }
+
+    @Test
+    public void testContainersPostPayloads() {
+        final var path1 = "/rests/data/container-test:cont";
+        assertNotNull(containerDoc.getPaths().get(path1));
+        final var jsonRef1 = getJsonRef(containerDoc, path1);
+        assertEquals("{\"cont1\":{\"$ref\":\"#/components/schemas/container-test_cont_config_cont1_post\"}}",
+            jsonRef1);
+        final var xmlRef1 = getXmlRef(containerDoc, path1);
+        assertEquals("#/components/schemas/container-test_cont_config_cont1_post_xml", xmlRef1);
+
+        final var path2 = "/rests/data/container-test:cont/cont1";
+        assertNotNull(containerDoc.getPaths().get(path2));
+        final var jsonRef2 = getJsonRef(containerDoc, path2);
+        assertEquals("{\"list4\":{\"type\":\"array\",\"items\":{\"$ref\":\""
+            + "#/components/schemas/container-test_cont_cont1_config_list4_post\"}}}", jsonRef2);
+        final var xmlRef2 = getXmlRef(containerDoc, path2);
+        assertEquals("#/components/schemas/container-test_cont_cont1_config_list4_post_xml", xmlRef2);
+
+        final var path3 = "/rests/data/container-test:cont/cont1/list4={key4}";
+        assertNotNull(containerDoc.getPaths().get(path3));
+        final var jsonRef3 = getJsonRef(containerDoc, path3);
+        assertEquals("{\"cont2\":{\"$ref\":\"#/components/schemas/"
+                + "container-test_cont_cont1_list4_config_cont2_post\"}}", jsonRef3);
+        final var xmlRef3 = getXmlRef(containerDoc, path3);
+        assertEquals("#/components/schemas/container-test_cont_cont1_list4_config_cont2_post_xml", xmlRef3);
+
+        final var path4 = "/rests/data/container-test:cont/cont1/list4={key4}/cont2";
+        assertNotNull(containerDoc.getPaths().get(path4));
+        final var jsonRef4 = getJsonRef(containerDoc, path4);
+        assertEquals("{\"list5\":{\"type\":\"array\",\"items\":{\"$ref\":\""
+                + "#/components/schemas/container-test_cont_cont1_list4_cont2_config_list5_post\"}}}", jsonRef4);
+        final var xmlRef4 = getXmlRef(containerDoc, path4);
+        assertEquals("#/components/schemas/container-test_cont_cont1_list4_cont2_config_list5_post_xml", xmlRef4);
+    }
+
+    @Test
+    public void testListsPostPayloads() {
+        final var path1 = "/rests/data/list-test:cont";
+        assertNotNull(listDoc.getPaths().get(path1));
+        final var jsonRef1 = getJsonRef(listDoc, path1);
+        assertEquals("{\"list1\":{\"type\":\"array\",\"items\":{\"$ref\":\""
+            + "#/components/schemas/list-test_cont_config_list1_post\"}}}", jsonRef1);
+        final var xmlRef1 = getXmlRef(listDoc, path1);
+        assertEquals("#/components/schemas/list-test_cont_config_list1_post_xml", xmlRef1);
+
+        final var path2 = "/rests/data/list-test:cont/list2={key2}";
+        assertNotNull(listDoc.getPaths().get(path2));
+        final var jsonRef2 = getJsonRef(listDoc, path2);
+        assertEquals("{\"list3\":{\"type\":\"array\",\"items\":{\"$ref\":\""
+            + "#/components/schemas/list-test_cont_list2_config_list3_post\"}}}", jsonRef2);
+        final var xmlRef2 = getXmlRef(listDoc, path2);
+        assertEquals("#/components/schemas/list-test_cont_list2_config_list3_post_xml", xmlRef2);
+    }
+
+    private static String getJsonRef(final OpenApiObject openApiObject, final String path) {
+        return openApiObject.getPaths().get(path).get("post").get("requestBody").get(CONTENT_KEY)
+            .get("application/json").get(SCHEMA_KEY).get("properties").toString();
+    }
+
+    private static String getXmlRef(final OpenApiObject openApiObject, final String path) {
+        return openApiObject.getPaths().get(path).get("post").get("requestBody").get(CONTENT_KEY)
+            .get("application/xml").get(SCHEMA_KEY).get("$ref").asText();
+    }
+}
diff --git a/restconf/sal-rest-docgen/src/test/resources/yang/container-test@2023-07-31.yang b/restconf/sal-rest-docgen/src/test/resources/yang/container-test@2023-07-31.yang
new file mode 100644 (file)
index 0000000..d72645a
--- /dev/null
@@ -0,0 +1,35 @@
+module container-test {
+  namespace "urn:opendaylight:params:xml:ns:yang:netconf:monitoring:cont-test";
+  prefix "ctest";
+  revision 2023-07-31 {
+    description "Test model.";
+  }
+
+  container cont {
+    container cont1 {
+      list list4 {
+        key "key4";
+        leaf key4 {
+          type string;
+        }
+        leaf value4 {
+          type string;
+        }
+        container cont2 {
+          leaf value7 {
+            type string;
+          }
+          list list5 {
+            key "key5";
+            leaf key5 {
+              type string;
+            }
+            leaf value5 {
+              type string;
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/restconf/sal-rest-docgen/src/test/resources/yang/list-test@2023-07-31.yang b/restconf/sal-rest-docgen/src/test/resources/yang/list-test@2023-07-31.yang
new file mode 100644 (file)
index 0000000..ec454ce
--- /dev/null
@@ -0,0 +1,36 @@
+module list-test {
+  namespace "urn:opendaylight:params:xml:ns:yang:netconf:monitoring:list-test";
+  prefix "ltest";
+  revision 2023-07-31 {
+    description "Test model.";
+  }
+  container cont {
+    list list1 {
+      key "key1";
+      leaf key1 {
+        type string;
+      }
+      leaf value1 {
+        type string;
+      }
+    }
+    list list2 {
+      key "key2";
+      leaf key2 {
+        type string;
+      }
+      leaf value2 {
+        type string;
+      }
+      list list3 {
+        key "key3";
+        leaf key3 {
+          type string;
+        }
+        leaf value3 {
+          type string;
+        }
+      }
+    }
+  }
+}