Add callback model (#861)

* Add callback model (#372)

This adds a new `CodegenCallback` class, a list of which is now present in
`CodegenOperation`. `CodegenOperation` now also includes a
`isCallbackRequest` boolean since `fromCallback()` (the method added to
`DefaultCodegen` to process operations which contain OpenAPI callbacks)
uses CodegenOperation as the model for a callback request.

A `CodegenOperation` which represents a callback request will have a
`null` operation id.

A test is included for this new model.

* Generate callback request `operationId`

* Add license to `CodegenCallback`
This commit is contained in:
Jack O'Sullivan 2018-08-28 14:10:13 +01:00 committed by William Cheng
parent 8689227b3e
commit 5926ee5f1f
5 changed files with 320 additions and 10 deletions

View File

@ -0,0 +1,80 @@
/*
* Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech)
* Copyright 2018 SmartBear Software
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openapitools.codegen;
import java.util.*;
public class CodegenCallback {
public String name;
public boolean hasMore;
public List<Url> urls = new ArrayList<>();
public Map<String, Object> vendorExtensions = new HashMap<>();
public static class Url {
public String expression;
public boolean hasMore;
public List<CodegenOperation> requests = new ArrayList<>();
public Map<String, Object> vendorExtensions = new HashMap<>();
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Url that = (Url) o;
return Objects.equals(that.expression, expression) && Objects.equals(that.hasMore, hasMore) &&
Objects.equals(that.requests, requests) && Objects.equals(that.vendorExtensions, vendorExtensions);
}
@Override
public int hashCode() {
return Objects.hash(expression, hasMore, requests, vendorExtensions);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("CodegenCallback.Urls {\n");
sb.append(" expression: ").append(expression).append("\n");
requests.forEach(r -> sb.append(" ").append(r).append("\n"));
sb.append("}");
return sb.toString();
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CodegenCallback that = (CodegenCallback) o;
return Objects.equals(that.name, name) && Objects.equals(that.hasMore, hasMore) &&
Objects.equals(that.urls, urls) && Objects.equals(that.vendorExtensions, vendorExtensions);
}
@Override
public int hashCode() {
return Objects.hash(name, hasMore, urls, vendorExtensions);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("CodegenCallback {\n");
sb.append(" name: ").append(name).append("\n");
urls.forEach(u -> sb.append(" ").append(u).append("\n"));
sb.append("}");
return sb.toString();
}
}

View File

@ -36,7 +36,7 @@ public class CodegenOperation {
isListContainer, isMultipart, hasMore = true,
isResponseBinary = false, isResponseFile = false, hasReference = false,
isRestfulIndex, isRestfulShow, isRestfulCreate, isRestfulUpdate, isRestfulDestroy,
isRestful, isDeprecated;
isRestful, isDeprecated, isCallbackRequest;
public String path, operationId, returnType, httpMethod, returnBaseType,
returnContainer, summary, unescapedNotes, notes, baseName, defaultResponse;
public CodegenDiscriminator discriminator;
@ -54,6 +54,7 @@ public class CodegenOperation {
public List<CodegenSecurity> authMethods;
public List<Tag> tags;
public List<CodegenResponse> responses = new ArrayList<CodegenResponse>();
public List<CodegenCallback> callbacks = new ArrayList<>();
public Set<String> imports = new HashSet<String>();
public List<Map<String, String>> examples;
public List<Map<String, String>> requestBodyExamples;
@ -293,6 +294,8 @@ public class CodegenOperation {
return false;
if (isDeprecated != that.isDeprecated)
return false;
if (isCallbackRequest != that.isCallbackRequest)
return false;
if (path != null ? !path.equals(that.path) : that.path != null)
return false;
if (operationId != null ? !operationId.equals(that.operationId) : that.operationId != null)
@ -347,6 +350,8 @@ public class CodegenOperation {
return false;
if (responses != null ? !responses.equals(that.responses) : that.responses != null)
return false;
if (callbacks != null ? !callbacks.equals(that.callbacks) : that.callbacks != null)
return false;
if (imports != null ? !imports.equals(that.imports) : that.imports != null)
return false;
if (examples != null ? !examples.equals(that.examples) : that.examples != null)
@ -386,6 +391,7 @@ public class CodegenOperation {
result = 31 * result + (isResponseFile ? 13:31);
result = 31 * result + (hasReference ? 13:31);
result = 31 * result + (isDeprecated ? 13:31);
result = 31 * result + (isCallbackRequest ? 13:31);
result = 31 * result + (path != null ? path.hashCode() : 0);
result = 31 * result + (operationId != null ? operationId.hashCode() : 0);
result = 31 * result + (returnType != null ? returnType.hashCode() : 0);
@ -413,6 +419,7 @@ public class CodegenOperation {
result = 31 * result + (authMethods != null ? authMethods.hashCode() : 0);
result = 31 * result + (tags != null ? tags.hashCode() : 0);
result = 31 * result + (responses != null ? responses.hashCode() : 0);
result = 31 * result + (callbacks != null ? callbacks.hashCode() : 0);
result = 31 * result + (imports != null ? imports.hashCode() : 0);
result = 31 * result + (examples != null ? examples.hashCode() : 0);
result = 31 * result + (externalDocs != null ? externalDocs.hashCode() : 0);

View File

@ -24,6 +24,7 @@ import com.samskivert.mustache.Mustache.Compiler;
import io.swagger.v3.core.util.Json;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.callbacks.Callback;
import io.swagger.v3.oas.models.examples.Example;
import io.swagger.v3.oas.models.headers.Header;
import io.swagger.v3.oas.models.media.ArraySchema;
@ -50,6 +51,7 @@ import io.swagger.v3.parser.util.SchemaTypeUtil;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.openapitools.codegen.CodegenDiscriminator.MappedModel;
import org.openapitools.codegen.examples.ExampleGenerator;
import org.openapitools.codegen.serializer.SerializerUtils;
@ -75,6 +77,7 @@ import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import java.util.stream.Collectors;
public class DefaultCodegen implements CodegenConfig {
@ -2232,14 +2235,17 @@ public class DefaultCodegen implements CodegenConfig {
Map<String, Schema> schemas,
OpenAPI openAPI) {
LOGGER.debug("fromOperation => operation: " + operation);
if (operation == null)
throw new RuntimeException("operation cannot be null in fromOperation");
CodegenOperation op = CodegenModelFactory.newInstance(CodegenModelType.OPERATION);
Set<String> imports = new HashSet<String>();
if (operation.getExtensions() != null && !operation.getExtensions().isEmpty()) {
op.vendorExtensions.putAll(operation.getExtensions());
}
if (operation == null)
throw new RuntimeException("operation cannot be null in fromOperation");
Object isCallbackRequest = op.vendorExtensions.remove("x-callback-request");
op.isCallbackRequest = Boolean.TRUE.equals(isCallbackRequest);
}
// store the original operationId for plug-in
op.operationIdOriginal = operation.getOperationId();
@ -2253,6 +2259,7 @@ public class DefaultCodegen implements CodegenConfig {
}
}
operationId = removeNonNameElementToCamelCase(operationId);
op.path = path;
op.operationId = toOperationId(operationId);
op.summary = escapeText(operation.getSummary());
@ -2344,6 +2351,15 @@ public class DefaultCodegen implements CodegenConfig {
}
}
if (operation.getCallbacks() != null && !operation.getCallbacks().isEmpty()) {
operation.getCallbacks().forEach((name, callback) -> {
CodegenCallback c = fromCallback(name, callback, schemas, openAPI);
c.hasMore = true;
op.callbacks.add(c);
});
op.callbacks.get(op.callbacks.size() - 1).hasMore = false;
}
List<Parameter> parameters = operation.getParameters();
List<CodegenParameter> allParams = new ArrayList<CodegenParameter>();
List<CodegenParameter> bodyParams = new ArrayList<CodegenParameter>();
@ -2621,6 +2637,79 @@ public class DefaultCodegen implements CodegenConfig {
return r;
}
/**
* Convert OAS Callback object to Codegen Callback object
*
* @param name callback name
* @param callback OAS Callback object
* @param schemas a map of OAS models
* @param openAPI a OAS object representing the spec
* @return Codegen Response object
*/
public CodegenCallback fromCallback(String name, Callback callback, Map<String, Schema> schemas, OpenAPI openAPI) {
CodegenCallback c = new CodegenCallback();
c.name = name;
if (callback.getExtensions() != null && !callback.getExtensions().isEmpty()) {
c.vendorExtensions.putAll(callback.getExtensions());
}
callback.forEach((expression, pi) -> {
CodegenCallback.Url u = new CodegenCallback.Url();
u.expression = expression;
u.hasMore = true;
if (pi.getExtensions() != null && !pi.getExtensions().isEmpty()) {
u.vendorExtensions.putAll(pi.getExtensions());
}
Stream.of(
Pair.of("get", pi.getGet()),
Pair.of("head", pi.getHead()),
Pair.of("put", pi.getPut()),
Pair.of("post", pi.getPost()),
Pair.of("delete", pi.getDelete()),
Pair.of("patch", pi.getPatch()),
Pair.of("options", pi.getOptions()))
.filter(p -> p.getValue() != null)
.forEach(p -> {
String method = p.getKey();
Operation op = p.getValue();
boolean genId = op.getOperationId() == null;
if (genId) {
op.setOperationId(getOrGenerateOperationId(op, c.name+"_"+expression.replaceAll("\\{\\$.*}", ""), method));
}
if (op.getExtensions() == null) {
op.setExtensions(new HashMap<>());
}
// This extension will be removed later by `fromOperation()` as it is only needed here to
// distinguish between normal operations and callback requests
op.getExtensions().put("x-callback-request", true);
CodegenOperation co = fromOperation(expression, method, op, schemas, openAPI);
if (genId) {
co.operationIdOriginal = null;
// legacy (see `fromOperation()`)
co.nickname = co.operationId;
}
u.requests.add(co);
});
if (!u.requests.isEmpty()) {
u.requests.get(u.requests.size() - 1).hasMore = false;
}
c.urls.add(u);
});
if (!c.urls.isEmpty()) {
c.urls.get(c.urls.size() - 1).hasMore = false;
}
return c;
}
/**
* Convert OAS Parameter object to Codegen Parameter object
*

View File

@ -38,12 +38,7 @@ import org.testng.Assert;
import org.testng.annotations.Test;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.*;
import java.util.stream.Collectors;
public class DefaultCodegenTest {
@ -407,6 +402,60 @@ public class DefaultCodegenTest {
verifyPersonDiscriminator(personModel.discriminator);
}
@Test
public void testCallbacks() {
final OpenAPI openAPI = new OpenAPIParser().readLocation("src/test/resources/3_0/callbacks.yaml", null, new ParseOptions()).getOpenAPI();
final CodegenConfig codegen = new DefaultCodegen();
final String path = "/streams";
Operation subscriptionOperation = openAPI.getPaths().get("/streams").getPost();
CodegenOperation op = codegen.fromOperation(path, "post", subscriptionOperation, openAPI.getComponents().getSchemas(), openAPI);
Assert.assertFalse(op.isCallbackRequest);
Assert.assertNotNull(op.operationId);
Assert.assertEquals(op.callbacks.size(), 2);
CodegenCallback cbB = op.callbacks.get(1);
Assert.assertEquals(cbB.name, "dummy");
Assert.assertFalse(cbB.hasMore);
Assert.assertEquals(cbB.urls.size(), 0);
CodegenCallback cbA = op.callbacks.get(0);
Assert.assertEquals(cbA.name, "onData");
Assert.assertTrue(cbA.hasMore);
Assert.assertEquals(cbA.urls.size(), 2);
CodegenCallback.Url urlB = cbA.urls.get(1);
Assert.assertEquals(urlB.expression, "{$request.query.callbackUrl}/test");
Assert.assertFalse(urlB.hasMore);
Assert.assertEquals(urlB.requests.size(), 0);
CodegenCallback.Url urlA = cbA.urls.get(0);
Assert.assertEquals(urlA.expression, "{$request.query.callbackUrl}/data");
Assert.assertTrue(urlA.hasMore);
Assert.assertEquals(urlA.requests.size(), 2);
urlA.requests.forEach(req -> {
Assert.assertTrue(req.isCallbackRequest);
Assert.assertNotNull(req.bodyParam);
Assert.assertEquals(req.responses.size(), 2);
switch (req.httpMethod.toLowerCase(Locale.getDefault())) {
case "post":
Assert.assertEquals(req.operationId, "onDataDataPost");
Assert.assertEquals(req.bodyParam.dataType, "NewNotificationData");
break;
case "delete":
Assert.assertEquals(req.operationId, "onDataDataDelete");
Assert.assertEquals(req.bodyParam.dataType, "DeleteNotificationData");
break;
default:
Assert.fail(String.format(Locale.getDefault(), "invalid callback request http method '%s'", req.httpMethod));
}
});
}
private void verifyPersonDiscriminator(CodegenDiscriminator discriminator) {
CodegenDiscriminator test = new CodegenDiscriminator();
test.setPropertyName("$_type");

View File

@ -0,0 +1,85 @@
openapi: 3.0.0
info:
title: Callback Example
version: 1.0.0
paths:
/streams:
post:
description: subscribes a client to receive out-of-band data
parameters:
- name: callbackUrl
in: query
required: true
description: |
the location where data will be sent. Must be network accessible
by the source server
schema:
type: string
format: uri
example: https://tonys-server.com
responses:
'201':
description: subscription successfully created
content:
application/json:
schema:
description: subscription information
required:
- subscriptionId
properties:
subscriptionId:
description: this unique identifier allows management of the subscription
type: string
example: 2531329f-fb09-4ef7-887e-84e648214436
callbacks:
onData:
'{$request.query.callbackUrl}/data':
post:
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/NewNotificationData'
responses:
'202':
description: |
Your server implementation should return this HTTP status code
if the data was received successfully
'204':
description: |
Your server should return this HTTP status code if no longer interested
in further updates
delete:
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/DeleteNotificationData'
responses:
'202':
description: |
Your server implementation should return this HTTP status code
if the data was received successfully
'204':
description: |
Your server should return this HTTP status code if no longer interested
in further updates
'{$request.query.callbackUrl}/test': {}
dummy: {}
components:
schemas:
NewNotificationData:
description: subscription payload
properties:
timestamp:
type: string
format: date-time
userData:
type: string
DeleteNotificationData:
description: subscription payload
properties:
timestamp:
type: string
format: date-time