Skip to content

Commit bbb1eff

Browse files
author
Clément Denis
committed
Improve body name and description
- Allow @description on resources classes and body method params - Add default description on arrays and maps - Use the resource name for body params instead of "body" - Mark request body always required (it actually is in Cloud Endpoints)
1 parent 77e6405 commit bbb1eff

7 files changed

Lines changed: 104 additions & 64 deletions

File tree

endpoints-framework/src/main/java/com/google/api/server/spi/config/Description.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,17 @@
2121
import java.lang.annotation.Target;
2222

2323
/**
24-
* Annotation to specify the description of an API parameter or enum constants.
24+
* Annotation to specify the description of a method parameter, enum type, enum constant or
25+
* resource (type used as request body).
26+
* If used on an enum type, the description will only be used for the Discovery format (OpenAPI
27+
* will inline enum values, as the semantics is poorer).
2528
* The description will be ignored if the annotation is used on resource fields.
2629
*/
27-
@Target({ElementType.PARAMETER, ElementType.FIELD})
30+
@Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.TYPE})
2831
@Retention(RetentionPolicy.RUNTIME)
2932
public @interface Description {
3033
/**
31-
* The parameter description.
34+
* A description.
3235
*/
3336
String value() default "";
3437
}

endpoints-framework/src/main/java/com/google/api/server/spi/config/model/SchemaRepository.java

Lines changed: 82 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@
22

33
import com.google.api.client.util.Maps;
44
import com.google.api.server.spi.TypeLoader;
5+
import com.google.api.server.spi.config.Description;
56
import com.google.api.server.spi.config.ResourcePropertySchema;
67
import com.google.api.server.spi.config.ResourceSchema;
78
import com.google.api.server.spi.config.annotationreader.ApiAnnotationIntrospector;
89
import com.google.api.server.spi.config.jsonwriter.JacksonResourceSchemaProvider;
910
import com.google.api.server.spi.config.jsonwriter.ResourceSchemaProvider;
11+
import com.google.api.server.spi.config.model.Schema.Builder;
1012
import com.google.api.server.spi.config.model.Schema.Field;
1113
import com.google.api.server.spi.config.model.Schema.SchemaReference;
1214
import com.google.common.annotations.VisibleForTesting;
1315
import com.google.common.base.Optional;
16+
import com.google.common.base.Strings;
1417
import com.google.common.collect.ImmutableList;
1518
import com.google.common.collect.LinkedHashMultimap;
1619
import com.google.common.collect.Multimap;
@@ -121,7 +124,7 @@ private Map<TypeToken<?>, Schema> getAllTypesForConfig(ApiConfig config) {
121124
}
122125

