Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ public class GetOpenApiDocAction extends EndpointsToolAction {
private Option titleOption = makeTitleOption();
private Option descriptionOption = makeDescriptionOption();
private Option apiNameOption = makeApiNameOption();
private Option tagTemplateOption = makeTagTemplateOption();
private Option operationIdTemplateOption = makeOperationIdTemplateOption();
private Option addGoogleJsonErrorAsDefaultResponseOption = makeVisibleFlagOption(
"addGoogleJsonErrorAsDefaultResponse", "Add GoogleJsonError as default response"
);
Expand All @@ -78,6 +80,7 @@ protected GetOpenApiDocAction(String alias, boolean displayHelp) {
setOptions(
Arrays.asList(classPathOption, outputOption, warOption, hostnameOption, basePathOption,
titleOption, descriptionOption, apiNameOption,
tagTemplateOption, operationIdTemplateOption,
addGoogleJsonErrorAsDefaultResponseOption, addErrorCodesForServiceExceptionsOption,
extractCommonParametersAsRefsOption, combineCommonParametersInSamePathOption));
setShortDescription("Generates an OpenAPI document");
Expand Down Expand Up @@ -109,6 +112,22 @@ private static Option makeApiNameOption() {
"API_NAME",
"Sets the api name. Endpoints Management will use hostname if not defined.");
}

private static Option makeTagTemplateOption() {
return EndpointsOption.makeVisibleNonFlagOption(
"tt",
"tagTemplate",
"TAG_TEMPLATE",
"Sets the tag template name. Defaults to " + SwaggerContext.DEFAULT_TAG_TEMPLATE + ".");
}

private static Option makeOperationIdTemplateOption() {
return EndpointsOption.makeVisibleNonFlagOption(
"oit",
"operationIdTemplate",
"OPERATION_ID_TEMPLATE",
"Sets the operation id template. Defaults to " + SwaggerContext.DEFAULT_OPERATION_ID_TEMPLATE + ".");
}

private static boolean getBooleanOptionValue(Option option) {
return option.getValue() != null;
Expand All @@ -132,6 +151,8 @@ public boolean execute() throws ClassNotFoundException, IOException, ApiConfigEx
getOptionOrDefault(titleOption, null),
getOptionOrDefault(descriptionOption, null),
getOptionOrDefault(apiNameOption, null),
getOptionOrDefault(tagTemplateOption, SwaggerContext.DEFAULT_TAG_TEMPLATE),
getOptionOrDefault(operationIdTemplateOption, SwaggerContext.DEFAULT_OPERATION_ID_TEMPLATE),
getBooleanOptionValue(addGoogleJsonErrorAsDefaultResponseOption),
getBooleanOptionValue(addErrorCodesForServiceExceptionsOption),
getBooleanOptionValue(extractCommonParametersAsRefsOption),
Expand All @@ -154,6 +175,7 @@ public boolean execute() throws ClassNotFoundException, IOException, ApiConfigEx
public String genOpenApiDoc(
URL[] classPath, String outputFilePath, String hostname, String basePath,
String title, String description, String apiName,
String tagTemplate, String operationIdTemplate,
boolean addGoogleJsonErrorAsDefaultResponse, boolean addErrorCodesForServiceExceptionsOption,
boolean extractCommonParametersAsRefsOption, boolean combineCommonParametersInSamePathOption,
List<String> serviceClassNames, boolean outputToDisk)
Expand Down Expand Up @@ -182,6 +204,8 @@ public String genOpenApiDoc(
.setTitle(title)
.setDescription(description)
.setApiName(apiName)
.setTagTemplate(tagTemplate)
.setOperationIdTemplate(operationIdTemplate)
.setAddGoogleJsonErrorAsDefaultResponse(addGoogleJsonErrorAsDefaultResponse)
.setAddErrorCodesForServiceExceptions(addErrorCodesForServiceExceptionsOption)
.setExtractCommonParametersAsRefs(extractCommonParametersAsRefsOption)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import com.google.api.server.spi.swagger.SwaggerGenerator.SwaggerContext;
import com.google.appengine.tools.util.Option;
import com.google.common.collect.Lists;

Expand All @@ -44,6 +45,8 @@ public class GetOpenApiDocActionTest extends EndpointsToolTest {
private String outputFilePath;
private String basePath;
private List<String> serviceClassNames;
private String tagTemplate;
private String operationIdTemplate;
private boolean outputToDisk;
private boolean addGoogleJsonErrorAsDefaultResponse;
private boolean addErrorCodesForServiceExceptionsOption;
Expand All @@ -56,6 +59,7 @@ protected void addTestAction(Map<String, EndpointsToolAction> actions) {
public String genOpenApiDoc(
URL[] classPath, String outputFilePath, String hostname, String basePath,
String title, String description, String apiName,
String tagTemplate, String operationIdTemplate,
boolean addGoogleJsonErrorAsDefaultResponse,
boolean addErrorCodesForServiceExceptionsOption,
boolean extractCommonParametersAsRefsOption,
Expand All @@ -65,6 +69,8 @@ public String genOpenApiDoc(
GetOpenApiDocActionTest.this.outputFilePath = outputFilePath;
GetOpenApiDocActionTest.this.basePath = basePath;
GetOpenApiDocActionTest.this.serviceClassNames = serviceClassNames;
GetOpenApiDocActionTest.this.tagTemplate = tagTemplate;
GetOpenApiDocActionTest.this.operationIdTemplate = operationIdTemplate;
GetOpenApiDocActionTest.this.outputToDisk = outputToDisk;
GetOpenApiDocActionTest.this.addGoogleJsonErrorAsDefaultResponse
= addGoogleJsonErrorAsDefaultResponse;
Expand Down Expand Up @@ -97,6 +103,7 @@ public void testGetOpenApiDoc() throws Exception {
new String[]{GetOpenApiDocAction.NAME, option(EndpointsToolAction.OPTION_CLASS_PATH_SHORT),
"classPath", option(EndpointsToolAction.OPTION_OUTPUT_DIR_SHORT), "outputDir",
option("addGoogleJsonErrorAsDefaultResponse", false),
option("tt"), "myCustomTemplate",
"MyService",
"MyService2"});
assertFalse(usagePrinted);
Expand All @@ -110,6 +117,8 @@ public void testGetOpenApiDoc() throws Exception {
assertEquals(EndpointsToolAction.DEFAULT_BASE_PATH, basePath);
assertTrue(addGoogleJsonErrorAsDefaultResponse);
assertFalse(addErrorCodesForServiceExceptionsOption);
assertEquals("myCustomTemplate", tagTemplate);
assertEquals(SwaggerContext.DEFAULT_OPERATION_ID_TEMPLATE, operationIdTemplate);
assertStringsEqual(Arrays.asList("MyService", "MyService2"), serviceClassNames);
assertTrue(outputToDisk);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@
import java.util.TreeMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.lang3.text.StrSubstitutor;

/**
* Generates a {@link Swagger} object representing a set of {@link ApiConfig} objects.
Expand All @@ -136,10 +137,7 @@ public class SwaggerGenerator {
private static final String METRIC_KIND = "GAUGE";
private static final String METRICS_KEY = "metrics";
private static final String QUOTA_KEY = "quota";

private static final Converter<String, String> CONVERTER =
CaseFormat.LOWER_CAMEL.converterTo(CaseFormat.UPPER_CAMEL);
private static final Joiner JOINER = Joiner.on("").skipNulls();

private static final ImmutableMap<Type, String> TYPE_TO_STRING_MAP =
ImmutableMap.<java.lang.reflect.Type, String>builder()
.put(String.class, "string")
Expand Down Expand Up @@ -402,7 +400,7 @@ private void writeApi(ApiKey apiKey, ImmutableList<? extends ApiConfig> apiConfi
addNonConflictingApiLimitMetric(genCtx.limitMetrics, limitMetric);
}
writeApiClass(apiConfig, swagger, context, genCtx);
swagger.tag(getTag(apiConfig));
swagger.tag(getTag(apiConfig, context));
}
List<Schema> schemas = genCtx.schemata.getAllSchemaForApi(apiKey);
for (Schema schema : schemas) {
Expand All @@ -426,8 +424,8 @@ private boolean isInlinedModel(Schema schema) {
return INLINED_MODEL_NAMES.contains(schema.name());
}

private Tag getTag(ApiConfig apiConfig) {
Tag tag = new Tag().name(getTagName(apiConfig));
private Tag getTag(ApiConfig apiConfig, SwaggerContext context) {
Tag tag = new Tag().name(getTagName(apiConfig, context));
String description = apiConfig.getDescription();
if (!Strings.isEmptyOrWhitespace(description)) {
tag.description(description);
Expand Down Expand Up @@ -466,8 +464,8 @@ private void writeApiMethod(ApiMethodConfig methodConfig, ApiConfig apiConfig, S
SwaggerContext context, GenerationContext genCtx) throws ApiConfigException {
Path path = getOrCreatePath(swagger, methodConfig);
Operation operation = new Operation()
.operationId(getOperationId(apiConfig, methodConfig))
.tags(Collections.singletonList(getTagName(apiConfig)))
.operationId(getOperationId(apiConfig, methodConfig, context))
.tags(Collections.singletonList(getTagName(apiConfig, context)))
.description(methodConfig.getDescription())
.deprecated(methodConfig.isDeprecated() ? true : null);
Collection<String> pathParameters = methodConfig.getPathParameters();
Expand Down Expand Up @@ -729,23 +727,16 @@ private MapProperty inlineMapProperty(SchemaReference schemaReference) {
return new MapProperty(convertToSwaggerProperty(mapField));
}

private static String getTagName(ApiConfig apiConfig) {
String tag = apiConfig.getName() + ":" + apiConfig.getVersion();
String resource = apiConfig.getApiClassConfig().getResource();
if (!Strings.isEmptyOrWhitespace(resource)) {
tag += "." + CONVERTER.convert(resource);
}
return tag;
private static String getTagName(ApiConfig apiConfig, SwaggerContext context) {
return NamingContext.build(apiConfig, null).resolve(context.tagTemplate);
}

private String getFullRef(RefType type, String name) {
return type.getInternalPrefix() + UrlEscapers.urlFormParameterEscaper().escape(name);
}

private static String getOperationId(ApiConfig apiConfig, ApiMethodConfig methodConfig) {
return FluentIterable.of(apiConfig.getName(), apiConfig.getVersion(),
apiConfig.getApiClassConfig().getResource(), methodConfig.getEndpointMethodName())
.transform(CONVERTER).join(JOINER);
private static String getOperationId(ApiConfig apiConfig, ApiMethodConfig methodConfig, SwaggerContext context) {
return NamingContext.build(apiConfig, methodConfig).resolve(context.operationIdTemplate);
}

private static Property getSwaggerArrayProperty(TypeToken<?> typeToken) {
Expand Down Expand Up @@ -857,13 +848,18 @@ private static void checkExistingDefinition(String defName, OAuth2Definition new
}

public static class SwaggerContext {
public static final String DEFAULT_TAG_TEMPLATE = "${apiName}:${apiVersion}${.Resource}";
public static final String DEFAULT_OPERATION_ID_TEMPLATE = "${apiName}:${apiVersion}${.Resource}${.method}";

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 String apiName;
private String tagTemplate = DEFAULT_TAG_TEMPLATE;
private String operationIdTemplate = DEFAULT_OPERATION_ID_TEMPLATE;
private boolean addGoogleJsonErrorAsDefaultResponse;
private boolean addErrorCodesForServiceExceptions;
private boolean extractCommonParametersAsRefs;
Expand Down Expand Up @@ -904,6 +900,16 @@ public SwaggerContext setApiName(String apiName) {
return this;
}

public SwaggerContext setTagTemplate(String tagTemplate) {
this.tagTemplate = tagTemplate;
return this;
}

public SwaggerContext setOperationIdTemplate(String operationIdTemplate) {
this.operationIdTemplate = operationIdTemplate;
return this;
}

public SwaggerContext setAddGoogleJsonErrorAsDefaultResponse(boolean addGoogleJsonErrorAsDefaultResponse) {
this.addGoogleJsonErrorAsDefaultResponse = addGoogleJsonErrorAsDefaultResponse;
return this;
Expand All @@ -930,4 +936,58 @@ private static class GenerationContext {
private ApiConfigValidator validator;
private SchemaRepository schemata;
}

/**
* A template mechanism based on Apache Commons lang's StrSubstitutor (placeholder syntax is "${var}).
*
* The following variables are available on API and API method contexts:
* - apiName
* - apiVersion
* - resource (might be null for API or method context)
* - method (is null when working in a method context)
*
* Each variable comes with following variants:
* - Uppercased variants (if "${apiName}" is "myApi", "${ApiName}" will be "MyAPi"
* - Prefixed with "-",":" or "." (only chars that are safe for use in Swagger tags for Endpoints Portal)
* - Prefixed variants should be used for nullable vars: "${.resource}" will be empty if the resource var is null, but will be ".myResource" if resource is "myResource"
* - Prefixed variants also come in uppercased flavors ("${.Resource}" will be ".MyResource" if resource var is "myResource")
*/
private static class NamingContext {

private static final Converter<String, String> UPPER
= CaseFormat.LOWER_CAMEL.converterTo(CaseFormat.UPPER_CAMEL);
private final Map<String, String> values = new HashMap<>();
private final String prefixes;

private static NamingContext build(ApiConfig apiConfig, ApiMethodConfig methodConfig) {
String resource = apiConfig.getApiClassConfig().getResource();
String method = methodConfig != null ? methodConfig.getEndpointMethodName() : null;
return new NamingContext("-:.")
.put("apiName", apiConfig.getName())
.put("apiVersion", apiConfig.getVersion())
.put("resource", resource)
.put("method", method);
}

NamingContext(String prefixes) {
this.prefixes = prefixes;
}

NamingContext put(String key, String value) {
value = com.google.common.base.Strings.nullToEmpty(value);
values.put(key, value);
values.put(UPPER.convert(key), UPPER.convert(value));
for (char c : prefixes.toCharArray()) {
values.put(c + key, value.isEmpty() ? "" : c + value);
values.put(c + UPPER.convert(key), value.isEmpty() ? "" : c + UPPER.convert(value));
}
return this;
}

String resolve(String template) {
return new StrSubstitutor(values).replace(template);
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import com.google.api.server.spi.config.Transformer;
import com.google.api.server.spi.config.model.ApiConfig;
import com.google.api.server.spi.config.model.SchemaRepository;
import com.google.api.server.spi.config.model.Serializers;
import com.google.api.server.spi.testing.DefaultValueSerializer;
import com.google.api.server.spi.testing.DuplicateMethodEndpoint;
import com.google.api.server.spi.testing.PassAuthenticator;
Expand Down Expand Up @@ -238,12 +239,15 @@ final class TestSerializer extends DefaultValueSerializer<String, Boolean> {}
public void testMultipleSerializersInstalled() throws Exception {
// TODO: The generic component of Comparable causes validation to miss certain error
// cases like this.
@SuppressWarnings("rawtypes")
final class ComparableSerializer extends DefaultValueSerializer<Comparable<String>, Integer> {}
final class CharSequenceSerializer extends DefaultValueSerializer<CharSequence, Long> {}
config.getSerializationConfig().addSerializationConfig(ComparableSerializer.class);
config.getSerializationConfig().addSerializationConfig(CharSequenceSerializer.class);

List<Class<? extends Transformer<?, ?>>> serializerClasses = Serializers
.getSerializerClasses(TypeToken.of(String.class), config.getSerializationConfig());
assertThat(serializerClasses.size()).isEqualTo(2);

try {
validator.validate(config);
fail("Expected MultipleTransformersException.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,17 @@ public void testWriteSwagger_FooEndpoint() throws Exception {
Swagger expected = readExpectedAsSwagger("foo_endpoint.swagger");
checkSwagger(expected, swagger);
}

@Test
public void testWriteSwagger_FooEndpointCustomTemplates() throws Exception {
ApiConfig config = configLoader.loadConfiguration(ServiceContext.create(), FooEndpoint.class);
Swagger swagger = generator.writeSwagger(ImmutableList.of(config), new SwaggerContext()
.setTagTemplate("${ApiName}${ApiVersion}")
.setOperationIdTemplate("${apiName}-${apiVersion}-${method}")
);
Swagger expected = readExpectedAsSwagger("foo_endpoint_custom_templates.swagger");
checkSwagger(expected, swagger);
}

@Test
public void testWriteSwagger_FooEndpointParameterCombineParamSamePath() throws Exception {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"tags": [
"absolutepath:v1"
],
"operationId": "AbsolutepathV1AbsolutePath",
"operationId": "absolutepath:v1.absolutePath",
"responses": {
"204": {
"description": "A successful response"
Expand All @@ -39,7 +39,7 @@
"tags": [
"absolutepath:v1"
],
"operationId": "AbsolutepathV1CreateFoo",
"operationId": "absolutepath:v1.createFoo",
"responses": {
"200": {
"description": "A Foo response",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"tags": [
"absolutepath:v1"
],
"operationId": "AbsolutepathV1CreateFoo",
"operationId": "absolutepath:v1.createFoo",
"responses": {
"200": {
"description": "A Foo response",
Expand All @@ -42,7 +42,7 @@
"tags": [
"absolutepath:v1"
],
"operationId": "AbsolutepathV1AbsolutePath",
"operationId": "absolutepath:v1.absolutePath",
"responses": {
"204": {
"description": "A successful response"
Expand All @@ -55,7 +55,7 @@
"tags": [
"absolutepath:v1"
],
"operationId": "AbsolutepathV1AbsolutePath2",
"operationId": "absolutepath:v1.absolutePath2",
"responses": {
"204": {
"description": "A successful response"
Expand Down
Loading