/** * Copyright 2012, 2013 Turn, Inc. * * 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.turn.shapeshifter; import com.turn.shapeshifter.ShapeshifterProtos.JsonSchema; import com.turn.shapeshifter.ShapeshifterProtos.JsonType; import com.google.common.base.CaseFormat; import com.google.common.base.Preconditions; import com.google.protobuf.Descriptors; import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Descriptors.EnumValueDescriptor; import com.google.protobuf.Descriptors.FieldDescriptor; import com.google.protobuf.Descriptors.FieldDescriptor.Type; /** * {@code AutoSchema} instances are schemas of the simplest kind. They can be * used in contexts where the customization of the external representation of a * type is either not needed or undesirable. As such, the implementation of * such a schema's {@link Parser} and {@link Serializer} are lean and fast. * * <p>Default conversions and conventions still apply for the {@link Parser} and * {@link Serializer} returned by an instance of this class. Namely: * * <ul> * <li>Field names are converted between Protocol Buffer * {@code lower_underscore} to JSON's {@code camelCase}. * <li>Enum names are converted between {@code PROTO_FORMAT} and * {@code jsonFormat}. * <li>Empty arrays and objects without properties are ignored * </ul> * * <p>An instance of {@code AutoSchema} is anonymous and defined uniquely by * the {@link Descriptor} from which it was instantiated. Since * Protocol Buffer message descriptors are immutable, instances of this class * are prime candidates for being cached throughout the lifetime of an * application. * * <p>This class takes its name from its use within the {@code Shapeshifter} * library, where instances of this class are generated on the fly as other * schema types find the need to handle types for which no schema was defined * by the user. * * <p>Instances of this class recursively build schemas for all message types * referred from the root descriptor. This is impossible to achieve when a loop * occurs in the descriptors. Assuming an instance of this class is built with * the descriptor for a message named ProtoA, an exception will be thrown when: * * <ul> * <li>ProtoA contains a field of type ProtoA * <li>ProtoA contains a field a type ProtoB, and ProtoB contains a field of * type ProtoA. * <li>More generally when there exists a chain of fields between ProtoA and * ProtoX, and ProtoX refers to any message type that is part of that chain, * including ProtoA and ProtoX themselves. * </ul> * * @author jsilland */ public class AutoSchema implements Schema { // This is the presumed case format for the values of an enum // defined in a protocol buffer. static final CaseFormat PROTO_ENUM_CASE_FORMAT = CaseFormat.UPPER_UNDERSCORE; static final CaseFormat JSON_ENUM_CASE_FORMAT = CaseFormat.LOWER_CAMEL; static final CaseFormat PROTO_FIELD_CASE_FORMAT = CaseFormat.LOWER_UNDERSCORE; static final CaseFormat JSON_FIELD_CASE_FORMAT = CaseFormat.LOWER_CAMEL; private final Descriptors.Descriptor descriptor; private final Parser parser; private final Serializer serializer; /** * Creates a new instance of this class that will use the given descriptor. */ public static AutoSchema of(Descriptors.Descriptor descriptor) { return new AutoSchema(Preconditions.checkNotNull(descriptor)); } /** * Package-private exhaustive constructor. * * @param descriptor the descriptor */ AutoSchema(Descriptor descriptor) { this.descriptor = descriptor; Preconditions.checkArgument(!isDescriptorLooping(descriptor), "Auto-schemas cannot describe types that have self-references"); this.parser = new AutoParser(descriptor); this.serializer = new AutoSerializer(descriptor); } /** * {@inheritDoc} */ @Override public Serializer getSerializer() { return serializer; } /** * {@inheritDoc} */ @Override public Parser getParser() { return parser; } /** * {@inheritDoc} */ @Override public JsonSchema getJsonSchema(ReadableSchemaRegistry registry) throws JsonSchemaException { JsonSchema.Builder jsonSchemaBuilder = JsonSchema.newBuilder(); jsonSchemaBuilder.setType(JsonType.OBJECT); for (FieldDescriptor field : descriptor.getFields()) { JsonSchema.Builder property = JsonSchema.newBuilder(); property.setName(PROTO_FIELD_CASE_FORMAT.to(JSON_FIELD_CASE_FORMAT, field.getName())); if (field.hasDefaultValue()) { if (field.getType().equals(Type.ENUM)) { EnumValueDescriptor defaultValue = (EnumValueDescriptor) field.getDefaultValue(); property.setDefault(PROTO_ENUM_CASE_FORMAT.to( JSON_ENUM_CASE_FORMAT, defaultValue.getName())); } else { property.setDefault(field.getDefaultValue().toString()); } } if (field.isRequired()) { property.setRequired(true); } if (field.isRepeated()) { property.setType(JsonType.ARRAY); if (field.getType().equals(Type.MESSAGE)) { try { property.setItems( registry.get(field.getMessageType()).getJsonSchema(registry)); } catch (SchemaObtentionException soe) { throw new JsonSchemaException(soe); } } else { property.setItems( JsonSchema.newBuilder().setType(getFieldType(field))); if (field.getType().equals(Type.ENUM)) { for (EnumValueDescriptor enumValue : field.getEnumType().getValues()) { property.addEnum(PROTO_ENUM_CASE_FORMAT.to( JSON_ENUM_CASE_FORMAT, enumValue.getName())); } } } } else { if (field.getType().equals(Type.MESSAGE)) { try { property.mergeFrom(registry.get(field.getMessageType()) .getJsonSchema(registry)); } catch (SchemaObtentionException soe) { throw new JsonSchemaException(soe); } } else { property.setType(getFieldType(field)); if (field.getType().equals(Type.ENUM)) { for (EnumValueDescriptor enumValue : field.getEnumType().getValues()) { property.addEnum(PROTO_ENUM_CASE_FORMAT.to( JSON_ENUM_CASE_FORMAT, enumValue.getName())); } } } } jsonSchemaBuilder.addProperties(property); } return jsonSchemaBuilder.build(); } /** * {@inheritDoc} */ @Override public Descriptor getDescriptor() { return descriptor; } /** * Returns the reified jon type of the given protocol buffer field. * * @param field the field to determine the JSON type of */ public static ShapeshifterProtos.JsonType getReifiedFieldType(FieldDescriptor field) { Preconditions.checkNotNull(field); return field.isRepeated() ? JsonType.ARRAY : getFieldType(field); } /** * Returns the JSON schema type of a given protocol buffer field. * * @param field the field to obtain the JSON type for */ private static ShapeshifterProtos.JsonType getFieldType(FieldDescriptor field) { Preconditions.checkNotNull(field); switch (field.getJavaType()) { case BOOLEAN: return JsonType.BOOLEAN; case BYTE_STRING: case STRING: case ENUM: return JsonType.STRING; case DOUBLE: case FLOAT: return JsonType.NUMBER; case INT: case LONG: return JsonType.INTEGER; case MESSAGE: return JsonType.OBJECT; default: break; } throw new IllegalStateException(); } static boolean isDescriptorLooping(Descriptors.Descriptor descriptor) { return ProtoDescriptorGraph.of(descriptor).isLooping(); } }