123126
private Schema getOrCreateTypeForConfig(
124-
TypeToken type, Map<TypeToken<?>, Schema> typesForConfig, ApiConfig config) {
127+
TypeToken<?> type, Map<TypeToken<?>, Schema> typesForConfig, ApiConfig config) {
125128
type = ApiAnnotationIntrospector.getSchemaType(type, config);
126129
Schema schema = typesForConfig.get(type);
127130
ApiKey key = config.getApiKey().withoutRoot();
@@ -139,54 +142,64 @@ private Schema getOrCreateTypeForConfig(
139142
TypeToken<?> arrayItemType = Types.getArrayItemType(type);
140143
if (typeLoader.isSchemaType(type)) {
141144
throw new IllegalArgumentException("Can't add a primitive type as a resource");
142-
} else if (arrayItemType != null) {
143-
Field.Builder arrayItemSchema = Field.builder().setName(ARRAY_UNUSED_MSG);
144-
fillInFieldInformation(arrayItemSchema, arrayItemType, null, typesForConfig, config);
145-
schema = Schema.builder()
146-
.setName(Types.getSimpleName(type, config.getSerializationConfig()))
147-
.setType("object")
148-
.addField("items", Field.builder()
149-
.setName("items")
150-
.setType(FieldType.ARRAY)
151-
.setArrayItemSchema(arrayItemSchema.build())
152-
.build())
153-
.build();
154-
typesForConfig.put(type, schema);
155-
schemaByApiKeys.put(key, schema);
156-
return schema;
157-
} else if (Types.isObject(type)) {
158-
typesForConfig.put(type, ANY_SCHEMA);
159-
schemaByApiKeys.put(key, ANY_SCHEMA);
160-
return ANY_SCHEMA;
161-
} else if (Types.isMapType(type)) {
162-
schema = MAP_SCHEMA;
163-
final TypeToken<Map<?, ?>> mapSupertype = type.getSupertype(Map.class);
164-
final boolean hasConcreteKeyValue = Types.isConcreteType(mapSupertype.getType());
165-
boolean forceJsonMapSchema = EndpointsFlag.MAP_SCHEMA_FORCE_JSON_MAP_SCHEMA.isEnabled();
166-
if (hasConcreteKeyValue && !forceJsonMapSchema) {
167-
schema = createMapSchema(mapSupertype, typesForConfig, config).or(schema);
168-
}
169-
typesForConfig.put(type, schema);
170-
schemaByApiKeys.put(key, schema);
171-
return schema;
172-
} else if (Types.isEnumType(type)) {
173-
Map<String, String> valuesAndDescriptions = Types.getEnumValuesAndDescriptions(type);
174-
Schema.Builder builder = Schema.builder()
175-
.setName(Types.getSimpleName(type, config.getSerializationConfig()))
176-
.setType("string");
177-
for (Entry<String, String> entry : valuesAndDescriptions.entrySet()) {
178-
builder.addEnumValue(entry.getKey());
179-
builder.addEnumDescription(entry.getValue());
180-
}
181-
schema = builder.build();
182-
typesForConfig.put(type, schema);
183-
schemaByApiKeys.put(key, schema);
184-
return schema;
185145
} else {
186-
schema = createBeanSchema(type, typesForConfig, config);
187-
typesForConfig.put(type, schema);
188-
schemaByApiKeys.put(key, schema);
189-
return schema;
146+
String simpleName = Types.getSimpleName(type, config.getSerializationConfig());
147+
if (arrayItemType != null) {
148+
Field.Builder arrayItemSchema = Field.builder().setName(ARRAY_UNUSED_MSG);
149+
fillInFieldInformation(arrayItemSchema, arrayItemType, typesForConfig, config);
150+
Field arrayField = arrayItemSchema.build();
151+
Builder builder = Schema.builder()
152+
.setName(simpleName)
153+
.setType("object")
154+
.addField("items", Field.builder()
155+
.setName("items")
156+
.setType(FieldType.ARRAY)
157+
.setArrayItemSchema(arrayField)
158+
.build());
159+
SchemaReference itemSchema = arrayField.schemaReference();
160+
if (itemSchema != null) {
161+
builder.setDescription("An ordered list of " + itemSchema.get().name());
162+
}
163+
schema = builder.build();
164+
typesForConfig.put(type, schema);
165+
schemaByApiKeys.put(key, schema);
166+
return schema;
167+
} else if (Types.isObject(type)) {
168+
typesForConfig.put(type, ANY_SCHEMA);
169+
schemaByApiKeys.put(key, ANY_SCHEMA);
170+
return ANY_SCHEMA;
171+
} else if (Types.isMapType(type)) {
172+
schema = MAP_SCHEMA;
173+
final TypeToken<Map<?, ?>> mapSupertype = ((TypeToken) type).getSupertype(Map.class);
174+
final boolean hasConcreteKeyValue = Types.isConcreteType(mapSupertype.getType());
175+
boolean forceJsonMapSchema = EndpointsFlag.MAP_SCHEMA_FORCE_JSON_MAP_SCHEMA.isEnabled();
176+
if (hasConcreteKeyValue && !forceJsonMapSchema) {
177+
schema = createMapSchema(mapSupertype, typesForConfig, config).or(schema);
178+
}
179+
typesForConfig.put(type, schema);
180+
schemaByApiKeys.put(key, schema);
181+
return schema;
182+
} else if (Types.isEnumType(type)) {
183+
Map<String, String> valuesAndDescriptions
184+
= Types.getEnumValuesAndDescriptions((TypeToken<Enum<?>>) type);
185+
Schema.Builder builder = Schema.builder()
186+
.setName(simpleName)
187+
.setType("string");
188+
setSchemaDescription(type, builder);
189+
for (Entry<String, String> entry : valuesAndDescriptions.entrySet()) {
190+
builder.addEnumValue(entry.getKey());
191+
builder.addEnumDescription(entry.getValue());
192+
}
193+
schema = builder.build();
194+
typesForConfig.put(type, schema);
195+
schemaByApiKeys.put(key, schema);
196+
return schema;
197+
} else {
198+
schema = createBeanSchema(type, typesForConfig, config);
199+
typesForConfig.put(type, schema);
200+
schemaByApiKeys.put(key, schema);
201+
return schema;
202+
}
190203
}
191204
}
192205

@@ -237,35 +250,42 @@ private Optional<Schema> createMapSchema(
237250
.setName(Types.getSimpleName(mapType, config.getSerializationConfig()))
238251
.setType("object");
239252
Field.Builder fieldBuilder = Field.builder().setName(MAP_UNUSED_MSG);
240-
fillInFieldInformation(fieldBuilder, valueSchemaType, null, typesForConfig, config);
241-
return Optional.of(builder.setMapValueSchema(fieldBuilder.build()).build());
253+
fillInFieldInformation(fieldBuilder, valueSchemaType, typesForConfig, config);
254+
Field mapValueField = fieldBuilder.build();
255+
SchemaReference valueSchema = mapValueField.schemaReference();
256+
if (valueSchema != null) {
257+
builder.setDescription(
258+
String.format("A collection of name / %s pairs", valueSchema.get().name()));
259+
}
260+
return Optional.of(builder.setMapValueSchema(mapValueField).build());
242261
}
243262

244263
private Schema createBeanSchema(
245264
TypeToken<?> type, Map<TypeToken<?>, Schema> typesForConfig, ApiConfig config) {
246265
Schema.Builder builder = Schema.builder()
247266
.setName(Types.getSimpleName(type, config.getSerializationConfig()))
248267
.setType("object");
268+
setSchemaDescription(type, builder);
249269
ResourceSchema schema = resourceSchemaProvider.getResourceSchema(type, config);
250270
for (Entry<String, ResourcePropertySchema> entry : schema.getProperties().entrySet()) {
251271
String propertyName = entry.getKey();
252272
ResourcePropertySchema propertySchema = entry.getValue();
253273
TypeToken<?> propertyType = propertySchema.getType();
254274
if (propertyType != null) {
255-
Field.Builder fieldBuilder = Field.builder().setName(propertyName);
256-
fillInFieldInformation(fieldBuilder, propertyType, propertySchema.getDescription(),
257-
typesForConfig, config);
275+
Field.Builder fieldBuilder = Field.builder()
276+
.setName(propertyName)
277+
.setDescription(propertySchema.getDescription());
278+
fillInFieldInformation(fieldBuilder, propertyType, typesForConfig, config);
258279
builder.addField(propertyName, fieldBuilder.build());
259280
}
260281
}
261282
return builder.build();
262283
}
263284

264285
private void fillInFieldInformation(Field.Builder builder, TypeToken<?> fieldType,
265-
String description, Map<TypeToken<?>, Schema> typesForConfig, ApiConfig config) {
286+
Map<TypeToken<?>, Schema> typesForConfig, ApiConfig config) {
266287
FieldType ft = FieldType.fromType(fieldType);
267288
builder.setType(ft);
268-
builder.setDescription(description);
269289
if (ft == FieldType.OBJECT || ft == FieldType.ENUM) {
270290
getOrCreateTypeForConfig(fieldType, typesForConfig, config);
271291
builder.setSchemaReference(SchemaReference.create(this, config, fieldType));
@@ -274,12 +294,18 @@ private void fillInFieldInformation(Field.Builder builder, TypeToken<?> fieldTyp
274294
fillInFieldInformation(
275295
arrayItemBuilder,
276296
ApiAnnotationIntrospector.getSchemaType(Types.getArrayItemType(fieldType), config),
277-
null,
278297
typesForConfig,
279298
config);
280299
builder.setArrayItemSchema(arrayItemBuilder.build());
281300
}
282301
}
302+
303+
private void setSchemaDescription(TypeToken<?> type, Builder builder) {
304+
Description description = type.getRawType().getAnnotation(Description.class);
305+
if (description != null && !Strings.isNullOrEmpty(description.value())) {
306+
builder.setDescription(description.value());
307+
}
308+
}
283309

284310
public static boolean isJsonMapSchema(Schema schema) {
285311
return schema == MAP_SCHEMA;

endpoints-framework/src/main/java/com/google/api/server/spi/swagger/SwaggerGenerator.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -469,9 +469,11 @@ private void writeApiMethod(ApiMethodConfig methodConfig, ApiConfig apiConfig, S
469469
case RESOURCE:
470470
TypeToken<?> requestType = parameterConfig.getSchemaBaseType();
471471
Schema schema = genCtx.schemata.getOrAdd(requestType, apiConfig);
472-
BodyParameter bodyParameter = new BodyParameter();
473-
bodyParameter.setName("body");
474-
bodyParameter.setSchema(getSchema(schema));
472+
BodyParameter bodyParameter = new BodyParameter()
473+
.name(schema.name())
474+
.description(parameterConfig.getDescription())
475+
.schema(getSchema(schema));
476+
bodyParameter.setRequired(true);
475477
operation.addParameter(bodyParameter);
476478
break;
477479
case UNKNOWN:
@@ -608,7 +610,10 @@ private void addDefinedMetricCosts(Map<String, ApiLimitMetricConfig> limitMetric
608610

609611
private Model convertToSwaggerSchema(Schema schema) {
610612
ModelImpl docSchema = new ModelImpl().type("object");
611-
Map<String, Property> fields = new TreeMap<>();
613+
String description = schema.description();
614+
if (!Strings.isEmptyOrWhitespace(description)) {
615+
docSchema.description(description);
616+
}
612617
if (!schema.fields().isEmpty()) {
613618
for (Field f : schema.fields().values()) {
614619
fields.put(f.name(), convertToSwaggerProperty(f));

endpoints-framework/src/test/java/com/google/api/server/spi/config/model/SchemaRepositoryTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ public void getOrAdd_mapType() throws Exception {
9797
Schema schema = repo.getOrAdd(methodConfig.getReturnType(), config);
9898
assertThat(schema).isEqualTo(Schema.builder()
9999
.setName("Map_String_TestEnum")
100+
.setDescription("A collection of name / TestEnum pairs")
100101
.setType("object")
101102
.setMapValueSchema(Field.builder()
102103
.setName(SchemaRepository.MAP_UNUSED_MSG)
@@ -138,6 +139,7 @@ public void getOrAdd_NestedMap() throws Exception {
138139
Schema expectedSchema = Schema.builder()
139140
.setName("Map_String_Map_String_String")
140141
.setType("object")
142+
.setDescription("A collection of name / Map_String_String pairs")
141143
.setMapValueSchema(Field.builder()
142144
.setName(SchemaRepository.MAP_UNUSED_MSG)
143145
.setType(FieldType.OBJECT)

test-utils/src/main/java/com/google/api/server/spi/testing/FooDescription.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616
package com.google.api.server.spi.testing;
1717

1818
import com.google.api.server.spi.config.ApiResourceProperty;
19+
import com.google.api.server.spi.config.Description;
1920

2021
/**
2122
* Test resource type with descriptions.
2223
*/
24+
@Description("Description at class level")
2325
public class FooDescription {
2426

2527
@ApiResourceProperty(description = "description of name")

test-utils/src/main/java/com/google/api/server/spi/testing/FooDescriptionEndpoint.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
public class FooDescriptionEndpoint {
3333
@ApiMethod(name = "foo.create", description = "create desc", path = "foos/{id}",
3434
httpMethod = HttpMethod.PUT)
35-
public FooDescription createFoo(@Named("id") @Description("id desc") String id, FooDescription foo) {
35+
public FooDescription createFoo(@Named("id") @Description("id desc") String id,
36+
@Description("Description at method parameter level") FooDescription foo) {
3637
return null;
3738
}
3839
@ApiMethod(name = "foo.get", description = "get desc", path = "foos/{id}",

test-utils/src/main/java/com/google/api/server/spi/testing/TestEnumDescription.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import com.google.api.server.spi.config.Description;
1919

20+
@Description("A list of enum values")
2021
public enum TestEnumDescription {
2122
@Description("description of value1")
2223
VALUE1,

0 commit comments

Comments
 (0)