com.squareup.wire.schema.internal.parser.ProtoParser Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of wire-schema Show documentation
Show all versions of wire-schema Show documentation
gRPC and protocol buffers for Android, Kotlin, and Java.
/*
* Copyright (C) 2013 Square, 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.squareup.wire.schema.internal.parser;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Range;
import com.squareup.wire.schema.Field;
import com.squareup.wire.schema.Location;
import com.squareup.wire.schema.ProtoFile;
import com.squareup.wire.schema.internal.Util;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/** Basic parser for {@code .proto} schema declarations. */
public final class ProtoParser {
private final SyntaxReader reader;
private final Location location;
/** Parse a named {@code .proto} schema. */
public static ProtoFileElement parse(Location location, String data) {
return new ProtoParser(location, data.toCharArray()).readProtoFile();
}
private final ImmutableList.Builder publicImports = ImmutableList.builder();
private final ImmutableList.Builder imports = ImmutableList.builder();
private final ImmutableList.Builder nestedTypes = ImmutableList.builder();
private final ImmutableList.Builder services = ImmutableList.builder();
private final ImmutableList.Builder extendsList = ImmutableList.builder();
private final ImmutableList.Builder options = ImmutableList.builder();
/** The number of declarations defined in the current file. */
private int declarationCount = 0;
/** The syntax of the file, or null if none is defined. */
private ProtoFile.Syntax syntax;
/** Output package name, or null if none yet encountered. */
private String packageName;
/** The current package name + nested type names, separated by dots. */
private String prefix = "";
ProtoParser(Location location, char[] data) {
this.reader = new SyntaxReader(data, location);
this.location = location;
}
ProtoFileElement readProtoFile() {
while (true) {
String documentation = reader.readDocumentation();
if (reader.exhausted()) {
return new ProtoFileElement(
location,
packageName,
syntax,
imports.build(),
publicImports.build(),
nestedTypes.build(),
services.build(),
extendsList.build(),
options.build()
);
}
Object declaration = readDeclaration(documentation, Context.FILE);
if (declaration instanceof TypeElement) {
nestedTypes.add((TypeElement) declaration);
} else if (declaration instanceof ServiceElement) {
services.add((ServiceElement) declaration);
} else if (declaration instanceof OptionElement) {
options.add((OptionElement) declaration);
} else if (declaration instanceof ExtendElement) {
extendsList.add((ExtendElement) declaration);
}
}
}
private Object readDeclaration(String documentation, Context context) {
int index = declarationCount++;
// Skip unnecessary semicolons, occasionally used after a nested message declaration.
if (reader.peekChar(';')) return null;
Location location = reader.location();
String label = reader.readWord();
if (label.equals("package")) {
if (!context.permitsPackage()) throw reader.unexpected(location, "'package' in " + context);
if (packageName != null) throw reader.unexpected(location, "too many package names");
packageName = reader.readName();
prefix = packageName + ".";
reader.require(';');
return null;
} else if (label.equals("import")) {
if (!context.permitsImport()) throw reader.unexpected(location, "'import' in " + context);
String importString = reader.readString();
if ("public".equals(importString)) {
publicImports.add(reader.readString());
} else {
imports.add(importString);
}
reader.require(';');
return null;
} else if (label.equals("syntax")) {
if (!context.permitsSyntax()) throw reader.unexpected(location, "'syntax' in " + context);
reader.require('=');
if (index != 0) {
throw reader.unexpected(
location, "'syntax' element must be the first declaration in a file");
}
String syntaxString = reader.readQuotedString();
try {
syntax = ProtoFile.Syntax.get(syntaxString);
} catch (IllegalArgumentException e) {
throw reader.unexpected(location, e.getMessage());
}
reader.require(';');
return null;
} else if (label.equals("option")) {
OptionElement result = new OptionReader(reader).readOption('=');
reader.require(';');
return result;
} else if (label.equals("reserved")) {
return readReserved(location, documentation);
} else if (label.equals("message")) {
return readMessage(location, documentation);
} else if (label.equals("enum")) {
return readEnumElement(location, documentation);
} else if (label.equals("service")) {
return readService(location, documentation);
} else if (label.equals("extend")) {
return readExtend(location, documentation);
} else if (label.equals("rpc")) {
if (!context.permitsRpc()) throw reader.unexpected(location, "'rpc' in " + context);
return readRpc(location, documentation);
} else if (label.equals("oneof")) {
if (!context.permitsOneOf()) {
throw reader.unexpected(location, "'oneof' must be nested in message");
}
return readOneOf(documentation);
} else if (label.equals("extensions")) {
if (!context.permitsExtensions()) {
throw reader.unexpected(location, "'extensions' must be nested");
}
return readExtensions(location, documentation);
} else if (context == Context.MESSAGE || context == Context.EXTEND) {
return readField(documentation, location, label);
} else if (context == Context.ENUM) {
return readEnumConstant(documentation, location, label);
} else {
throw reader.unexpected(location, "unexpected label: " + label);
}
}
/** Reads a message declaration. */
private MessageElement readMessage(Location location, String documentation) {
String name = reader.readName();
String previousPrefix = prefix;
prefix = prefix + name + ".";
ImmutableList.Builder fields = ImmutableList.builder();
ImmutableList.Builder oneOfs = ImmutableList.builder();
ImmutableList.Builder nestedTypes = ImmutableList.builder();
ImmutableList.Builder extensions = ImmutableList.builder();
ImmutableList.Builder options = ImmutableList.builder();
ImmutableList.Builder reserveds = ImmutableList.builder();
ImmutableList.Builder groups = ImmutableList.builder();
reader.require('{');
while (true) {
String nestedDocumentation = reader.readDocumentation();
if (reader.peekChar('}')) break;
Object declared = readDeclaration(nestedDocumentation, Context.MESSAGE);
if (declared instanceof FieldElement) {
fields.add((FieldElement) declared);
} else if (declared instanceof OneOfElement) {
oneOfs.add((OneOfElement) declared);
} else if (declared instanceof GroupElement) {
groups.add((GroupElement) declared);
} else if (declared instanceof TypeElement) {
nestedTypes.add((TypeElement) declared);
} else if (declared instanceof ExtensionsElement) {
extensions.add((ExtensionsElement) declared);
} else if (declared instanceof OptionElement) {
options.add((OptionElement) declared);
} else if (declared instanceof ExtendElement) {
// Extend declarations always add in a global scope regardless of nesting.
extendsList.add((ExtendElement) declared);
} else if (declared instanceof ReservedElement) {
reserveds.add((ReservedElement) declared);
}
}
prefix = previousPrefix;
return new MessageElement(
location,
name,
documentation,
nestedTypes.build(),
options.build(),
reserveds.build(),
fields.build(),
oneOfs.build(),
extensions.build(),
groups.build()
);
}
/** Reads an extend declaration. */
private ExtendElement readExtend(Location location, String documentation) {
String name = reader.readName();
reader.require('{');
ImmutableList.Builder fields = ImmutableList.builder();
while (true) {
String nestedDocumentation = reader.readDocumentation();
if (reader.peekChar('}')) break;
Object declared = readDeclaration(nestedDocumentation, Context.EXTEND);
if (declared instanceof FieldElement) {
fields.add((FieldElement) declared);
}
}
return new ExtendElement(location, name, documentation, fields.build());
}
/** Reads a service declaration and returns it. */
private ServiceElement readService(Location location, String documentation) {
String name = reader.readName();
reader.require('{');
ImmutableList.Builder rpcs = ImmutableList.builder();
ImmutableList.Builder options = ImmutableList.builder();
while (true) {
String rpcDocumentation = reader.readDocumentation();
if (reader.peekChar('}')) break;
Object declared = readDeclaration(rpcDocumentation, Context.SERVICE);
if (declared instanceof RpcElement) {
rpcs.add((RpcElement) declared);
} else if (declared instanceof OptionElement) {
options.add((OptionElement) declared);
}
}
return new ServiceElement(
location,
name,
documentation,
rpcs.build(),
options.build()
);
}
/** Reads an enumerated type declaration and returns it. */
private EnumElement readEnumElement(Location location, String documentation) {
String name = reader.readName();
ImmutableList.Builder constants = ImmutableList.builder();
ImmutableList.Builder options = ImmutableList.builder();
reader.require('{');
while (true) {
String valueDocumentation = reader.readDocumentation();
if (reader.peekChar('}')) break;
Object declared = readDeclaration(valueDocumentation, Context.ENUM);
if (declared instanceof EnumConstantElement) {
constants.add((EnumConstantElement) declared);
} else if (declared instanceof OptionElement) {
options.add((OptionElement) declared);
}
}
return new EnumElement(location, name, documentation, options.build(), constants.build());
}
private Object readField(String documentation, Location location, String word) {
Field.Label label;
String type;
switch (word) {
case "required":
if (syntax == ProtoFile.Syntax.PROTO_3) {
throw reader.unexpected(
location, "'required' label forbidden in proto3 field declarations");
}
label = Field.Label.REQUIRED;
type = reader.readDataType();
break;
case "optional":
if (syntax == ProtoFile.Syntax.PROTO_3) {
throw reader.unexpected(
location, "'optional' label forbidden in proto3 field declarations");
}
label = Field.Label.OPTIONAL;
type = reader.readDataType();
break;
case "repeated":
label = Field.Label.REPEATED;
type = reader.readDataType();
break;
default:
if (syntax != ProtoFile.Syntax.PROTO_3
&& (!word.equals("map") || reader.peekChar() != '<')) {
throw reader.unexpected(location, "unexpected label: " + word);
}
label = null;
type = reader.readDataType(word);
break;
}
if (type.startsWith("map<") && label != null) {
throw reader.unexpected(location, "'map' type cannot have label");
}
if (type.equals("group")) {
return readGroup(location, documentation, label);
}
return readField(location, documentation, label, type);
}
/** Reads an field declaration and returns it. */
private FieldElement readField(
Location location, String documentation, @Nullable Field.Label label, String type) {
String name = reader.readName();
reader.require('=');
int tag = reader.readInt();
List options = new OptionReader(reader).readOptions();
reader.require(';');
options = new ArrayList<>(options); // Mutable copy for extractDefault.
String defaultValue = stripDefault(options);
documentation = reader.tryAppendTrailingDocumentation(documentation);
return new FieldElement(
location,
label,
type,
name,
defaultValue,
tag,
documentation,
new ArrayList<>(options));
}
/**
* Defaults aren't options. This finds an option named "default", removes, and returns it. Returns
* null if no default option is present.
*/
private @Nullable String stripDefault(List options) {
String result = null;
for (Iterator i = options.iterator(); i.hasNext();) {
OptionElement option = i.next();
if (option.getName().equals("default")) {
i.remove();
result = String.valueOf(option.getValue()); // Defaults aren't options!
}
}
return result;
}
private OneOfElement readOneOf(String documentation) {
String name = reader.readName();
ImmutableList.Builder fields = ImmutableList.builder();
ImmutableList.Builder groups = ImmutableList.builder();
reader.require('{');
while (true) {
String nestedDocumentation = reader.readDocumentation();
if (reader.peekChar('}')) break;
Location location = reader.location();
String type = reader.readDataType();
if (type.equals("group")) {
groups.add(readGroup(location, nestedDocumentation, null));
} else {
fields.add(readField(location, nestedDocumentation, null, type));
}
}
return new OneOfElement(
name,
documentation,
fields.build(),
groups.build()
);
}
private GroupElement readGroup(Location location, String documentation, Field.Label label) {
String name = reader.readWord();
reader.require('=');
int tag = reader.readInt();
ImmutableList.Builder fields = ImmutableList.builder();
reader.require('{');
while (true) {
String nestedDocumentation = reader.readDocumentation();
if (reader.peekChar('}')) break;
Location fieldLocation = reader.location();
String fieldLabel = reader.readWord();
Object field = readField(nestedDocumentation, fieldLocation, fieldLabel);
if (!(field instanceof FieldElement)) {
throw reader.unexpected("expected field declaration, was " + field);
}
fields.add((FieldElement) field);
}
return new GroupElement(
label,
location,
name,
tag,
documentation,
fields.build()
);
}
/** Reads a reserved tags and names list like "reserved 10, 12 to 14, 'foo';". */
private ReservedElement readReserved(Location location, String documentation) {
ImmutableList.Builder