Skip to content
This repository was archived by the owner on Jun 30, 2023. It is now read-only.

Add description on resource fields #132

Merged
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 @@ -29,6 +29,12 @@
*/
String name() default "";

/**
* The description that the property represented by the annotated getter, setter, or field should appear
* as in the API.
*/
String description() default "";

/**
* Whether or not the property represented by the annotated getter, setter or field should be
* ignored for the API.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
*/
public class ResourcePropertySchema {
private final TypeToken<?> type;
private String description;

private ResourcePropertySchema(TypeToken<?> type) {
this.type = type;
Expand All @@ -48,6 +49,14 @@ public TypeToken<?> getType() {
return type;
}

public String getDescription() {
return description;
}

public void setDescription(String description) {
this.description = description;
}

/**
* Returns a default resource property schema for a given type.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,19 @@ public PropertyName findNameForSerialization(Annotated a) {

@Override
public PropertyName findNameForDeserialization(Annotated a) {
ApiResourceProperty apiName = a.getAnnotation(ApiResourceProperty.class);
if (apiName != null && apiName.ignored() != AnnotationBoolean.TRUE) {
return PropertyName.construct(apiName.name());
}
return null;
ApiResourceProperty annotation = findAnnotation(a);
return annotation != null ? PropertyName.construct(annotation.name()) : null;
}

@Override
public String findPropertyDescription(Annotated a) {
ApiResourceProperty annotation = findAnnotation(a);
return annotation != null ? annotation.description() : null;
}

private ApiResourceProperty findAnnotation(Annotated a) {
ApiResourceProperty annotation = a.getAnnotation(ApiResourceProperty.class);
return annotation != null && annotation.ignored() != AnnotationBoolean.TRUE ? annotation : null;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ public ResourceSchema getResourceSchema(TypeToken<?> type, ApiConfig config) {
continue;
}
if (propertyType != null) {
schemaBuilder.addProperty(name, ResourcePropertySchema.of(propertyType));
ResourcePropertySchema propertySchema = ResourcePropertySchema.of(propertyType);
propertySchema.setDescription(definition.getMetadata().getDescription());
schemaBuilder.addProperty(name, propertySchema);
} else {
logger.warning("No type found for property '" + name + "' on class '" + type + "'.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public abstract class Schema {
/** The name of the schema. */
public abstract String name();
public abstract String type();
@Nullable public abstract String description();

/** A map from field names to fields for the schema. */
public abstract ImmutableSortedMap<String, Field> fields();
Expand Down Expand Up @@ -44,6 +45,7 @@ public abstract static class Builder {

public abstract Builder setName(String name);
public abstract Builder setType(String type);
@Nullable public abstract Builder setDescription(String description);
public abstract Builder setFields(ImmutableSortedMap<String, Field> fields);
public Builder addField(String name, Field field) {
fieldsBuilder.put(name, field);
Expand Down Expand Up @@ -76,6 +78,8 @@ public static abstract class Field {
/** The type classification of the field. */
public abstract FieldType type();

@Nullable public abstract String description();

/**
* If {@link #type()} is {@link FieldType#OBJECT}, a reference to the schema type that the field
* refers to.
Expand All @@ -97,6 +101,7 @@ public static Builder builder() {
public abstract static class Builder {
public abstract Builder setName(String name);
public abstract Builder setType(FieldType type);
@Nullable public abstract Builder setDescription(String description);
public abstract Builder setSchemaReference(SchemaReference ref);
public abstract Builder setArrayItemSchema(Field schema);
public abstract Field build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ private Schema getOrCreateTypeForConfig(
throw new IllegalArgumentException("Can't add a primitive type as a resource");
} else if (arrayItemType != null) {
Field.Builder arrayItemSchema = Field.builder().setName(ARRAY_UNUSED_MSG);
fillInFieldInformation(arrayItemSchema, arrayItemType, typesForConfig, config);
fillInFieldInformation(arrayItemSchema, arrayItemType, null, typesForConfig, config);
schema = Schema.builder()
.setName(Types.getSimpleName(type, config.getSerializationConfig()))
.setType("object")
Expand Down Expand Up @@ -192,20 +192,22 @@ private Schema createBeanSchema(
ResourceSchema schema = resourceSchemaProvider.getResourceSchema(type, config);
for (Entry<String, ResourcePropertySchema> entry : schema.getProperties().entrySet()) {
String propertyName = entry.getKey();
TypeToken<?> propertyType = entry.getValue().getType();
ResourcePropertySchema propertySchema = entry.getValue();
TypeToken<?> propertyType = propertySchema.getType();
if (propertyType != null) {
Field.Builder fieldBuilder = Field.builder().setName(propertyName);
fillInFieldInformation(fieldBuilder, propertyType, typesForConfig, config);
fillInFieldInformation(fieldBuilder, propertyType, propertySchema.getDescription(), typesForConfig, config);
builder.addField(propertyName, fieldBuilder.build());
}
}
return builder.build();
}

private void fillInFieldInformation(Field.Builder builder, TypeToken<?> fieldType,
Map<TypeToken<?>, Schema> typesForConfig, ApiConfig config) {
String description, Map<TypeToken<?>, Schema> typesForConfig, ApiConfig config) {
FieldType ft = FieldType.fromType(fieldType);
builder.setType(ft);
builder.setDescription(description);
if (ft == FieldType.OBJECT || ft == FieldType.ENUM) {
getOrCreateTypeForConfig(fieldType, typesForConfig, config);
builder.setSchemaReference(SchemaReference.create(this, config, fieldType));
Expand All @@ -214,6 +216,7 @@ private void fillInFieldInformation(Field.Builder builder, TypeToken<?> fieldTyp
fillInFieldInformation(
arrayItemBuilder,
ApiAnnotationIntrospector.getSchemaType(Types.getArrayItemType(fieldType), config),
null,
typesForConfig,
config);
builder.setArrayItemSchema(arrayItemBuilder.build());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ private JsonSchema convertToDiscoverySchema(Schema schema) {
}
docSchema.setProperties(fields);
}
docSchema.setDescription(schema.description());
if (!schema.enumValues().isEmpty()) {
docSchema.setEnum(new ArrayList<>(schema.enumValues()));
docSchema.setEnumDescriptions(new ArrayList<>(schema.enumDescriptions()));
Expand All @@ -238,6 +239,7 @@ private JsonSchema convertToDiscoverySchema(Field f) {
}
JsonSchema fieldSchema = new JsonSchema()
.setType(f.type().getDiscoveryType())
.setDescription(f.description())
.setFormat(f.type().getDiscoveryFormat());
if (f.type() == FieldType.ARRAY) {
fieldSchema.setItems(convertToDiscoverySchema(f.arrayItemSchema()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
*/
package com.google.api.server.spi.swagger;

import com.google.api.server.spi.Constant;
import com.google.api.server.spi.EndpointMethod;
import com.google.api.server.spi.Strings;
import com.google.api.server.spi.TypeLoader;
Expand Down Expand Up @@ -149,19 +148,19 @@ public class SwaggerGenerator {
.put(DateAndTime.class, "date-time")
.put(Date.class, "date-time")
.build();
private static final ImmutableMap<FieldType, Property> FIELD_TYPE_TO_PROPERTY_MAP =
ImmutableMap.<FieldType, Property>builder()
.put(FieldType.BOOLEAN, new BooleanProperty())
.put(FieldType.BYTE_STRING, new ByteArrayProperty())
.put(FieldType.DATE, new DateProperty())
.put(FieldType.DATE_TIME, new DateTimeProperty())
.put(FieldType.DOUBLE, new DoubleProperty())
.put(FieldType.FLOAT, new FloatProperty())
.put(FieldType.INT8, new IntegerProperty())
.put(FieldType.INT16, new IntegerProperty())
.put(FieldType.INT32, new IntegerProperty())
.put(FieldType.INT64, new LongProperty())
.put(FieldType.STRING, new StringProperty())
private static final ImmutableMap<FieldType, Class<? extends Property>> FIELD_TYPE_TO_PROPERTY_CLASS_MAP =
ImmutableMap.<FieldType, Class<? extends Property>>builder()
.put(FieldType.BOOLEAN, BooleanProperty.class)
.put(FieldType.BYTE_STRING, ByteArrayProperty.class)
.put(FieldType.DATE, DateProperty.class)
.put(FieldType.DATE_TIME, DateTimeProperty.class)
.put(FieldType.DOUBLE, DoubleProperty.class)
.put(FieldType.FLOAT, FloatProperty.class)
.put(FieldType.INT8, IntegerProperty.class)
.put(FieldType.INT16, IntegerProperty.class)
.put(FieldType.INT32, IntegerProperty.class)
.put(FieldType.INT64, LongProperty.class)
.put(FieldType.STRING, StringProperty.class)
.build();

private static final Function<ApiConfig, ApiKey> CONFIG_TO_ROOTLESS_KEY =
Expand Down Expand Up @@ -420,15 +419,26 @@ private Model convertToSwaggerSchema(Schema schema) {
}

private Property convertToSwaggerProperty(Field f) {
Property p = FIELD_TYPE_TO_PROPERTY_MAP.get(f.type());
if (p != null) {
return p;
} else if (f.type() == FieldType.OBJECT || f.type() == FieldType.ENUM) {
return new RefProperty(f.schemaReference().get().name());
} else if (f.type() == FieldType.ARRAY) {
return new ArrayProperty(convertToSwaggerProperty(f.arrayItemSchema()));
Property p = null;
Class<? extends Property> propertyClass = FIELD_TYPE_TO_PROPERTY_CLASS_MAP.get(f.type());
if (propertyClass != null) {
try {
p = propertyClass.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
//cannot happen, as Property subclasses are guaranteed to have a default constructor
}
} else {
if (f.type() == FieldType.OBJECT || f.type() == FieldType.ENUM) {
p = new RefProperty(f.schemaReference().get().name());
} else if (f.type() == FieldType.ARRAY) {
p = new ArrayProperty(convertToSwaggerProperty(f.arrayItemSchema()));
}
}
if (p == null) {
throw new IllegalArgumentException("could not convert field " + f);
}
throw new IllegalArgumentException("could not convert field " + f);
p.description(f.description());
return p;
}

private static String getOperationId(ApiConfig apiConfig, ApiMethodConfig methodConfig) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ public void testRenamedProperty() {
assertThat(schema.getProperties().keySet()).containsExactly("bar");
}

@Test
public void testDescribedProperty() {
ResourceSchema schema = getResourceSchema(DescribedPropertyBean.class);
assertEquals("description of foo", schema.getProperties().get("foo").getDescription());
assertEquals("description of bar", schema.getProperties().get("bar").getDescription());
}

@Test
public void testMissingPropertyType() {
ResourceSchema schema = getResourceSchema(MissingPropertyTypeBean.class);
Expand Down Expand Up @@ -183,6 +190,15 @@ public String getFoo() {
}
}

private static class DescribedPropertyBean {
@ApiResourceProperty(description = "description of foo")
private String foo;
@ApiResourceProperty(description = "description of bar")
public String getBar() {
return null;
}
}

/**
* A JavaBean that has a JsonProperty, but no supporting JavaBean property to access it.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import com.google.api.server.spi.testing.ArrayEndpoint;
import com.google.api.server.spi.testing.EnumEndpoint;
import com.google.api.server.spi.testing.EnumEndpointV2;
import com.google.api.server.spi.testing.FooDescriptionEndpoint;
import com.google.api.server.spi.testing.FooEndpoint;
import com.google.api.server.spi.testing.MultipleParameterEndpoint;
import com.google.api.server.spi.testing.NamespaceEndpoint;
Expand Down Expand Up @@ -152,6 +153,13 @@ public void testWriteDiscovery_AbsoluteCommonPathEndpoint() throws Exception {
compareDiscovery(expected, doc);
}

@Test
public void testWriteDiscovery_FooEndpointWithDescription() throws Exception {
RestDescription doc = getDiscovery(context, FooDescriptionEndpoint.class);
RestDescription expected = readExpectedAsDiscovery("foo_with_description_endpoint.json");
compareDiscovery(expected, doc);
}

@Test
public void testWriteDiscovery_multipleApisWithSharedSchema() throws Exception {
// Read in an API that uses a resource with fields that have their own schema, then read in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
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.FooDescriptionEndpoint;
import com.google.api.server.spi.testing.FooEndpoint;
import com.google.api.server.spi.testing.LimitMetricsEndpoint;
import com.google.common.collect.ImmutableList;
Expand Down Expand Up @@ -162,6 +163,14 @@ public void testWriteSwagger_LimitMetricsEndpoint() throws Exception {
compareSwagger(expected, swagger);
}

@Test
public void testWriteSwagger_FooEndpointWithDescription() throws Exception {
ApiConfig config = configLoader.loadConfiguration(ServiceContext.create(), FooDescriptionEndpoint.class);
Swagger swagger = generator.writeSwagger(ImmutableList.of(config), false, context);
Swagger expected = readExpectedAsSwagger("foo_with_description_endpoint.swagger");
compareSwagger(expected, swagger);
}

private Swagger getSwagger(Class<?> serviceClass, SwaggerContext context, boolean internal)
throws Exception {
ApiConfig config = configLoader.loadConfiguration(ServiceContext.create(), serviceClass);
Expand Down
Loading