Codegen: JavaGenerator [2] - parse JSON schema into Java typed representation

Summary:
This parses the output JSON schema into Java Type's, without any alias resolution. Some parts of the schema have incomplete information, hence a bunch of TODOs in the code expressing the issues.

Notes:
* The type structure here could be implemented in other codegen generators as well (including the RNCodegen.js' generators) for better type safety and correctness. With this exercise, I was able to catch a few issues mentioned above.
* This commit does not produce any Java code with JavaPoet yet, just parsing the schema.

Changelog: [Internal]

Reviewed By: mdvacca

Differential Revision: D23152891

fbshipit-source-id: 9f774dd98975f7202d3faf11c3fbfb83d4c97f5a
This commit is contained in:
Kevin Gozali
2020-08-17 14:10:04 -07:00
committed by Facebook GitHub Bot
parent d1f217e829
commit e26dd1c35d
24 changed files with 432 additions and 18 deletions
@@ -20,6 +20,7 @@ gradlePlugin {
dependencies {
implementation 'com.android.tools.build:gradle:3.5.3'
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.google.guava:guava:29.0-jre'
implementation 'com.squareup:javapoet:1.13.0'
}
@@ -11,6 +11,7 @@ import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import javax.lang.model.element.Modifier;
@@ -26,7 +27,7 @@ public final class JavaGenerator {
mOutputDir = outputDir;
}
public void build() throws IOException {
public void build() throws FileNotFoundException, IOException {
final MethodSpec main =
MethodSpec.methodBuilder("main")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
@@ -43,8 +44,6 @@ public final class JavaGenerator {
final JavaFile javaFile = JavaFile.builder(mJavaPackageName, helloWorld).build();
System.out.println(javaFile.toString());
javaFile.writeTo(mOutputDir);
}
}
@@ -0,0 +1,250 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.react.codegen.generator;
import com.facebook.react.codegen.generator.model.AliasType;
import com.facebook.react.codegen.generator.model.AnyType;
import com.facebook.react.codegen.generator.model.ArrayType;
import com.facebook.react.codegen.generator.model.BooleanType;
import com.facebook.react.codegen.generator.model.DoubleType;
import com.facebook.react.codegen.generator.model.FloatType;
import com.facebook.react.codegen.generator.model.FunctionType;
import com.facebook.react.codegen.generator.model.GenericObjectType;
import com.facebook.react.codegen.generator.model.Int32Type;
import com.facebook.react.codegen.generator.model.NativeModuleType;
import com.facebook.react.codegen.generator.model.NullableType;
import com.facebook.react.codegen.generator.model.NumberType;
import com.facebook.react.codegen.generator.model.ObjectType;
import com.facebook.react.codegen.generator.model.PromiseType;
import com.facebook.react.codegen.generator.model.RawSchema;
import com.facebook.react.codegen.generator.model.ReservedFunctionValueType;
import com.facebook.react.codegen.generator.model.StringType;
import com.facebook.react.codegen.generator.model.Type;
import com.facebook.react.codegen.generator.model.TypeId;
import com.facebook.react.codegen.generator.model.VoidType;
import com.google.common.collect.ImmutableList;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public final class SchemaJsonParser {
public static RawSchema parse(final File schemaFile) throws FileNotFoundException, IOException {
final JsonParser parser = new JsonParser();
final JsonElement rootElement = parser.parse(new FileReader(schemaFile));
final Map<String, List<NativeModuleType>> collection = new HashMap<>();
if (rootElement.isJsonObject()) {
final JsonObject root = rootElement.getAsJsonObject();
final JsonObject modules = root.getAsJsonObject("modules");
modules
.entrySet()
.forEach(
entry -> {
final String jsModuleName = entry.getKey();
final JsonObject jsModule = entry.getValue().getAsJsonObject();
final JsonObject nativeModules = jsModule.getAsJsonObject("nativeModules");
if (nativeModules == null) {
// TODO: Handle components-related sections.
return;
}
collection.put(
jsModuleName,
nativeModules.entrySet().stream()
.map(
nativeModuleEntry -> {
return parseNativeModule(
TypeId.of(jsModuleName, nativeModuleEntry.getKey()),
nativeModuleEntry.getValue().getAsJsonObject());
})
.collect(Collectors.toList()));
});
}
return new RawSchema(collection);
}
private static NativeModuleType parseNativeModule(final TypeId typeId, final JsonObject json) {
final JsonObject aliases = json.getAsJsonObject("aliases");
final JsonArray properties = json.getAsJsonArray("properties");
// TODO: parse aliases
ImmutableList.Builder<NativeModuleType.Property> methods = new ImmutableList.Builder<>();
properties.forEach(
p -> {
final JsonObject node = p.getAsJsonObject();
final String name = node.has("name") ? node.get("name").getAsString() : null;
final JsonObject typeAnnotation = node.getAsJsonObject("typeAnnotation");
// TODO (T71845349): "optional" field shouldn't be part of the Function's typeAnnotation.
final boolean optional = typeAnnotation.get("optional").getAsBoolean();
final TypeId propertyTypeId = TypeId.of(typeId.moduleName, name);
methods.add(
new NativeModuleType.Property(
name, parseTypeAnnotation(propertyTypeId, typeAnnotation), optional));
});
return new NativeModuleType(typeId, methods.build());
}
// Parse type information from a JSON "typeAnnotation" node.
private static Type parseTypeAnnotation(final TypeId typeId, final JsonObject typeAnnotation)
throws IllegalStateException {
final String type = typeAnnotation.get("type").getAsString();
// TODO (T71824250): Support NullableTypeAnnotation in the schema instead of a field here.
final boolean nullable =
typeAnnotation.has("nullable") ? typeAnnotation.get("nullable").getAsBoolean() : false;
Type parsedType = null;
switch (type) {
case AliasType.TYPE_NAME:
// TODO
parsedType = new AliasType(typeId, typeId);
break;
case AnyType.TYPE_NAME:
parsedType = new AnyType(typeId);
break;
case ArrayType.TYPE_NAME:
parsedType = parseArrayTypeAnnotation(typeId, typeAnnotation);
break;
case BooleanType.TYPE_NAME:
parsedType = new BooleanType(typeId);
break;
case DoubleType.TYPE_NAME:
parsedType = new DoubleType(typeId);
break;
case FloatType.TYPE_NAME:
parsedType = new FloatType(typeId);
break;
case FunctionType.TYPE_NAME:
parsedType = parseFunctionTypeAnnotation(typeId, typeAnnotation);
break;
case GenericObjectType.TYPE_NAME:
parsedType = new GenericObjectType(typeId);
break;
case Int32Type.TYPE_NAME:
parsedType = new Int32Type(typeId);
break;
case NumberType.TYPE_NAME:
parsedType = new NumberType(typeId);
break;
case ObjectType.TYPE_NAME:
parsedType = parseObjectTypeAnnotation(typeId, typeAnnotation);
break;
case PromiseType.TYPE_NAME:
parsedType = new PromiseType(typeId);
break;
case ReservedFunctionValueType.TYPE_NAME:
// TODO
parsedType =
new ReservedFunctionValueType(typeId, ReservedFunctionValueType.ReservedName.RootTag);
case StringType.TYPE_NAME:
parsedType = new StringType(typeId);
break;
case VoidType.TYPE_NAME:
return VoidType.VOID;
default:
throw new IllegalStateException("Found invalid type annotation: " + type);
}
return maybeCreateNullableType(nullable, parsedType);
}
private static Type parseArrayTypeAnnotation(
final TypeId typeId, final JsonObject typeAnnotation) {
final JsonObject elementTypeAnnotation = typeAnnotation.getAsJsonObject("elementType");
// TODO (T71847026): Some array types are missing elementType annotation.
final Type elementType =
elementTypeAnnotation != null
? parseTypeAnnotation(
TypeId.of(
typeId.moduleName,
elementTypeAnnotation.has("name")
? elementTypeAnnotation.get("name").getAsString()
: null),
elementTypeAnnotation)
: new AnyType(TypeId.of(typeId.moduleName));
return new ArrayType(typeId, elementType);
}
private static Type parseFunctionTypeAnnotation(
final TypeId typeId, final JsonObject typeAnnotation) {
final JsonArray params = typeAnnotation.getAsJsonArray("params");
ImmutableList.Builder<FunctionType.ArgumentType> paramsList = new ImmutableList.Builder<>();
// TODO (T71846321): Some functions are missing params specification.
if (params != null) {
params.forEach(
p -> {
final JsonObject node = p.getAsJsonObject();
final String name = node.has("name") ? node.get("name").getAsString() : null;
paramsList.add(
FunctionType.createArgument(
name,
parseTypeAnnotation(
TypeId.of(typeId.moduleName, name),
node.getAsJsonObject("typeAnnotation"))));
});
}
// TODO (T71846321): Some functions are missing a return type.
final JsonObject returnTypeAnnotation = typeAnnotation.getAsJsonObject("returnTypeAnnotation");
final Type returnType =
returnTypeAnnotation != null
? parseTypeAnnotation(TypeId.of(typeId.moduleName), returnTypeAnnotation)
: VoidType.VOID;
return new FunctionType(typeId, paramsList.build(), returnType);
}
private static Type parseObjectTypeAnnotation(
final TypeId typeId, final JsonObject typeAnnotation) {
final JsonArray properties = typeAnnotation.getAsJsonArray("properties");
ImmutableList.Builder<ObjectType.Property> propertiesList = new ImmutableList.Builder<>();
properties.forEach(
p -> {
final JsonObject node = p.getAsJsonObject();
final String name = node.has("name") ? node.get("name").getAsString() : null;
final boolean optional = node.get("optional").getAsBoolean();
final JsonObject propertyTypeAnnotation = node.getAsJsonObject("typeAnnotation");
final TypeId propertyTypeId = TypeId.of(typeId.moduleName, name);
// TODO (T67898313): Some object properties are missing typeAnnotation.
final Type propertyType =
propertyTypeAnnotation != null
? parseTypeAnnotation(propertyTypeId, propertyTypeAnnotation)
: new AnyType(propertyTypeId);
propertiesList.add(new ObjectType.Property(name, propertyType, optional));
});
return new ObjectType(typeId, propertiesList.build());
}
private static Type maybeCreateNullableType(final boolean nullable, final Type original) {
if (!nullable || original instanceof VoidType) {
return original;
}
return new NullableType(TypeId.of(original.getTypeId()), original);
}
}
@@ -8,6 +8,7 @@
package com.facebook.react.codegen.generator.model;
public final class AliasType extends Type {
public static final String TYPE_NAME = "TypeAliasTypeAnnotation";
public final TypeId referredTypeId;
@@ -8,6 +8,7 @@
package com.facebook.react.codegen.generator.model;
public final class AnyType extends Type {
public static final String TYPE_NAME = "AnyTypeAnnotation";
public AnyType(final TypeId typeId) {
super(typeId);
@@ -8,6 +8,7 @@
package com.facebook.react.codegen.generator.model;
public final class ArrayType extends Type {
public static final String TYPE_NAME = "ArrayTypeAnnotation";
public final Type elementType;
@@ -8,6 +8,7 @@
package com.facebook.react.codegen.generator.model;
public final class BooleanType extends Type {
public static final String TYPE_NAME = "BooleanTypeAnnotation";
public BooleanType(final TypeId typeId) {
super(typeId);
@@ -8,6 +8,7 @@
package com.facebook.react.codegen.generator.model;
public final class DoubleType extends NumberType {
public static final String TYPE_NAME = "DoubleTypeAnnotation";
public DoubleType(final TypeId typeId) {
super(typeId);
@@ -8,6 +8,7 @@
package com.facebook.react.codegen.generator.model;
public final class FloatType extends NumberType {
public static final String TYPE_NAME = "FloatTypeAnnotation";
public FloatType(final TypeId typeId) {
super(typeId);
@@ -12,11 +12,14 @@ import java.util.List;
import java.util.Objects;
public final class FunctionType extends Type {
public static final String TYPE_NAME = "FunctionTypeAnnotation";
public static class ArgumentType {
public final String name;
public final Type type;
// Note: Function argument is not optional.
// TODO (T71926678): Revisit if optional should be supported.
private ArgumentType(String name, Type type) {
this.name = name;
this.type = type;
@@ -39,19 +42,29 @@ public final class FunctionType extends Type {
public int hashCode() {
return Objects.hash(name, type);
}
@Override
public String toString() {
return name + ": " + type;
}
}
public static ArgumentType createArgument(String name, Type type) {
return new ArgumentType(name, type);
}
public final List<ArgumentType> parameters;
public final Type returnType;
public FunctionType(final TypeId typeId, List<ArgumentType> parameters, Type returnType) {
public FunctionType(
final TypeId typeId, final List<ArgumentType> parameters, final Type returnType) {
super(typeId);
this.parameters = Collections.unmodifiableList(parameters);
this.returnType = returnType;
}
@Override
public boolean equals(Object o) {
public boolean equals(final Object o) {
if (this == o) {
return true;
}
@@ -59,7 +72,7 @@ public final class FunctionType extends Type {
return false;
}
FunctionType that = (FunctionType) o;
final FunctionType that = (FunctionType) o;
return Objects.equals(this.parameters, that.parameters)
&& Objects.equals(this.returnType, that.returnType);
}
@@ -68,4 +81,9 @@ public final class FunctionType extends Type {
public int hashCode() {
return Objects.hash(super.hashCode(), parameters, returnType);
}
@Override
public String toString() {
return "(" + returnType + ")" + this.getTypeId() + "(" + parameters + ")";
}
}
@@ -8,6 +8,7 @@
package com.facebook.react.codegen.generator.model;
public final class GenericObjectType extends Type {
public static final String TYPE_NAME = "GenericObjectTypeAnnotation";
public GenericObjectType(final TypeId typeId) {
super(typeId);
@@ -8,6 +8,7 @@
package com.facebook.react.codegen.generator.model;
public final class Int32Type extends NumberType {
public static final String TYPE_NAME = "Int32TypeAnnotation";
public Int32Type(final TypeId typeId) {
super(typeId);
@@ -0,0 +1,45 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.react.codegen.generator.model;
import java.util.Collections;
import java.util.List;
public final class NativeModuleType extends Type {
public static String TYPE_NAME = "<NONE>"; // Not an actual type in the schema.
public final List<Property> properties;
public static class Property {
public final String name;
public final FunctionType type;
public final boolean optional;
public Property(final String name, final Type type, final boolean optional) {
assertType(type, FunctionType.class);
this.name = name;
this.type = (FunctionType) type;
this.optional = optional;
}
@Override
public String toString() {
return name + ": " + (this.optional ? "?" : "") + type;
}
}
public NativeModuleType(final TypeId typeId, final List<Property> properties) {
super(typeId);
this.properties = Collections.unmodifiableList(properties);
}
@Override
public String toString() {
return getTypeId() + " -> " + properties;
}
}
@@ -0,0 +1,19 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.react.codegen.generator.model;
public final class NullableType extends Type {
public static final String TYPE_NAME = "NullableTypeAnnotation";
public final Type referredType;
public NullableType(final TypeId typeId, final Type referredType) {
super(typeId);
this.referredType = referredType;
}
}
@@ -8,6 +8,7 @@
package com.facebook.react.codegen.generator.model;
public class NumberType extends Type {
public static final String TYPE_NAME = "NumberTypeAnnotation";
public NumberType(final TypeId typeId) {
super(typeId);
@@ -7,26 +7,38 @@
package com.facebook.react.codegen.generator.model;
public final class ObjectType extends Type {
import java.util.Collections;
import java.util.List;
public ObjectType(final TypeId typeId) {
super(typeId);
}
public final class ObjectType extends Type {
public static final String TYPE_NAME = "ObjectTypeAnnotation";
public static class Property {
public final String propertyName;
public final String name;
public final Type type;
public final boolean optional;
public Property(String propertyName, Type type, boolean optional) {
this.propertyName = propertyName;
public Property(String name, Type type, boolean optional) {
this.name = name;
this.type = type;
this.optional = optional;
}
@Override
public String toString() {
return propertyName + " " + type;
return (optional ? "?" : "") + name + ": " + type;
}
}
public final List<Property> properties;
public ObjectType(final TypeId typeId, final List<Property> properties) {
super(typeId);
this.properties = Collections.unmodifiableList(properties);
}
@Override
public String toString() {
return getTypeId() + " -> " + properties;
}
}
@@ -0,0 +1,16 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.react.codegen.generator.model;
public final class PromiseType extends Type {
public static final String TYPE_NAME = "GenericPromiseTypeAnnotation";
public PromiseType(final TypeId typeId) {
super(typeId);
}
}
@@ -0,0 +1,26 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.react.codegen.generator.model;
import java.util.List;
import java.util.Map;
/** Represents the parsed JSON schema without any type resolution. */
public final class RawSchema {
public final Map<String, List<NativeModuleType>> modules;
public RawSchema(final Map<String, List<NativeModuleType>> modules) {
this.modules = modules;
}
@Override
public String toString() {
return modules.toString();
}
}
@@ -8,6 +8,8 @@
package com.facebook.react.codegen.generator.model;
public final class ReservedFunctionValueType extends Type {
public static final String TYPE_NAME = "ReservedFunctionValueTypeAnnotation";
public enum ReservedName {
RootTag,
}
@@ -8,6 +8,7 @@
package com.facebook.react.codegen.generator.model;
public final class StringType extends Type {
public static final String TYPE_NAME = "StringTypeAnnotation";
public StringType(final TypeId typeId) {
super(typeId);
@@ -10,8 +10,18 @@ package com.facebook.react.codegen.generator.model;
import java.util.Objects;
public abstract class Type {
public static String TYPE_NAME = "<NONE>";
protected final TypeId mTypeId;
public static <T extends Type> void assertType(
final Type type, final Class<T> expectedTypeClass) {
if (!expectedTypeClass.isInstance(type)) {
throw new IllegalStateException(
"Expected: " + expectedTypeClass.getName() + " but found: " + type.getClass().getName());
}
}
public Type(final TypeId typeId) {
mTypeId = typeId;
}
@@ -38,6 +38,10 @@ public final class TypeId {
return new TypeId(moduleName, typeName);
}
public static TypeId of(final TypeId typeId) {
return of(typeId.moduleName, typeId.typeName);
}
@Override
public String toString() {
return String.format(
@@ -8,8 +8,10 @@
package com.facebook.react.codegen.generator.model;
public final class VoidType extends Type {
public static final String TYPE_NAME = "VoidTypeAnnotation";
public static final VoidType VOID = new VoidType(TypeId.of(""));
public VoidType(final TypeId typeId) {
private VoidType(final TypeId typeId) {
super(typeId);
}
}
@@ -12,7 +12,7 @@ import com.facebook.react.codegen.generator.JavaGenerator;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.io.File;
import java.io.IOException;
import org.gradle.api.GradleException;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.Task;
@@ -159,8 +159,8 @@ public class CodegenPlugin implements Plugin<Project> {
final JavaGenerator generator = new JavaGenerator(schemaFile, javaPackageName, outputDir);
try {
generator.build();
} catch (final IOException e) {
// Ignore for now.
} catch (final Exception ex) {
throw new GradleException("Failed to generate Java from schema.", ex);
}
}
}