Fix module's root POST request payload
[netconf.git] / restconf / restconf-openapi / src / main / java / org / opendaylight / restconf / openapi / model / builder / OperationBuilder.java
1 /*
2  * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.restconf.openapi.model.builder;
9
10 import static javax.ws.rs.core.Response.Status.NO_CONTENT;
11 import static javax.ws.rs.core.Response.Status.OK;
12 import static org.opendaylight.restconf.openapi.impl.DefinitionGenerator.INPUT_SUFFIX;
13 import static org.opendaylight.restconf.openapi.impl.DefinitionGenerator.OUTPUT_SUFFIX;
14
15 import java.util.AbstractMap.SimpleEntry;
16 import java.util.ArrayList;
17 import java.util.HashMap;
18 import java.util.List;
19 import java.util.Map;
20 import javax.ws.rs.HttpMethod;
21 import javax.ws.rs.core.MediaType;
22 import javax.ws.rs.core.Response;
23 import org.eclipse.jdt.annotation.NonNull;
24 import org.opendaylight.restconf.openapi.impl.DefinitionNames;
25 import org.opendaylight.restconf.openapi.model.MediaTypeObject;
26 import org.opendaylight.restconf.openapi.model.Operation;
27 import org.opendaylight.restconf.openapi.model.Parameter;
28 import org.opendaylight.restconf.openapi.model.Property;
29 import org.opendaylight.restconf.openapi.model.RequestBody;
30 import org.opendaylight.restconf.openapi.model.ResponseObject;
31 import org.opendaylight.restconf.openapi.model.Schema;
32 import org.opendaylight.restconf.openapi.model.Xml;
33 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
34 import org.opendaylight.yangtools.yang.model.api.InputSchemaNode;
35 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
36 import org.opendaylight.yangtools.yang.model.api.OperationDefinition;
37 import org.opendaylight.yangtools.yang.model.api.OutputSchemaNode;
38
39 public final class OperationBuilder {
40     public static final String COMPONENTS_PREFIX = "#/components/schemas/";
41     public static final String INPUT_KEY = "input";
42     public static final String SUMMARY_TEMPLATE = "%s - %s - %s - %s";
43     private static final List<String> MIME_TYPES = List.of(MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON);
44     private static final String OBJECT = "object";
45     private static final String POST_DESCRIPTION = """
46         \n
47         Note:
48         In example payload, you can see only the first data node child of the resource to be created, following the
49         guidelines of RFC 8040, which allows us to create only one resource in POST request.
50         """;
51
52     private OperationBuilder() {
53         // Hidden on purpose
54     }
55
56     public static Operation buildPost(final @NonNull DataSchemaNode childNode, final String parentName,
57             final String nodeName, final String discriminator, final String moduleName,
58             final @NonNull String deviceName, final String description, final List<Parameter> pathParams) {
59         final var summary = SUMMARY_TEMPLATE.formatted(HttpMethod.POST, deviceName, moduleName, nodeName);
60         final List<String> tags = List.of(deviceName + " " + moduleName);
61         final List<Parameter> parameters = new ArrayList<>(pathParams);
62         final RequestBody requestBody;
63         final List<String> nameElements = new ArrayList<>();
64         if (parentName != null) {
65             nameElements.add(parentName);
66         }
67         if (childNode.isConfiguration()) {
68             final String childNodeName = childNode.getQName().getLocalName();
69             nameElements.add(nodeName);
70             nameElements.add(childNodeName + discriminator);
71             final String childDefName = String.join("_", nameElements);
72             requestBody = createRequestBodyParameter(childDefName, childNodeName, childNode instanceof ListSchemaNode,
73                 summary, childNodeName);
74         } else {
75             nameElements.add(nodeName + discriminator);
76             final String defName = String.join("_", nameElements);
77             requestBody = createPostDataRequestBodyParameter(defName, nodeName);
78         }
79         final Map<String, ResponseObject> responses = Map.of(String.valueOf(Response.Status.CREATED.getStatusCode()),
80             buildResponse(Response.Status.CREATED.getReasonPhrase()));
81
82         return new Operation.Builder()
83             .tags(tags)
84             .parameters(parameters)
85             .requestBody(requestBody)
86             .responses(responses)
87             .description(description + POST_DESCRIPTION)
88             .summary(summary)
89             .build();
90     }
91
92     public static Operation buildGet(final DataSchemaNode node, final String parentName, final String moduleName,
93             final @NonNull String deviceName, final List<Parameter> pathParams, final boolean isConfig) {
94         final String nodeName = node.getQName().getLocalName();
95         final String defName = parentName + "_" + nodeName;
96         final String description = node.getDescription().orElse("");
97         final String summary = SUMMARY_TEMPLATE.formatted(HttpMethod.GET, deviceName, moduleName,
98                 node.getQName().getLocalName());
99         final List<String> tags = List.of(deviceName + " " + moduleName);
100         final List<Parameter> parameters = new ArrayList<>(pathParams);
101         parameters.add(buildQueryParameters(isConfig));
102         final boolean isList = node instanceof ListSchemaNode;
103         final ResponseObject response = createResponse(defName, nodeName, isList,
104             String.valueOf(OK.getStatusCode()), summary);
105         final Map<String, ResponseObject> responses = Map.of(String.valueOf(OK.getStatusCode()),
106             response);
107
108         return new Operation.Builder()
109             .tags(tags)
110             .parameters(parameters)
111             .responses(responses)
112             .description(description)
113             .summary(summary)
114             .build();
115     }
116
117     private static Parameter buildQueryParameters(final boolean isConfig) {
118         final List<String> cases = List.of("config", "nonconfig", "all");
119
120         return new Parameter.Builder()
121             .in("query")
122             .name("content")
123             .required(!isConfig)
124             .schema(new Schema.Builder().type("string").schemaEnum(cases).build())
125             .build();
126     }
127
128     public static Operation buildPut(final DataSchemaNode node, final String parentName, final String moduleName,
129             final @NonNull String deviceName, final List<Parameter> pathParams, final String fullName) {
130         final String nodeName = node.getQName().getLocalName();
131         final String summary = SUMMARY_TEMPLATE.formatted(HttpMethod.PUT, moduleName, deviceName, nodeName);
132         final List<String> tags = List.of(deviceName + " " + moduleName);
133         final List<Parameter> parameters = new ArrayList<>(pathParams);
134         final String defName = parentName + "_" + nodeName;
135         final boolean isList = node instanceof ListSchemaNode;
136         final RequestBody requestBody = createRequestBodyParameter(defName, fullName, isList, summary, nodeName);
137
138         final var created = new SimpleEntry<>(String.valueOf(Response.Status.CREATED.getStatusCode()),
139             buildResponse(Response.Status.CREATED.getReasonPhrase()));
140         final var updated = new SimpleEntry<>(String.valueOf(NO_CONTENT.getStatusCode()),
141             buildResponse("Updated"));
142
143         return new Operation.Builder()
144             .tags(tags)
145             .parameters(parameters)
146             .requestBody(requestBody)
147             .responses(Map.of(created.getKey(), created.getValue(), updated.getKey(), updated.getValue()))
148             .description(node.getDescription().orElse(""))
149             .summary(summary)
150             .build();
151     }
152
153     public static Operation buildPatch(final DataSchemaNode node, final String parentName, final String moduleName,
154             final @NonNull String deviceName, final List<Parameter> pathParams, final String fullName) {
155         final String nodeName = node.getQName().getLocalName();
156         final String summary = SUMMARY_TEMPLATE.formatted(HttpMethod.PATCH, moduleName, deviceName, nodeName);
157         final List<String> tags = List.of(deviceName + " " + moduleName);
158         final List<Parameter> parameters = new ArrayList<>(pathParams);
159         final String defName = parentName + "_" + nodeName;
160         final boolean isList = node instanceof ListSchemaNode;
161         final RequestBody requestBody = createRequestBodyParameter(defName, fullName, isList, summary, nodeName);
162
163         final SimpleEntry<String, ResponseObject> created = new SimpleEntry<>(String.valueOf(OK.getStatusCode()),
164             buildResponse(OK.getReasonPhrase()));
165         final SimpleEntry<String, ResponseObject> updated = new SimpleEntry<>(String.valueOf(NO_CONTENT
166             .getStatusCode()), buildResponse("Updated"));
167
168         return new Operation.Builder()
169             .tags(tags)
170             .parameters(parameters)
171             .requestBody(requestBody)
172             .responses(Map.of(created.getKey(), created.getValue(), updated.getKey(), updated.getValue()))
173             .description(node.getDescription().orElse(""))
174             .summary(summary)
175             .build();
176     }
177
178     public static Operation buildDelete(final DataSchemaNode node, final String moduleName,
179             final @NonNull String deviceName, final List<Parameter> pathParams) {
180         final String summary = SUMMARY_TEMPLATE.formatted(HttpMethod.DELETE, deviceName, moduleName,
181                 node.getQName().getLocalName());
182         final List<String> tags = List.of(deviceName + " " + moduleName);
183         final String description = node.getDescription().orElse("");
184         final List<Parameter> parameters = new ArrayList<>(pathParams);
185         final Map<String, ResponseObject> responses = Map.of(String.valueOf(NO_CONTENT.getStatusCode()),
186             buildResponse("Deleted"));
187
188         return new Operation.Builder()
189             .tags(tags)
190             .parameters(parameters)
191             .responses(responses)
192             .description(description)
193             .summary(summary)
194             .build();
195     }
196
197     public static Operation buildPostOperation(final OperationDefinition operDef, final String moduleName,
198             final @NonNull String deviceName, final String parentName, final DefinitionNames definitionNames,
199             final List<Parameter> parentPathParameters) {
200         final List<Parameter> parameters = new ArrayList<>(parentPathParameters);
201         final String operationName = operDef.getQName().getLocalName();
202         final String inputName = operationName + INPUT_SUFFIX;
203         final String summary = SUMMARY_TEMPLATE.formatted(HttpMethod.POST, deviceName, moduleName, operationName);
204
205         final InputSchemaNode input = operDef.getInput();
206         final OutputSchemaNode output = operDef.getOutput();
207         final RequestBody requestBody;
208         if (!input.getChildNodes().isEmpty()) {
209             final String discriminator = definitionNames.getDiscriminator(input);
210             final String clearDefName = parentName + "_" + operationName + INPUT_SUFFIX;
211             final String defName = clearDefName + discriminator;
212             requestBody = createRequestBodyParameter(defName, INPUT_KEY, false, summary, inputName);
213         } else {
214             final Map<String, Property> properties = Map.of(INPUT_KEY, new Property.Builder().type(OBJECT).build());
215             final Schema jsonSchema = new Schema.Builder()
216                 .type(OBJECT)
217                 .properties(properties)
218                 .build();
219             final MediaTypeObject jsonTypeValue = new MediaTypeObject.Builder()
220                 .schema(jsonSchema)
221                 .build();
222             final SimpleEntry<String, MediaTypeObject> jsonEntry = new SimpleEntry<>(MediaType.APPLICATION_JSON,
223                 jsonTypeValue);
224
225             final Xml xml = new Xml(INPUT_KEY, input.getQName().getNamespace().toString(), null);
226             final Schema xmlSchema = new Schema.Builder()
227                 .type(OBJECT)
228                 .xml(xml)
229                 .build();
230             final MediaTypeObject xmlTypeValue = new MediaTypeObject.Builder()
231                 .schema(xmlSchema)
232                 .build();
233             final SimpleEntry<String, MediaTypeObject> xmlEntry = new SimpleEntry<>(MediaType.APPLICATION_XML,
234                 xmlTypeValue);
235
236             requestBody = new RequestBody.Builder()
237                 .content(Map.of(jsonEntry.getKey(), jsonEntry.getValue(), xmlEntry.getKey(), xmlEntry.getValue()))
238                 .description(inputName)
239                 .build();
240         }
241         final String description = String.format("RPC %s success", operationName);
242
243         final Map<String, ResponseObject> responses;
244         if (!output.getChildNodes().isEmpty()) {
245             final String defName = parentName + "_" + operationName + OUTPUT_SUFFIX
246                 + definitionNames.getDiscriminator(output);
247             final Schema schema = new Schema.Builder()
248                 .ref(COMPONENTS_PREFIX + defName)
249                 .build();
250             responses = Map.of(String.valueOf(OK.getStatusCode()), buildResponse(description, schema));
251         } else {
252             responses = Map.of(String.valueOf(NO_CONTENT.getStatusCode()), buildResponse(description));
253         }
254         final String desc = operDef.getDescription().orElse("");
255         final List<String> tags = List.of(deviceName + " " + moduleName);
256         return new Operation.Builder()
257             .tags(tags)
258             .parameters(parameters)
259             .requestBody(requestBody)
260             .responses(responses)
261             .description(desc)
262             .summary(summary)
263             .build();
264     }
265
266     private static RequestBody createPostDataRequestBodyParameter(final String defName, final String name) {
267         final MediaTypeObject value = buildMediaTypeObject(defName);
268         final Map<String, MediaTypeObject> content = Map.of(MediaType.APPLICATION_JSON, value,
269             MediaType.APPLICATION_XML, value);
270         return new RequestBody.Builder()
271             .content(content)
272             .description(name)
273             .build();
274     }
275
276     private static RequestBody createRequestBodyParameter(final String defName, final String name,
277             final boolean isList, final String summary, final String description) {
278         final Map<String, MediaTypeObject> content = getStringMediaTypeObjectMap(defName, name, isList, summary);
279         return new RequestBody.Builder()
280             .content(content)
281             .description(description)
282             .build();
283     }
284
285     private static ResponseObject createResponse(final String defName, final String name,
286         final boolean isList, final String description, final String summary) {
287         final Map<String, MediaTypeObject> content = getStringMediaTypeObjectMap(defName, name, isList, summary);
288         return new ResponseObject.Builder()
289             .content(content)
290             .description(description)
291             .build();
292     }
293
294     private static Map<String, MediaTypeObject> getStringMediaTypeObjectMap(final String defName, final String name,
295             final boolean isList, final String summary) {
296         final Map<String, Property> properties;
297         if (isList) {
298             properties = Map.of(name, new Property.Builder()
299                 .type("array")
300                 .items(new Property.Builder()
301                     .type(OBJECT)
302                     .ref(COMPONENTS_PREFIX + defName)
303                     .build())
304                 .build());
305         } else {
306             properties = Map.of(name, new Property.Builder()
307                 .type(OBJECT)
308                 .ref(COMPONENTS_PREFIX + defName)
309                 .build());
310         }
311         final MediaTypeObject jsonSchema = new MediaTypeObject.Builder()
312             .schema(new Schema.Builder()
313                 .properties(properties)
314                 .build())
315             .build();
316         final Map<String, MediaTypeObject> content;
317         if (summary != null && summary.contains(HttpMethod.PATCH)) {
318             content = Map.of("application/yang-data+json", jsonSchema,
319                 "application/yang-data+xml", buildMediaTypeObject(defName));
320         } else {
321             content = Map.of(MediaType.APPLICATION_JSON, jsonSchema,
322                 MediaType.APPLICATION_XML, buildMediaTypeObject(defName));
323         }
324         return content;
325     }
326
327     private static Schema buildRefSchema(final String defName) {
328         return new Schema.Builder()
329             .ref(COMPONENTS_PREFIX + defName)
330             .build();
331     }
332
333     private static MediaTypeObject buildMediaTypeObject(final String defName) {
334         return new MediaTypeObject.Builder()
335             .schema(buildRefSchema(defName))
336             .build();
337     }
338
339     private static ResponseObject buildResponse(final String description) {
340         return new ResponseObject.Builder()
341             .description(description)
342             .build();
343     }
344
345     private static ResponseObject buildResponse(final String description, final Schema schema) {
346         final MediaTypeObject body = new MediaTypeObject.Builder()
347             .schema(schema)
348             .build();
349         final Map<String, MediaTypeObject> content = new HashMap<>();
350         for (final String mimeType : MIME_TYPES) {
351             content.put(mimeType, body);
352         }
353         return new ResponseObject.Builder()
354             .content(content)
355             .description(description)
356             .build();
357     }
358 }