diff --git a/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/EndpointsToolAction.java b/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/EndpointsToolAction.java index 2380a822..49e775f4 100644 --- a/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/EndpointsToolAction.java +++ b/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/EndpointsToolAction.java @@ -214,10 +214,6 @@ protected String getBuildSystem(Option buildSystemOption) { return getOptionOrDefault(buildSystemOption, DEFAULT_BUILD_SYSTEM); } - protected String getFormat(Option formatOption) { - return getOptionOrDefault(formatOption, DEFAULT_FORMAT); - } - protected boolean getDebug(Option debugOption) { return debugOption.getValue() != null; } @@ -406,13 +402,6 @@ public void setHelpDisplayNeeded(boolean helpDisplayNeeded) { this.helpDisplayNeeded = helpDisplayNeeded; } - /** - * Return the example string which will be displayed in the usage. - */ - public String getExampleString() { - return exampleString; - } - /** * Set the example string which will be displayed in the usage. */ @@ -467,6 +456,17 @@ public static EndpointsOption makeVisibleNonFlagOption( return new EndpointsOption(shortName, longName, false, true, placeHolderValue, description); } + public static EndpointsOption makeVisibleFlagOption( + @Nullable String longName, + @Nullable String description) { + return new EndpointsOption(null, longName, true, true, null, description) { + @Override + public void apply() { + getValues().add("true"); + } + }; + } + public static EndpointsOption makeInvisibleFlagOption( @Nullable String shortName, @Nullable String longName) { diff --git a/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/GetDiscoveryDocAction.java b/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/GetDiscoveryDocAction.java index 88bc0fe2..83b84e30 100644 --- a/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/GetDiscoveryDocAction.java +++ b/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/GetDiscoveryDocAction.java @@ -60,13 +60,12 @@ public class GetDiscoveryDocAction extends EndpointsToolAction { private Option classPathOption = makeClassPathOption(); private Option outputOption = makeOutputOption(); private Option warOption = makeWarOption(); - private Option debugOption = makeDebugOption(); private Option hostnameOption = makeHostnameOption(); private Option basePathOption = makeBasePathOption(); public GetDiscoveryDocAction() { super(NAME); - setOptions(Arrays.asList(classPathOption, outputOption, warOption, debugOption, hostnameOption, + setOptions(Arrays.asList(classPathOption, outputOption, warOption, hostnameOption, basePathOption)); setShortDescription("Generates discovery documents"); setExampleString(" get-discovery-doc " diff --git a/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/GetOpenApiDocAction.java b/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/GetOpenApiDocAction.java index 4ea985f4..71ca24a1 100644 --- a/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/GetOpenApiDocAction.java +++ b/endpoints-framework-tools/src/main/java/com/google/api/server/spi/tools/GetOpenApiDocAction.java @@ -15,6 +15,7 @@ */ package com.google.api.server.spi.tools; +import static com.google.api.server.spi.tools.EndpointsToolAction.EndpointsOption.makeVisibleFlagOption; import static java.nio.charset.StandardCharsets.UTF_8; import com.google.api.server.spi.ServiceContext; @@ -52,6 +53,20 @@ public class GetOpenApiDocAction extends EndpointsToolAction { private Option warOption = makeWarOption(); private Option hostnameOption = makeHostnameOption(); private Option basePathOption = makeBasePathOption(); + private Option titleOption = makeTitleOption(); + private Option descriptionOption = makeDescriptionOption(); + private Option addGoogleJsonErrorAsDefaultResponseOption = makeVisibleFlagOption( + "addGoogleJsonErrorAsDefaultResponse", "Add GoogleJsonError as default response" + ); + private Option addErrorCodesForServiceExceptionsOption = makeVisibleFlagOption( + "addErrorCodesForServiceExceptions", "Add GoogleJsonError for codes in ServiceExceptions" + ); + private Option extractCommonParametersAsRefsOption = makeVisibleFlagOption( + "extractCommonParametersAsRefs", "Extract common parameters as refs at specification level" + ); + private Option combineCommonParametersInSamePathOption = makeVisibleFlagOption( + "combineCommonParametersInSamePath", "Combine common parameters in same path" + ); public GetOpenApiDocAction() { this(NAME, true); @@ -60,13 +75,36 @@ public GetOpenApiDocAction() { protected GetOpenApiDocAction(String alias, boolean displayHelp) { super(alias); setOptions( - Arrays.asList(classPathOption, outputOption, warOption, hostnameOption, basePathOption)); + Arrays.asList(classPathOption, outputOption, warOption, hostnameOption, basePathOption, + titleOption, descriptionOption, + addGoogleJsonErrorAsDefaultResponseOption, addErrorCodesForServiceExceptionsOption, + extractCommonParametersAsRefsOption, combineCommonParametersInSamePathOption)); setShortDescription("Generates an OpenAPI document"); setExampleString(" " + getNames()[0] + " com.google.devrel.samples.ttt.spi.BoardV1 com.google.devrel.samples.ttt.spi.ScoresV1"); setHelpDisplayNeeded(displayHelp); } + private static Option makeTitleOption() { + return EndpointsOption.makeVisibleNonFlagOption( + "t", + "title", + "TITLE", + "Sets the title for the generated document. Default is the app's host."); + } + + private static Option makeDescriptionOption() { + return EndpointsOption.makeVisibleNonFlagOption( + "d", + "description", + "DESCRIPTION", + "Sets the description for the generated document. Is empty by default."); + } + + private static boolean getBooleanOptionValue(Option option) { + return option.getValue() != null; + } + @Override public String getUsageString() { return getNames()[0] + " ..."; @@ -81,7 +119,14 @@ public boolean execute() throws ClassNotFoundException, IOException, ApiConfigEx } genOpenApiDoc(computeClassPath(warPath, getClassPath(classPathOption)), getOpenApiOutputPath(outputOption), getHostname(hostnameOption, warPath), - getBasePath(basePathOption), serviceClassNames, true); + getBasePath(basePathOption), + getOptionOrDefault(titleOption, null), + getOptionOrDefault(descriptionOption, null), + getBooleanOptionValue(addGoogleJsonErrorAsDefaultResponseOption), + getBooleanOptionValue(addErrorCodesForServiceExceptionsOption), + getBooleanOptionValue(extractCommonParametersAsRefsOption), + getBooleanOptionValue(combineCommonParametersInSamePathOption), + serviceClassNames, true); return true; } @@ -98,6 +143,9 @@ public boolean execute() throws ClassNotFoundException, IOException, ApiConfigEx */ public String genOpenApiDoc( URL[] classPath, String outputFilePath, String hostname, String basePath, + String title, String description, + boolean addGoogleJsonErrorAsDefaultResponse, boolean addErrorCodesForServiceExceptionsOption, + boolean extractCommonParametersAsRefsOption, boolean combineCommonParametersInSamePathOption, List serviceClassNames, boolean outputToDisk) throws ClassNotFoundException, IOException, ApiConfigException { File outputFile = new File(outputFilePath); @@ -120,7 +168,13 @@ public String genOpenApiDoc( SwaggerGenerator generator = new SwaggerGenerator(); SwaggerContext swaggerContext = new SwaggerContext() .setHostname(hostname) - .setBasePath(basePath); + .setBasePath(basePath) + .setTitle(title) + .setDescription(description) + .setAddErrorCodesForServiceExceptions(addGoogleJsonErrorAsDefaultResponse) + .setAddErrorCodesForServiceExceptions(addErrorCodesForServiceExceptionsOption) + .setExtractCommonParametersAsRefs(extractCommonParametersAsRefsOption) + .setCombineCommonParametersInSamePath(combineCommonParametersInSamePathOption); Swagger swagger = generator.writeSwagger(apiConfigs, swaggerContext); String swaggerStr = Json.mapper().writer(new EndpointsPrettyPrinter()) .writeValueAsString(swagger); diff --git a/endpoints-framework-tools/src/test/java/com/google/api/server/spi/tools/GetOpenApiDocActionTest.java b/endpoints-framework-tools/src/test/java/com/google/api/server/spi/tools/GetOpenApiDocActionTest.java index aa37e6b1..d55e9089 100644 --- a/endpoints-framework-tools/src/test/java/com/google/api/server/spi/tools/GetOpenApiDocActionTest.java +++ b/endpoints-framework-tools/src/test/java/com/google/api/server/spi/tools/GetOpenApiDocActionTest.java @@ -45,6 +45,8 @@ public class GetOpenApiDocActionTest extends EndpointsToolTest { private String basePath; private List serviceClassNames; private boolean outputToDisk; + private boolean addGoogleJsonErrorAsDefaultResponse; + private boolean addErrorCodesForServiceExceptionsOption; @Override protected void addTestAction(Map actions) { @@ -53,12 +55,21 @@ protected void addTestAction(Map actions) { @Override public String genOpenApiDoc( URL[] classPath, String outputFilePath, String hostname, String basePath, + String title, String description, + boolean addGoogleJsonErrorAsDefaultResponse, + boolean addErrorCodesForServiceExceptionsOption, + boolean extractCommonParametersAsRefsOption, + boolean combineCommonParametersInSamePathOption, List serviceClassNames, boolean outputToDisk) { GetOpenApiDocActionTest.this.classPath = classPath; GetOpenApiDocActionTest.this.outputFilePath = outputFilePath; GetOpenApiDocActionTest.this.basePath = basePath; GetOpenApiDocActionTest.this.serviceClassNames = serviceClassNames; GetOpenApiDocActionTest.this.outputToDisk = outputToDisk; + GetOpenApiDocActionTest.this.addGoogleJsonErrorAsDefaultResponse + = addGoogleJsonErrorAsDefaultResponse; + GetOpenApiDocActionTest.this.addErrorCodesForServiceExceptionsOption + = addErrorCodesForServiceExceptionsOption; return null; } @@ -84,7 +95,9 @@ public void setUp() throws Exception { public void testGetOpenApiDoc() throws Exception { tool.execute( new String[]{GetOpenApiDocAction.NAME, option(EndpointsToolAction.OPTION_CLASS_PATH_SHORT), - "classPath", option(EndpointsToolAction.OPTION_OUTPUT_DIR_SHORT), "outputDir", "MyService", + "classPath", option(EndpointsToolAction.OPTION_OUTPUT_DIR_SHORT), "outputDir", + option("addGoogleJsonErrorAsDefaultResponse", false), + "MyService", "MyService2"}); assertFalse(usagePrinted); assertThat(Lists.newArrayList(classPath)) @@ -95,6 +108,8 @@ public void testGetOpenApiDoc() throws Exception { .toURL()); assertEquals("outputDir", outputFilePath); assertEquals(EndpointsToolAction.DEFAULT_BASE_PATH, basePath); + assertTrue(addGoogleJsonErrorAsDefaultResponse); + assertFalse(addErrorCodesForServiceExceptionsOption); assertStringsEqual(Arrays.asList("MyService", "MyService2"), serviceClassNames); assertTrue(outputToDisk); } diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/ApiMethodConfig.java b/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/ApiMethodConfig.java index a42fcf09..191121fc 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/ApiMethodConfig.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/config/model/ApiMethodConfig.java @@ -15,17 +15,28 @@ */ package com.google.api.server.spi.config.model; -import com.google.api.server.spi.Constant; import com.google.api.server.spi.EndpointMethod; +import com.google.api.server.spi.ServiceException; import com.google.api.server.spi.TypeLoader; import com.google.api.server.spi.config.AuthLevel; import com.google.api.server.spi.config.Authenticator; import com.google.api.server.spi.config.PeerAuthenticator; import com.google.api.server.spi.config.model.ApiParameterConfig.Classification; import com.google.api.server.spi.config.scope.AuthScopeExpression; +import com.google.api.server.spi.response.BadRequestException; +import com.google.api.server.spi.response.ConflictException; +import com.google.api.server.spi.response.ErrorMap; +import com.google.api.server.spi.response.ForbiddenException; +import com.google.api.server.spi.response.InternalServerErrorException; +import com.google.api.server.spi.response.NotFoundException; +import com.google.api.server.spi.response.ServiceUnavailableException; +import com.google.api.server.spi.response.UnauthorizedException; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; import com.google.common.reflect.TypeToken; + import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.ArrayList; @@ -45,6 +56,17 @@ */ public class ApiMethodConfig { + private static final Map, Integer> + KNOWN_EXCEPTION_CODES = ImmutableMap., Integer>builder() + .put(BadRequestException.class, BadRequestException.CODE) + .put(ForbiddenException.class, ForbiddenException.CODE) + .put(ServiceUnavailableException.class, ServiceUnavailableException.CODE) + .put(UnauthorizedException.class, UnauthorizedException.CODE) + .put(NotFoundException.class, NotFoundException.CODE) + .put(ConflictException.class, ConflictException.CODE) + .put(InternalServerErrorException.class, InternalServerErrorException.CODE) + .build(); + private enum RestMethod { LIST("list", "GET") { @Override @@ -159,6 +181,7 @@ public String guessResourceName( private boolean deprecated = false; private Boolean apiKeyRequired; private TypeToken returnType; + private Class[] exceptionTypes; private List metricCosts; private final TypeLoader typeLoader; @@ -231,6 +254,7 @@ protected void setDefaults(EndpointMethod endpointMethod, TypeLoader typeLoader, ignored = false; apiKeyRequired = null; returnType = endpointMethod.getReturnType(); + exceptionTypes = endpointMethod.getMethod().getExceptionTypes(); metricCosts = ImmutableList.of(); } @@ -519,6 +543,24 @@ public TypeToken getReturnType() { return returnType; } + public Map getErrorCodesAndDescriptions() { + ErrorMap errorMap = new ErrorMap(); + Map descriptionByCode = Maps.newLinkedHashMap(); + for (Class exceptionType : exceptionTypes) { + //TODO support custom exception types by either: + // - introspecting the CODE static field (convention) + // - introducing a new annotation for Exceptions + Integer code = KNOWN_EXCEPTION_CODES.get(exceptionType); + if (code != null) { + String reason = errorMap.getReason(code); + if (reason != null) { + descriptionByCode.put(code, reason); + } + } + } + return descriptionByCode; + } + /** * Returns whether or not the method has a resource (is non-void) in the response. */ diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/response/BadRequestException.java b/endpoints-framework/src/main/java/com/google/api/server/spi/response/BadRequestException.java index 378826cb..3686a4ee 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/response/BadRequestException.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/response/BadRequestException.java @@ -22,7 +22,7 @@ */ public class BadRequestException extends ServiceException { - private static final int CODE = 400; + public static final int CODE = 400; public BadRequestException(String message) { super(CODE, message); diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/response/ConflictException.java b/endpoints-framework/src/main/java/com/google/api/server/spi/response/ConflictException.java index e85523bf..e3722f7c 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/response/ConflictException.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/response/ConflictException.java @@ -22,7 +22,7 @@ */ public class ConflictException extends ServiceException { - private static final int CODE = 409; + public static final int CODE = 409; public ConflictException(String message) { super(CODE, message); diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/response/ForbiddenException.java b/endpoints-framework/src/main/java/com/google/api/server/spi/response/ForbiddenException.java index 1ed9e4e4..db0b7df1 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/response/ForbiddenException.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/response/ForbiddenException.java @@ -22,7 +22,7 @@ */ public class ForbiddenException extends ServiceException { - private static final int CODE = 403; + public static final int CODE = 403; public ForbiddenException(String message) { super(CODE, message); diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/response/InternalServerErrorException.java b/endpoints-framework/src/main/java/com/google/api/server/spi/response/InternalServerErrorException.java index 12ba1ac2..27cb065f 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/response/InternalServerErrorException.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/response/InternalServerErrorException.java @@ -22,7 +22,7 @@ */ public class InternalServerErrorException extends ServiceException { - private static final int CODE = 500; + public static final int CODE = 500; public InternalServerErrorException(String message) { super(CODE, message); diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/response/NotFoundException.java b/endpoints-framework/src/main/java/com/google/api/server/spi/response/NotFoundException.java index e620bd7d..b7717b61 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/response/NotFoundException.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/response/NotFoundException.java @@ -22,7 +22,7 @@ */ public class NotFoundException extends ServiceException { - private static final int CODE = 404; + public static final int CODE = 404; public NotFoundException(String message) { super(CODE, message); diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/response/ServiceUnavailableException.java b/endpoints-framework/src/main/java/com/google/api/server/spi/response/ServiceUnavailableException.java index ed6decf9..dd7e19b3 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/response/ServiceUnavailableException.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/response/ServiceUnavailableException.java @@ -22,7 +22,7 @@ */ public class ServiceUnavailableException extends ServiceException { - private static final int CODE = 503; + public static final int CODE = 503; public ServiceUnavailableException(String message) { super(CODE, message); diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/response/UnauthorizedException.java b/endpoints-framework/src/main/java/com/google/api/server/spi/response/UnauthorizedException.java index 97e4c82e..4016c014 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/response/UnauthorizedException.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/response/UnauthorizedException.java @@ -29,7 +29,7 @@ public class UnauthorizedException extends ServiceException { public static final String AUTH_SCHEME_BEARER = "Bearer"; private static final Map GOOGLE_REALM = ImmutableMap.of("realm", "\"https://accounts.google.com/\""); - private static final int CODE = 401; + public static final int CODE = 401; private final String authScheme; private final Map params; diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/swagger/SwaggerGenerator.java b/endpoints-framework/src/main/java/com/google/api/server/spi/swagger/SwaggerGenerator.java index a1f5f878..eec71400 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/swagger/SwaggerGenerator.java +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/swagger/SwaggerGenerator.java @@ -15,6 +15,8 @@ */ package com.google.api.server.spi.swagger; +import com.google.api.client.googleapis.json.GoogleJsonError; +import com.google.api.client.googleapis.json.GoogleJsonErrorContainer; import com.google.api.server.spi.EndpointMethod; import com.google.api.server.spi.Strings; import com.google.api.server.spi.TypeLoader; @@ -43,30 +45,21 @@ import com.google.common.base.Converter; import com.google.common.base.Function; import com.google.common.base.Joiner; +import com.google.common.base.Suppliers; import com.google.common.collect.FluentIterable; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.HashMultiset; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.ImmutableSortedMap.Builder; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; +import com.google.common.collect.Multiset; +import com.google.common.collect.Multisets; import com.google.common.reflect.TypeToken; - -import java.lang.reflect.Type; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Objects; -import java.util.TreeMap; -import java.util.stream.Stream; - import io.swagger.models.ExternalDocs; import io.swagger.models.Info; import io.swagger.models.Model; @@ -84,9 +77,10 @@ import io.swagger.models.auth.SecuritySchemeDefinition; import io.swagger.models.parameters.AbstractSerializableParameter; import io.swagger.models.parameters.BodyParameter; +import io.swagger.models.parameters.Parameter; import io.swagger.models.parameters.PathParameter; import io.swagger.models.parameters.QueryParameter; -import io.swagger.models.parameters.SerializableParameter; +import io.swagger.models.parameters.RefParameter; import io.swagger.models.properties.ArrayProperty; import io.swagger.models.properties.BooleanProperty; import io.swagger.models.properties.ByteArrayProperty; @@ -103,6 +97,24 @@ import io.swagger.models.properties.RefProperty; import io.swagger.models.properties.StringProperty; +import java.lang.reflect.Type; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; +import java.util.TreeMap; +import java.util.function.Supplier; +import java.util.stream.Stream; + /** * Generates a {@link Swagger} object representing a set of {@link ApiConfig} objects. */ @@ -180,7 +192,11 @@ public class SwaggerGenerator { //using an object property with empty properties is semantically identical private static final ObjectProperty FREE_FORM_PROPERTY = new ObjectProperty() .properties(Collections.emptyMap()); - + //some well-known types should be inlined to avoid polluting model namespace + private static final ImmutableSet INLINED_MODEL_NAMES = ImmutableSet.of( + GoogleJsonError.class.getSimpleName(), GoogleJsonError.ErrorInfo.class.getSimpleName() + ); + private static final Function CONFIG_TO_ROOTLESS_KEY = new Function() { @Override @@ -215,10 +231,13 @@ private Swagger writeSwagger(Iterable configs, SwaggerContext context .host(context.hostname) .basePath(context.basePath) .info(new Info() - .title(context.hostname) - .version(context.docVersion)); + .title(context.title != null ? context.title : context.hostname) + .description(context.description) + .version(context.docVersion) + //TODO contact, license, termsOfService could be configured + ); for (ApiKey apiKey : configsByKey.keySet()) { - writeApi(apiKey, configsByKey.get(apiKey), swagger, genCtx); + writeApi(apiKey, configsByKey.get(apiKey), swagger, context, genCtx); } //reorder paths by string order to have consistent output Builder builder = ImmutableSortedMap.naturalOrder(); @@ -226,10 +245,85 @@ private Swagger writeSwagger(Iterable configs, SwaggerContext context builder.put(pathEntry); } swagger.paths(builder.build()); + combineCommonParameters(swagger, context); writeQuotaDefinitions(swagger, genCtx); return swagger; } + private void combineCommonParameters(Swagger swagger, SwaggerContext context) { + if (!context.extractCommonParametersAsRefs && !context.combineCommonParametersInSamePath) { + return; + } + + Map> paramNameCounter = new LinkedHashMap<>(); + Multimap specLevelParameters = HashMultimap.create(); + Map> pathLevelParameters = Maps.newHashMap(); + + //collect parameters on all operations + swagger.getPaths().values().forEach(path -> { + Multimap parameters = HashMultimap.create(); + path.getOperations().forEach(operation -> { + operation.getParameters().forEach(parameter -> { + Multiset counter = Optional + .ofNullable(paramNameCounter.get(parameter.getName())) + .orElse(HashMultiset.create()); + counter.add(parameter); + paramNameCounter.put(parameter.getName(), counter); + specLevelParameters.put(parameter, path); + parameters.put(parameter, operation); + }); + pathLevelParameters.put(path, parameters); + }); + }); + + if (context.extractCommonParametersAsRefs) { + //combine common spec-level params (only if more than one path) + specLevelParameters.asMap().forEach((parameter, paths) -> { + //parameters used in more than one path are replaced + if (paths.size() > 1) { + //if multiple params are named the same, only replace the one with the most occurrences + //TODO add param name suffix depending on "in" and "required" values to deduplicate + Multiset paramCounter = Multisets + .copyHighestCountFirst(paramNameCounter.get(parameter.getName())); + if (paramCounter.iterator().next().equals(parameter)) { + swagger.addParameter(parameter.getName(), parameter); + swagger.getPaths().values().forEach(path -> path.getOperations().forEach(operation -> { + List opParameters = operation.getParameters(); + if (opParameters.contains(parameter)) { + opParameters.add(new RefParameter(parameter.getName()) + .asDefault(parameter.getName())); + opParameters.remove(parameter); + } + })); + pathLevelParameters.values().forEach(pathParameters -> pathParameters.removeAll(parameter)); + } + } + }); + } + + if (context.combineCommonParametersInSamePath) { + //combine remaining common path-level params + pathLevelParameters.forEach((path, parameterMap) -> { + parameterMap.asMap().forEach((parameter, operations) -> { + //if parameter is used in all operations, replace it + if (operations.size() == path.getOperations().size()) { + path.addParameter(parameter); + operations.forEach(operation -> operation.getParameters().remove(parameter)); + } + }); + //nullify empty parameters at path level + //TODO deserialization with standard Swagger lib recreates an empty list on null value, + // causing comparisons in tests to fail. But could save some size on the final document. + /*path.getOperations().forEach(operation -> { + List parameters = operation.getParameters(); + if (parameters != null && parameters.isEmpty()) { + operation.setParameters(null); + } + });*/ + }); + } + } + private void writeQuotaDefinitions(Swagger swagger, GenerationContext genCtx) { if (!genCtx.limitMetrics.isEmpty()) { Map>> quotaDefinitions = new HashMap<>(); @@ -258,7 +352,7 @@ private void writeQuotaDefinitions(Swagger swagger, GenerationContext genCtx) { } private void writeApi(ApiKey apiKey, ImmutableList apiConfigs, - Swagger swagger, GenerationContext genCtx) + Swagger swagger, SwaggerContext context, GenerationContext genCtx) throws ApiConfigException { // TODO: This may result in duplicate validations in the future if made available online genCtx.validator.validate(apiConfigs); @@ -266,20 +360,29 @@ private void writeApi(ApiKey apiKey, ImmutableList apiConfi for (ApiLimitMetricConfig limitMetric : apiConfig.getApiLimitMetrics()) { addNonConflictingApiLimitMetric(genCtx.limitMetrics, limitMetric); } - writeApiClass(apiConfig, swagger, genCtx); + writeApiClass(apiConfig, swagger, context, genCtx); swagger.tag(getTag(apiConfig)); } List schemas = genCtx.schemata.getAllSchemaForApi(apiKey); for (Schema schema : schemas) { - if (isNotInlined(schema)) { - getOrCreateDefinitionMap(swagger).put(schema.name(), convertToSwaggerSchema(schema)); + //enum, maps and some explicitly listed models should be inlined + if (isEnumModel(schema) || isMapModel(schema) || isInlinedModel(schema)) { + continue; } + getOrCreateDefinitionMap(swagger).put(schema.name(), convertToSwaggerSchema(schema)); } } - //enum and map schemas are inlined, shouldn't be in the model definitions - private boolean isNotInlined(Schema schema) { - return schema.enumValues().isEmpty() && schema.mapValueSchema() == null; + private boolean isEnumModel(Schema schema) { + return !schema.enumValues().isEmpty(); + } + + private boolean isMapModel(Schema schema) { + return SchemaRepository.isJsonMapSchema(schema) || schema.mapValueSchema() != null; + } + + private boolean isInlinedModel(Schema schema) { + return INLINED_MODEL_NAMES.contains(schema.name()); } private Tag getTag(ApiConfig apiConfig) { @@ -307,20 +410,19 @@ private void addNonConflictingApiLimitMetric( limitMetrics.put(limitMetric.name(), limitMetric); } - private void writeApiClass(ApiConfig apiConfig, Swagger swagger, + private void writeApiClass(ApiConfig apiConfig, Swagger swagger, SwaggerContext context, GenerationContext genCtx) throws ApiConfigException { Map methodConfigs = apiConfig.getApiClassConfig().getMethods(); for (Map.Entry methodConfig : methodConfigs.entrySet()) { if (!methodConfig.getValue().isIgnored()) { ApiMethodConfig config = methodConfig.getValue(); - writeApiMethod(config, apiConfig, swagger, genCtx); + writeApiMethod(config, apiConfig, swagger, context, genCtx); } } } - private void writeApiMethod( - ApiMethodConfig methodConfig, ApiConfig apiConfig, Swagger swagger, GenerationContext genCtx) - throws ApiConfigException { + private void writeApiMethod(ApiMethodConfig methodConfig, ApiConfig apiConfig, Swagger swagger, + SwaggerContext context, GenerationContext genCtx) throws ApiConfigException { Path path = getOrCreatePath(swagger, methodConfig); Operation operation = new Operation(); operation.setOperationId(getOperationId(apiConfig, methodConfig)); @@ -388,6 +490,30 @@ private void writeApiMethod( response.setResponseSchema(getSchema(schema)); } operation.response(responseCode, response); + + boolean addGoogleJsonErrorAsDefaultResponse = context.addGoogleJsonErrorAsDefaultResponse; + boolean addErrorCodesForServiceExceptions = context.addErrorCodesForServiceExceptions; + if (addGoogleJsonErrorAsDefaultResponse || addErrorCodesForServiceExceptions) { + //add error response model only if necessary + Supplier errorModelSupplier = Suppliers.memoize(() -> getSchema(genCtx.schemata + .getOrAdd(TypeToken.of(GoogleJsonErrorContainer.class), apiConfig))); + if (addErrorCodesForServiceExceptions) { + //add error code specific to the exceptions thrown by the method + Map errorCodes = methodConfig.getErrorCodesAndDescriptions(); + for (Entry entry : errorCodes.entrySet()) { + operation.response(entry.getKey(), new Response() + .description(entry.getValue()) + .responseSchema(errorModelSupplier.get())); + } + } + if (addGoogleJsonErrorAsDefaultResponse) { + //add GoogleJsonError as the default response + operation.defaultResponse(new Response() + .description("A failed response") + .responseSchema(errorModelSupplier.get())); + } + } + writeAuthConfig(swagger, methodConfig, operation); if (methodConfig.isApiKeyRequired()) { List>> security = operation.getSecurity(); @@ -498,10 +624,13 @@ private Property convertToSwaggerProperty(Field f) { } else { SchemaReference schemaReference = f.schemaReference(); if (f.type() == FieldType.OBJECT) { - if (schemaReference.type().isSubtypeOf(Map.class)) { + Schema schema = schemaReference.get(); + if (isInlinedModel(schema)) { + p = inlineObjectProperty(schemaReference); + } else if (isMapModel(schema)) { p = inlineMapProperty(schemaReference); } else { - String name = schemaReference.get().name(); + String name = schema.name(); p = new RefProperty(name).asDefault(name); } } else if (f.type() == FieldType.ARRAY) { @@ -520,9 +649,15 @@ private Property convertToSwaggerProperty(Field f) { return p; } + private Property inlineObjectProperty(SchemaReference schemaReference) { + Schema schema = schemaReference.get(); + Map properties = Maps + .transformValues(schema.fields(), this::convertToSwaggerProperty); + return new ObjectProperty(ImmutableMap.copyOf(properties)); + } + private MapProperty inlineMapProperty(SchemaReference schemaReference) { - Schema schema = schemaReference.repository() - .get(schemaReference.type(), schemaReference.apiConfig()); + Schema schema = schemaReference.get(); Field mapField = schema.mapValueSchema(); if (SchemaRepository.isJsonMapSchema(schema) || mapField == null) { //map field should not be null for non-JsonMap schema, handling anyway @@ -651,12 +786,17 @@ private static void checkExistingDefinition(String defName, OAuth2Definition new } } - //TODO add title and description public static class SwaggerContext { private Scheme scheme = Scheme.HTTPS; private String hostname = "myapi.appspot.com"; private String basePath = "/_ah/api"; private String docVersion = "1.0.0"; + private String title; + private String description; + private boolean addGoogleJsonErrorAsDefaultResponse; + private boolean addErrorCodesForServiceExceptions; + private boolean extractCommonParametersAsRefs; + private boolean combineCommonParametersInSamePath; public SwaggerContext setApiRoot(String apiRoot) { try { @@ -693,6 +833,36 @@ public SwaggerContext setDocVersion(String docVersion) { this.docVersion = docVersion; return this; } + + public SwaggerContext setTitle(String title) { + this.title = title; + return this; + } + + public SwaggerContext setDescription(String description) { + this.description = description; + return this; + } + + public SwaggerContext setAddGoogleJsonErrorAsDefaultResponse(boolean addGoogleJsonErrorAsDefaultResponse) { + this.addGoogleJsonErrorAsDefaultResponse = addGoogleJsonErrorAsDefaultResponse; + return this; + } + + public SwaggerContext setAddErrorCodesForServiceExceptions(boolean addErrorCodesForServiceExceptions) { + this.addErrorCodesForServiceExceptions = addErrorCodesForServiceExceptions; + return this; + } + + public SwaggerContext setExtractCommonParametersAsRefs(boolean extractCommonParametersAsRefs) { + this.extractCommonParametersAsRefs = extractCommonParametersAsRefs; + return this; + } + + public SwaggerContext setCombineCommonParametersInSamePath(boolean combineCommonParametersInSamePath) { + this.combineCommonParametersInSamePath = combineCommonParametersInSamePath; + return this; + } } private static class GenerationContext { diff --git a/endpoints-framework/src/main/java/com/google/api/server/spi/swagger/UNSUPPORTED_FEATURES.md b/endpoints-framework/src/main/java/com/google/api/server/spi/swagger/UNSUPPORTED_FEATURES.md index 3471ecee..0f0186b5 100644 --- a/endpoints-framework/src/main/java/com/google/api/server/spi/swagger/UNSUPPORTED_FEATURES.md +++ b/endpoints-framework/src/main/java/com/google/api/server/spi/swagger/UNSUPPORTED_FEATURES.md @@ -1,10 +1,8 @@ - Info - - **title, description => visible in Endpoints Portal** - license, contact, terms of service - Paths - Configurable tag content and format - Parameters - - **common parameters (global and per-path) => make spec shorter** - range for ints (minimum, maximum) - cardinality (minItems, maxItems) - other repeated param features (uniqueItems, default values) @@ -13,6 +11,5 @@ - Models - required properties (need new annotation property) - Responses - - **schema for Google JSON error format as default for errors** - - **error code descriptions** => use introspection to analyze throws of subclasses of ServiceException + - use introspection or new annotation to describe usage of any subclasses of ServiceException - headers in response \ No newline at end of file diff --git a/endpoints-framework/src/test/java/com/google/api/server/spi/swagger/SwaggerGeneratorTest.java b/endpoints-framework/src/test/java/com/google/api/server/spi/swagger/SwaggerGeneratorTest.java index 81cbcdb4..4a741998 100644 --- a/endpoints-framework/src/test/java/com/google/api/server/spi/swagger/SwaggerGeneratorTest.java +++ b/endpoints-framework/src/test/java/com/google/api/server/spi/swagger/SwaggerGeneratorTest.java @@ -22,6 +22,7 @@ import com.google.api.server.spi.Constant; import com.google.api.server.spi.IoUtil; import com.google.api.server.spi.ServiceContext; +import com.google.api.server.spi.ServiceException; import com.google.api.server.spi.TypeLoader; import com.google.api.server.spi.config.AnnotationBoolean; import com.google.api.server.spi.config.Api; @@ -31,11 +32,15 @@ import com.google.api.server.spi.config.ApiMethod; import com.google.api.server.spi.config.annotationreader.ApiConfigAnnotationReader; import com.google.api.server.spi.config.model.ApiConfig; +import com.google.api.server.spi.response.BadRequestException; +import com.google.api.server.spi.response.ConflictException; +import com.google.api.server.spi.response.NotFoundException; import com.google.api.server.spi.swagger.SwaggerGenerator.SwaggerContext; import com.google.api.server.spi.testing.AbsoluteCommonPathEndpoint; import com.google.api.server.spi.testing.AbsolutePathEndpoint; import com.google.api.server.spi.testing.ArrayEndpoint; import com.google.api.server.spi.testing.EnumEndpoint; +import com.google.api.server.spi.testing.FooCommonParamsEndpoint; import com.google.api.server.spi.testing.FooDescriptionEndpoint; import com.google.api.server.spi.testing.FooEndpoint; import com.google.api.server.spi.testing.LimitMetricsEndpoint; @@ -86,6 +91,37 @@ public void testWriteSwagger_FooEndpoint() throws Exception { Swagger expected = readExpectedAsSwagger("foo_endpoint.swagger"); checkSwagger(expected, swagger); } + + @Test + public void testWriteSwagger_FooEndpointParameterCombineParamSamePath() throws Exception { + ApiConfig config = configLoader.loadConfiguration(ServiceContext.create(), + FooCommonParamsEndpoint.class); + Swagger swagger = generator.writeSwagger(ImmutableList.of(config), context + .setCombineCommonParametersInSamePath(true)); + Swagger expected = readExpectedAsSwagger("foo_endpoint_combine_params_same_path.swagger"); + checkSwagger(expected, swagger); + } + + @Test + public void testWriteSwagger_FooEndpointParameterExtractParamRef() throws Exception { + ApiConfig config = configLoader.loadConfiguration(ServiceContext.create(), + FooCommonParamsEndpoint.class); + Swagger swagger = generator.writeSwagger(ImmutableList.of(config), context + .setExtractCommonParametersAsRefs(true)); + Swagger expected = readExpectedAsSwagger("foo_endpoint_extract_param_refs.swagger"); + checkSwagger(expected, swagger); + } + + @Test + public void testWriteSwagger_FooEndpointParameterCombineAllParam() throws Exception { + ApiConfig config = configLoader.loadConfiguration(ServiceContext.create(), + FooCommonParamsEndpoint.class); + Swagger swagger = generator.writeSwagger(ImmutableList.of(config), context + .setExtractCommonParametersAsRefs(true) + .setCombineCommonParametersInSamePath(true)); + Swagger expected = readExpectedAsSwagger("foo_endpoint_combine_all_params.swagger"); + checkSwagger(expected, swagger); + } @Test public void testWriteSwagger_FooEndpointDefaultContext() throws Exception { @@ -256,6 +292,34 @@ public void testWriteSwagger_MultiVersionEndpoint() throws Exception { checkSwagger(expected, swagger); } + @Test + public void testWriteSwagger_ErrorAsDefaultResponse() throws Exception { + ApiConfig config = configLoader.loadConfiguration(ServiceContext.create(), ExceptionEndpoint.class); + Swagger swagger = generator.writeSwagger(ImmutableList.of(config), context + .setAddGoogleJsonErrorAsDefaultResponse(true)); + Swagger expected = readExpectedAsSwagger("error_codes_default_response.swagger"); + checkSwagger(expected, swagger); + } + + @Test + public void testWriteSwagger_ServiceExceptionErrorCodes() throws Exception { + ApiConfig config = configLoader.loadConfiguration(ServiceContext.create(), ExceptionEndpoint.class); + Swagger swagger = generator.writeSwagger(ImmutableList.of(config), context + .setAddErrorCodesForServiceExceptions(true)); + Swagger expected = readExpectedAsSwagger("error_codes_service_exceptions.swagger"); + checkSwagger(expected, swagger); + } + + @Test + public void testWriteSwagger_AllErrors() throws Exception { + ApiConfig config = configLoader.loadConfiguration(ServiceContext.create(), ExceptionEndpoint.class); + Swagger swagger = generator.writeSwagger(ImmutableList.of(config), context + .setAddGoogleJsonErrorAsDefaultResponse(true) + .setAddErrorCodesForServiceExceptions(true)); + Swagger expected = readExpectedAsSwagger("error_codes_all.swagger"); + checkSwagger(expected, swagger); + } + private Swagger getSwagger(Class serviceClass, SwaggerContext context) throws Exception { ApiConfig config = configLoader.loadConfiguration(ServiceContext.create(), serviceClass); @@ -335,4 +399,23 @@ public void unknownScope() { } @ApiMethod(audiences = {"audience2"}) public void overrideAudience() { } } + + @Api(name = "exceptions", version = "v1") + private static class ExceptionEndpoint { + @ApiMethod + public void doesNotThrow() { } + + @ApiMethod + public void throwsServiceException() throws ServiceException { } + + @ApiMethod + public void throwsNotFoundException() throws NotFoundException { } + + @ApiMethod + public void throwsMultipleExceptions() throws BadRequestException, ConflictException { } + + @ApiMethod + public void throwsUnknownException() throws IllegalStateException { } + } + } diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/error_codes_all.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/error_codes_all.swagger new file mode 100644 index 00000000..6681340c --- /dev/null +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/error_codes_all.swagger @@ -0,0 +1,185 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0.0", + "title": "swagger-test.appspot.com" + }, + "host": "swagger-test.appspot.com", + "basePath": "/api", + "tags": [ + { + "name": "exceptions:v1" + } + ], + "schemes": [ + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/exceptions/v1/doesNotThrow": { + "post": { + "tags": [ + "exceptions:v1" + ], + "operationId": "ExceptionsV1DoesNotThrow", + "parameters": [], + "responses": { + "204": { + "description": "A successful response" + }, + "default": { + "description": "A failed response", + "schema": { + "$ref": "#/definitions/GoogleJsonErrorContainer" + } + } + } + } + }, + "/exceptions/v1/throwsMultipleExceptions": { + "post": { + "tags": [ + "exceptions:v1" + ], + "operationId": "ExceptionsV1ThrowsMultipleExceptions", + "parameters": [], + "responses": { + "204": { + "description": "A successful response" + }, + "400": { + "description": "badRequest", + "schema": { + "$ref": "#/definitions/GoogleJsonErrorContainer" + } + }, + "409": { + "description": "conflict", + "schema": { + "$ref": "#/definitions/GoogleJsonErrorContainer" + } + }, + "default": { + "description": "A failed response", + "schema": { + "$ref": "#/definitions/GoogleJsonErrorContainer" + } + } + } + } + }, + "/exceptions/v1/throwsNotFoundException": { + "post": { + "tags": [ + "exceptions:v1" + ], + "operationId": "ExceptionsV1ThrowsNotFoundException", + "parameters": [], + "responses": { + "204": { + "description": "A successful response" + }, + "404": { + "description": "notFound", + "schema": { + "$ref": "#/definitions/GoogleJsonErrorContainer" + } + }, + "default": { + "description": "A failed response", + "schema": { + "$ref": "#/definitions/GoogleJsonErrorContainer" + } + } + } + } + }, + "/exceptions/v1/throwsServiceException": { + "post": { + "tags": [ + "exceptions:v1" + ], + "operationId": "ExceptionsV1ThrowsServiceException", + "parameters": [], + "responses": { + "204": { + "description": "A successful response" + }, + "default": { + "description": "A failed response", + "schema": { + "$ref": "#/definitions/GoogleJsonErrorContainer" + } + } + } + } + }, + "/exceptions/v1/throwsUnknownException": { + "post": { + "tags": [ + "exceptions:v1" + ], + "operationId": "ExceptionsV1ThrowsUnknownException", + "parameters": [], + "responses": { + "204": { + "description": "A successful response" + }, + "default": { + "description": "A failed response", + "schema": { + "$ref": "#/definitions/GoogleJsonErrorContainer" + } + } + } + } + } + }, + "definitions": { + "GoogleJsonErrorContainer": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "errors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "domain": { + "type": "string" + }, + "location": { + "type": "string" + }, + "locationType": { + "type": "string" + }, + "message": { + "type": "string" + }, + "reason": { + "type": "string" + } + } + } + }, + "message": { + "type": "string" + } + } + } + } + } + } +} diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/error_codes_default_response.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/error_codes_default_response.swagger new file mode 100644 index 00000000..bebaf75d --- /dev/null +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/error_codes_default_response.swagger @@ -0,0 +1,167 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0.0", + "title": "swagger-test.appspot.com" + }, + "host": "swagger-test.appspot.com", + "basePath": "/api", + "tags": [ + { + "name": "exceptions:v1" + } + ], + "schemes": [ + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/exceptions/v1/doesNotThrow": { + "post": { + "tags": [ + "exceptions:v1" + ], + "operationId": "ExceptionsV1DoesNotThrow", + "parameters": [], + "responses": { + "204": { + "description": "A successful response" + }, + "default": { + "description": "A failed response", + "schema": { + "$ref": "#/definitions/GoogleJsonErrorContainer" + } + } + } + } + }, + "/exceptions/v1/throwsMultipleExceptions": { + "post": { + "tags": [ + "exceptions:v1" + ], + "operationId": "ExceptionsV1ThrowsMultipleExceptions", + "parameters": [], + "responses": { + "204": { + "description": "A successful response" + }, + "default": { + "description": "A failed response", + "schema": { + "$ref": "#/definitions/GoogleJsonErrorContainer" + } + } + } + } + }, + "/exceptions/v1/throwsNotFoundException": { + "post": { + "tags": [ + "exceptions:v1" + ], + "operationId": "ExceptionsV1ThrowsNotFoundException", + "parameters": [], + "responses": { + "204": { + "description": "A successful response" + }, + "default": { + "description": "A failed response", + "schema": { + "$ref": "#/definitions/GoogleJsonErrorContainer" + } + } + } + } + }, + "/exceptions/v1/throwsServiceException": { + "post": { + "tags": [ + "exceptions:v1" + ], + "operationId": "ExceptionsV1ThrowsServiceException", + "parameters": [], + "responses": { + "204": { + "description": "A successful response" + }, + "default": { + "description": "A failed response", + "schema": { + "$ref": "#/definitions/GoogleJsonErrorContainer" + } + } + } + } + }, + "/exceptions/v1/throwsUnknownException": { + "post": { + "tags": [ + "exceptions:v1" + ], + "operationId": "ExceptionsV1ThrowsUnknownException", + "parameters": [], + "responses": { + "204": { + "description": "A successful response" + }, + "default": { + "description": "A failed response", + "schema": { + "$ref": "#/definitions/GoogleJsonErrorContainer" + } + } + } + } + } + }, + "definitions": { + "GoogleJsonErrorContainer": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "errors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "domain": { + "type": "string" + }, + "location": { + "type": "string" + }, + "locationType": { + "type": "string" + }, + "message": { + "type": "string" + }, + "reason": { + "type": "string" + } + } + } + }, + "message": { + "type": "string" + } + } + } + } + } + } +} diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/error_codes_service_exceptions.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/error_codes_service_exceptions.swagger new file mode 100644 index 00000000..51ceb92b --- /dev/null +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/error_codes_service_exceptions.swagger @@ -0,0 +1,155 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0.0", + "title": "swagger-test.appspot.com" + }, + "host": "swagger-test.appspot.com", + "basePath": "/api", + "tags": [ + { + "name": "exceptions:v1" + } + ], + "schemes": [ + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/exceptions/v1/doesNotThrow": { + "post": { + "tags": [ + "exceptions:v1" + ], + "operationId": "ExceptionsV1DoesNotThrow", + "parameters": [], + "responses": { + "204": { + "description": "A successful response" + } + } + } + }, + "/exceptions/v1/throwsMultipleExceptions": { + "post": { + "tags": [ + "exceptions:v1" + ], + "operationId": "ExceptionsV1ThrowsMultipleExceptions", + "parameters": [], + "responses": { + "204": { + "description": "A successful response" + }, + "400": { + "description": "badRequest", + "schema": { + "$ref": "#/definitions/GoogleJsonErrorContainer" + } + }, + "409": { + "description": "conflict", + "schema": { + "$ref": "#/definitions/GoogleJsonErrorContainer" + } + } + } + } + }, + "/exceptions/v1/throwsNotFoundException": { + "post": { + "tags": [ + "exceptions:v1" + ], + "operationId": "ExceptionsV1ThrowsNotFoundException", + "parameters": [], + "responses": { + "204": { + "description": "A successful response" + }, + "404": { + "description": "notFound", + "schema": { + "$ref": "#/definitions/GoogleJsonErrorContainer" + } + } + } + } + }, + "/exceptions/v1/throwsServiceException": { + "post": { + "tags": [ + "exceptions:v1" + ], + "operationId": "ExceptionsV1ThrowsServiceException", + "parameters": [], + "responses": { + "204": { + "description": "A successful response" + } + } + } + }, + "/exceptions/v1/throwsUnknownException": { + "post": { + "tags": [ + "exceptions:v1" + ], + "operationId": "ExceptionsV1ThrowsUnknownException", + "parameters": [], + "responses": { + "204": { + "description": "A successful response" + } + } + } + } + }, + "definitions": { + "GoogleJsonErrorContainer": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "errors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "domain": { + "type": "string" + }, + "location": { + "type": "string" + }, + "locationType": { + "type": "string" + }, + "message": { + "type": "string" + }, + "reason": { + "type": "string" + } + } + } + }, + "message": { + "type": "string" + } + } + } + } + } + } +} diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint_combine_all_params.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint_combine_all_params.swagger new file mode 100644 index 00000000..27162fbd --- /dev/null +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint_combine_all_params.swagger @@ -0,0 +1,401 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0.0", + "title": "swagger-test.appspot.com" + }, + "host": "swagger-test.appspot.com", + "basePath": "/api", + "tags": [ + { + "name": "foo:v1", + "description": "Just Foo Things", + "externalDocs": { + "url": "https://example.com" + } + } + ], + "schemes": [ + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/foo/v1/fooos": { + "get": { + "tags": [ + "foo:v1" + ], + "operationId": "FooV1ListFooos", + "parameters": [ + { + "$ref": "#/parameters/n" + } + ], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/CollectionResponse_Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + } + }, + "/foo/v1/fooos/{n}": { + "get": { + "tags": [ + "foo:v1" + ], + "operationId": "FooV1ListFooosInPath", + "parameters": [], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/CollectionResponse_Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "parameters": [ + { + "name": "n", + "in": "path", + "required": true, + "type": "integer", + "format": "int32" + } + ] + }, + "/foo/v1/fooosNotRequired": { + "get": { + "tags": [ + "foo:v1" + ], + "operationId": "FooV1ListFooosNotRequired", + "parameters": [], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/CollectionResponse_Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "parameters": [ + { + "name": "n", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + } + ] + }, + "/foo/v1/foos": { + "get": { + "tags": [ + "foo:v1" + ], + "description": "list desc", + "operationId": "FooV1ListFoos", + "parameters": [ + { + "$ref": "#/parameters/n" + } + ], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/CollectionResponse_Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "post": { + "tags": [ + "foo:v1" + ], + "operationId": "FooV1Toplevel", + "parameters": [], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/CollectionResponse_Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + } + }, + "/foo/v1/foos/{id}": { + "get": { + "tags": [ + "foo:v1" + ], + "description": "get desc", + "operationId": "FooV1GetFoo", + "parameters": [], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "post": { + "tags": [ + "foo:v1" + ], + "description": "update desc", + "operationId": "FooV1UpdateFoo", + "parameters": [ + { + "in": "body", + "name": "body", + "required": false, + "schema": { + "$ref": "#/definitions/Foo" + } + } + ], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "put": { + "tags": [ + "foo:v1" + ], + "description": "create desc", + "operationId": "FooV1CreateFoo", + "parameters": [ + { + "in": "body", + "name": "body", + "required": false, + "schema": { + "$ref": "#/definitions/Foo" + } + } + ], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "delete": { + "tags": [ + "foo:v1" + ], + "description": "delete desc", + "operationId": "FooV1DeleteFoo", + "parameters": [], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "parameters": [ + { + "name": "id", + "in": "path", + "description": "id desc", + "required": true, + "type": "string" + } + ] + } + }, + "securityDefinitions": { + "google_id_token-3a26ea04": { + "type": "oauth2", + "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth", + "flow": "implicit", + "scopes": { + "https://www.googleapis.com/auth/userinfo.email": "View your email address" + }, + "x-google-issuer": "https://accounts.google.com", + "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v1/certs", + "x-google-audiences": "audience" + }, + "google_id_token_legacy-3a26ea04": { + "type": "oauth2", + "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth", + "flow": "implicit", + "scopes": { + "https://www.googleapis.com/auth/userinfo.email": "View your email address" + }, + "x-google-issuer": "accounts.google.com", + "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v1/certs", + "x-google-audiences": "audience" + } + }, + "definitions": { + "CollectionResponse_Foo": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/Foo" + } + }, + "nextPageToken": { + "type": "string" + } + } + }, + "Foo": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "integer", + "format": "int32" + } + } + } + }, + "parameters": { + "n": { + "name": "n", + "in": "query", + "required": true, + "type": "integer", + "format": "int32" + } + } +} diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint_combine_params_same_path.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint_combine_params_same_path.swagger new file mode 100644 index 00000000..df513d5b --- /dev/null +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint_combine_params_same_path.swagger @@ -0,0 +1,401 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0.0", + "title": "swagger-test.appspot.com" + }, + "host": "swagger-test.appspot.com", + "basePath": "/api", + "tags": [ + { + "name": "foo:v1", + "description": "Just Foo Things", + "externalDocs": { + "url": "https://example.com" + } + } + ], + "schemes": [ + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/foo/v1/fooos": { + "get": { + "tags": [ + "foo:v1" + ], + "operationId": "FooV1ListFooos", + "parameters": [], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/CollectionResponse_Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "parameters": [ + { + "name": "n", + "in": "query", + "required": true, + "type": "integer", + "format": "int32" + } + ] + }, + "/foo/v1/fooos/{n}": { + "get": { + "tags": [ + "foo:v1" + ], + "operationId": "FooV1ListFooosInPath", + "parameters": [], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/CollectionResponse_Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "parameters": [ + { + "name": "n", + "in": "path", + "required": true, + "type": "integer", + "format": "int32" + } + ] + }, + "/foo/v1/fooosNotRequired": { + "get": { + "tags": [ + "foo:v1" + ], + "operationId": "FooV1ListFooosNotRequired", + "parameters": [], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/CollectionResponse_Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "parameters": [ + { + "name": "n", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + } + ] + }, + "/foo/v1/foos": { + "get": { + "tags": [ + "foo:v1" + ], + "description": "list desc", + "operationId": "FooV1ListFoos", + "parameters": [ + { + "name": "n", + "in": "query", + "required": true, + "type": "integer", + "format": "int32" + } + ], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/CollectionResponse_Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "post": { + "tags": [ + "foo:v1" + ], + "operationId": "FooV1Toplevel", + "parameters": [], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/CollectionResponse_Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + } + }, + "/foo/v1/foos/{id}": { + "get": { + "tags": [ + "foo:v1" + ], + "description": "get desc", + "operationId": "FooV1GetFoo", + "parameters": [], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "post": { + "tags": [ + "foo:v1" + ], + "description": "update desc", + "operationId": "FooV1UpdateFoo", + "parameters": [ + { + "in": "body", + "name": "body", + "required": false, + "schema": { + "$ref": "#/definitions/Foo" + } + } + ], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "put": { + "tags": [ + "foo:v1" + ], + "description": "create desc", + "operationId": "FooV1CreateFoo", + "parameters": [ + { + "in": "body", + "name": "body", + "required": false, + "schema": { + "$ref": "#/definitions/Foo" + } + } + ], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "delete": { + "tags": [ + "foo:v1" + ], + "description": "delete desc", + "operationId": "FooV1DeleteFoo", + "parameters": [], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "parameters": [ + { + "name": "id", + "in": "path", + "description": "id desc", + "required": true, + "type": "string" + } + ] + } + }, + "securityDefinitions": { + "google_id_token-3a26ea04": { + "type": "oauth2", + "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth", + "flow": "implicit", + "scopes": { + "https://www.googleapis.com/auth/userinfo.email": "View your email address" + }, + "x-google-issuer": "https://accounts.google.com", + "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v1/certs", + "x-google-audiences": "audience" + }, + "google_id_token_legacy-3a26ea04": { + "type": "oauth2", + "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth", + "flow": "implicit", + "scopes": { + "https://www.googleapis.com/auth/userinfo.email": "View your email address" + }, + "x-google-issuer": "accounts.google.com", + "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v1/certs", + "x-google-audiences": "audience" + } + }, + "definitions": { + "CollectionResponse_Foo": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/Foo" + } + }, + "nextPageToken": { + "type": "string" + } + } + }, + "Foo": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "integer", + "format": "int32" + } + } + } + } +} diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint_extract_param_refs.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint_extract_param_refs.swagger new file mode 100644 index 00000000..79e0bfd6 --- /dev/null +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/foo_endpoint_extract_param_refs.swagger @@ -0,0 +1,420 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0.0", + "title": "swagger-test.appspot.com" + }, + "host": "swagger-test.appspot.com", + "basePath": "/api", + "tags": [ + { + "name": "foo:v1", + "description": "Just Foo Things", + "externalDocs": { + "url": "https://example.com" + } + } + ], + "schemes": [ + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/foo/v1/fooos": { + "get": { + "tags": [ + "foo:v1" + ], + "operationId": "FooV1ListFooos", + "parameters": [ + { + "$ref": "#/parameters/n" + } + ], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/CollectionResponse_Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + } + }, + "/foo/v1/fooos/{n}": { + "get": { + "tags": [ + "foo:v1" + ], + "operationId": "FooV1ListFooosInPath", + "parameters": [ + { + "name": "n", + "in": "path", + "required": true, + "type": "integer", + "format": "int32" + } + ], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/CollectionResponse_Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + } + }, + "/foo/v1/fooosNotRequired": { + "get": { + "tags": [ + "foo:v1" + ], + "operationId": "FooV1ListFooosNotRequired", + "parameters": [ + { + "name": "n", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + } + ], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/CollectionResponse_Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + } + }, + "/foo/v1/foos": { + "get": { + "tags": [ + "foo:v1" + ], + "description": "list desc", + "operationId": "FooV1ListFoos", + "parameters": [ + { + "$ref": "#/parameters/n" + } + ], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/CollectionResponse_Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "post": { + "tags": [ + "foo:v1" + ], + "operationId": "FooV1Toplevel", + "parameters": [], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/CollectionResponse_Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + } + }, + "/foo/v1/foos/{id}": { + "get": { + "tags": [ + "foo:v1" + ], + "description": "get desc", + "operationId": "FooV1GetFoo", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "id desc", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "post": { + "tags": [ + "foo:v1" + ], + "description": "update desc", + "operationId": "FooV1UpdateFoo", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "id desc", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": false, + "schema": { + "$ref": "#/definitions/Foo" + } + } + ], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "put": { + "tags": [ + "foo:v1" + ], + "description": "create desc", + "operationId": "FooV1CreateFoo", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "id desc", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": false, + "schema": { + "$ref": "#/definitions/Foo" + } + } + ], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + }, + "delete": { + "tags": [ + "foo:v1" + ], + "description": "delete desc", + "operationId": "FooV1DeleteFoo", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "id desc", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "A successful response", + "schema": { + "$ref": "#/definitions/Foo" + } + } + }, + "security": [ + { + "google_id_token-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + }, + { + "google_id_token_legacy-3a26ea04": [ + "https://www.googleapis.com/auth/userinfo.email" + ] + } + ] + } + } + }, + "securityDefinitions": { + "google_id_token-3a26ea04": { + "type": "oauth2", + "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth", + "flow": "implicit", + "scopes": { + "https://www.googleapis.com/auth/userinfo.email": "View your email address" + }, + "x-google-issuer": "https://accounts.google.com", + "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v1/certs", + "x-google-audiences": "audience" + }, + "google_id_token_legacy-3a26ea04": { + "type": "oauth2", + "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth", + "flow": "implicit", + "scopes": { + "https://www.googleapis.com/auth/userinfo.email": "View your email address" + }, + "x-google-issuer": "accounts.google.com", + "x-google-jwks_uri": "https://www.googleapis.com/oauth2/v1/certs", + "x-google-audiences": "audience" + } + }, + "definitions": { + "CollectionResponse_Foo": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/Foo" + } + }, + "nextPageToken": { + "type": "string" + } + } + }, + "Foo": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "integer", + "format": "int32" + } + } + } + }, + "parameters": { + "n": { + "name": "n", + "in": "query", + "required": true, + "type": "integer", + "format": "int32" + } + } +} diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/map_endpoint.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/map_endpoint.swagger index eb8e118c..a607d808 100644 --- a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/map_endpoint.swagger +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/map_endpoint.swagger @@ -71,7 +71,8 @@ "schema": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } } } @@ -295,7 +296,8 @@ "schema": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } } } @@ -408,9 +410,6 @@ } } }, - "JsonMap": { - "type": "object" - }, "MapContainer": { "type": "object", "properties": { @@ -512,13 +511,15 @@ "stringArrayMap": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } }, "stringCollectionMap": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } }, "stringMap": { diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/map_endpoint_legacy.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/map_endpoint_legacy.swagger index 2ef30008..021a26a6 100644 --- a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/map_endpoint_legacy.swagger +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/map_endpoint_legacy.swagger @@ -34,7 +34,8 @@ "schema": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } } } @@ -71,7 +72,8 @@ "schema": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } } } @@ -91,7 +93,8 @@ "schema": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } } } @@ -111,7 +114,8 @@ "schema": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } } } @@ -131,7 +135,8 @@ "schema": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } } } @@ -151,7 +156,8 @@ "schema": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } } } @@ -171,7 +177,8 @@ "schema": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } } } @@ -191,7 +198,8 @@ "schema": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } } } @@ -211,7 +219,8 @@ "schema": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } } } @@ -231,7 +240,8 @@ "schema": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } } } @@ -251,7 +261,8 @@ "schema": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } } } @@ -271,7 +282,8 @@ "schema": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } } } @@ -291,7 +303,8 @@ "schema": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } } } @@ -311,7 +324,8 @@ "schema": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } } } @@ -331,7 +345,8 @@ "schema": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } } } @@ -368,7 +383,8 @@ "schema": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } } } @@ -377,9 +393,6 @@ } }, "definitions": { - "JsonMap": { - "type": "object" - }, "MapContainer": { "type": "object", "properties": { @@ -387,7 +400,8 @@ "type": "object", "description": "A map of string values", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } } } @@ -398,67 +412,78 @@ "bazMap": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } }, "booleanKeyMap": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } }, "dateKeyMap": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } }, "dateTimeKeyMap": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } }, "enumKeyMap": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } }, "floatKeyMap": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } }, "fooMap": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } }, "fooMapMap": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } }, "intKeyMap": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } }, "intMap": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } }, "longKeyMap": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } }, "mapOfStrings": { @@ -470,31 +495,36 @@ "mapSubclass": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } }, "stringArrayMap": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } }, "stringCollectionMap": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } }, "stringMap": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } }, "stringValueMap": { "type": "object", "additionalProperties": { - "type": "object" + "type": "object", + "properties": {} } } } diff --git a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/multiple_scopes.swagger b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/multiple_scopes.swagger index b9916328..d7a45419 100644 --- a/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/multiple_scopes.swagger +++ b/endpoints-framework/src/test/resources/com/google/api/server/spi/swagger/multiple_scopes.swagger @@ -29,7 +29,7 @@ "operationId": "MultipleScopesV1NoOverride", "parameters": [], "responses": { - "200": { + "204": { "description": "A successful response" } }, @@ -55,7 +55,7 @@ "operationId": "MultipleScopesV1OverrideAudience", "parameters": [], "responses": { - "200": { + "204": { "description": "A successful response" } }, @@ -81,7 +81,7 @@ "operationId": "MultipleScopesV1ScopeOverride", "parameters": [], "responses": { - "200": { + "204": { "description": "A successful response" } }, @@ -107,7 +107,7 @@ "operationId": "MultipleScopesV1UnknownScope", "parameters": [], "responses": { - "200": { + "204": { "description": "A successful response" } }, diff --git a/test-utils/src/main/java/com/google/api/server/spi/testing/FooCommonParamsEndpoint.java b/test-utils/src/main/java/com/google/api/server/spi/testing/FooCommonParamsEndpoint.java new file mode 100644 index 00000000..4faaffe0 --- /dev/null +++ b/test-utils/src/main/java/com/google/api/server/spi/testing/FooCommonParamsEndpoint.java @@ -0,0 +1,48 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * 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 com.google.api.server.spi.testing; + +import com.google.api.server.spi.config.Api; +import com.google.api.server.spi.config.ApiMethod; +import com.google.api.server.spi.config.ApiMethod.HttpMethod; +import com.google.api.server.spi.config.Named; +import com.google.api.server.spi.config.Nullable; +import com.google.api.server.spi.response.CollectionResponse; + +@Api( + name = "foo", + version = "v1", + audiences = {"audience"}, + title = "The Foo API", + description = "Just Foo Things", + documentationLink = "https://example.com", + canonicalName = "CanonicalName") +public class FooCommonParamsEndpoint extends FooEndpoint { + @ApiMethod(name = "fooo.list", path = "fooos", httpMethod = HttpMethod.GET) + public CollectionResponse listFooos(@Named("n") Integer n) { + return null; + } + + @ApiMethod(path = "fooos/{n}", httpMethod = HttpMethod.GET) + public CollectionResponse listFooosInPath(@Named("n") Integer n) { + return null; + } + + @ApiMethod(path = "fooosNotRequired", httpMethod = HttpMethod.GET) + public CollectionResponse listFooosNotRequired(@Named("n") @Nullable Integer n) { + return null; + } +}