Skip to content

Commit

Permalink
Add Initial POJO Schema generation (#24)
Browse files Browse the repository at this point in the history
Add initial implementation of Schema generation.
  • Loading branch information
hpmellema authored Apr 11, 2024
1 parent b980b0f commit f3fdbd9
Show file tree
Hide file tree
Showing 10 changed files with 362 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import software.amazon.smithy.codegen.core.SymbolProvider;
import software.amazon.smithy.codegen.core.directed.*;
import software.amazon.smithy.java.codegen.generators.ExceptionGenerator;
import software.amazon.smithy.java.codegen.generators.SharedSchemasGenerator;
import software.amazon.smithy.java.codegen.generators.StructureGenerator;
import software.amazon.smithy.utils.SmithyUnstableApi;

Expand Down Expand Up @@ -38,6 +39,13 @@ public CodeGenerationContext createContext(
);
}

@Override
public void customizeBeforeShapeGeneration(
CustomizeDirective<CodeGenerationContext, JavaCodegenSettings> directive
) {
new SharedSchemasGenerator().accept(directive);
}

@Override
public void generateService(GenerateServiceDirective<CodeGenerationContext, JavaCodegenSettings> directive) {
// TODO
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,9 @@
import software.amazon.smithy.codegen.core.SmithyIntegration;
import software.amazon.smithy.java.codegen.writer.JavaWriter;

/**
* Java SPI for customizing Java code generation, renaming shapes, modifying the model,
* adding custom code, etc.
*/
public interface JavaCodegenIntegration
extends SmithyIntegration<JavaCodegenSettings, JavaWriter, CodeGenerationContext> {}
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public String toMemberName(MemberShape shape) {
Shape containerShape = model.expectShape(shape.getContainer());
if (containerShape.isEnumShape() || containerShape.isIntEnumShape()) {
return CaseUtils.toSnakeCase(SymbolUtils.MEMBER_ESCAPER.escape(shape.getMemberName()))
.toUpperCase(Locale.ROOT);
.toUpperCase(Locale.ENGLISH);
}

// If a member name contains an underscore, convert to camel case
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.java.codegen;

import java.util.Locale;
import software.amazon.smithy.java.codegen.writer.JavaWriter;
import software.amazon.smithy.java.runtime.core.schema.PreludeSchemas;
import software.amazon.smithy.model.loader.Prelude;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ToShapeId;
import software.amazon.smithy.utils.CaseUtils;
import software.amazon.smithy.utils.SmithyInternalApi;

/**
* Provides various utility functions around SDK schemas
*/
@SmithyInternalApi
public final class SchemaUtils {

/**
* Determines the name to use for the Schema constant for a member.
*
* @param memberName Member shape to generate schema name from
* @return name to use for static schema property
*/
public static String toMemberSchemaName(String memberName) {
return "SCHEMA_" + CaseUtils.toSnakeCase(memberName).toUpperCase(Locale.ENGLISH);
}

/**
* Determines the name to use for shape Schemas.
*
* @param toShapeId shape to generate name for
* @return name to use for static schema property
*/
public static String toSchemaName(ToShapeId toShapeId) {
return CaseUtils.toSnakeCase(toShapeId.toShapeId().getName()).toUpperCase(Locale.ENGLISH);
}

/**
* Writes the schema property to use for a given shape.
*
* <p>If a shape is a prelude shape then it will use a property from {@link PreludeSchemas}.
* Otherwise, the shape will use the generated {@code SharedSchemas} utility class.
*
* @param writer Writer to use for writing the Schema type.
* @param shape shape to write Schema type for.
*/
public static void writeSchemaType(JavaWriter writer, Shape shape) {
if (Prelude.isPreludeShape(shape)) {
writer.write("$T.$L", PreludeSchemas.class, shape.getType().name());
} else {
writer.write("SharedSchemas.$L", toSchemaName(shape));
}
}

private SchemaUtils() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public final class $T extends $T {
directive.symbol(),
ModeledSdkException.class,
writer.consumer(w -> w.writeIdString(shape)),
new SchemaGenerator(),
new SchemaGenerator(writer, directive.shape(), directive.symbolProvider(), directive.model()),
new PropertyGenerator(writer, shape, directive.symbolProvider()),
new ConstructorGenerator(writer, shape, directive.symbolProvider()),
new GetterGenerator(writer, shape, directive.symbolProvider(), directive.model()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,197 @@

package software.amazon.smithy.java.codegen.generators;

public class SchemaGenerator implements Runnable {
import software.amazon.smithy.codegen.core.SymbolProvider;
import software.amazon.smithy.java.codegen.SchemaUtils;
import software.amazon.smithy.java.codegen.sections.SchemaTraitSection;
import software.amazon.smithy.java.codegen.writer.JavaWriter;
import software.amazon.smithy.java.runtime.core.schema.SdkSchema;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.shapes.ListShape;
import software.amazon.smithy.model.shapes.MapShape;
import software.amazon.smithy.model.shapes.MemberShape;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ShapeType;
import software.amazon.smithy.model.shapes.ShapeVisitor;
import software.amazon.smithy.model.shapes.StructureShape;

/**
* Generates a schema constant for a given shape.
*
* <p>Member schemas are always generated as {@code private} while all other
* schema properties are generated as {@code package-private}.
*/
final class SchemaGenerator implements Runnable {

private final JavaWriter writer;
private final Shape shape;
private final SymbolProvider symbolProvider;
private final Model model;

public SchemaGenerator(JavaWriter writer, Shape shape, SymbolProvider symbolProvider, Model model) {
this.writer = writer;
this.shape = shape;
this.symbolProvider = symbolProvider;
this.model = model;
}

@Override
public void run() {
shape.accept(new SchemaGeneratorVisitor(writer, symbolProvider, model));
}

private static final class SchemaGeneratorVisitor extends ShapeVisitor.Default<Void> {

private final JavaWriter writer;
private final SymbolProvider symbolProvider;
private final Model model;

private SchemaGeneratorVisitor(JavaWriter writer, SymbolProvider symbolProvider, Model model) {
this.writer = writer;
this.symbolProvider = symbolProvider;
this.model = model;
}

@Override
protected Void getDefault(Shape shape) {
writer.write(
"""
static final $1T $2L = $1T.builder()
.type($3T.$4L)
.id($5S)
${6C|}
.build();
""",
SdkSchema.class,
SchemaUtils.toSchemaName(shape),
ShapeType.class,
shape.getType().name(),
shape.toShapeId(),
writer.consumer(w -> writeSchemaTraitBlock(w, shape))
);
return null;
}

@Override
public Void listShape(ListShape shape) {
var target = model.expectShape(shape.getMember().getTarget());
writer.write(
"""
static final $1T $2L = $1T.builder()
.type($3T.LIST)
.id($4S)
${5C|}
.members(SdkSchema.memberBuilder(0, "member", $6C))
.build();
""",
SdkSchema.class,
SchemaUtils.toSchemaName(shape),
ShapeType.class,
shape.toShapeId(),
writer.consumer(w -> writeSchemaTraitBlock(w, shape)),
writer.consumer(w -> SchemaUtils.writeSchemaType(w, target))
);

return null;
}

@Override
public Void mapShape(MapShape shape) {
var keyShape = model.expectShape(shape.getKey().getTarget());
var valueShape = model.expectShape(shape.getValue().getTarget());
writer.write(
"""
static final $1T $2L = $1T.builder()
.type($3T.MAP)
.id($4S)
${5C}
.members(
$1T.memberBuilder(0, "key", $6C),
$1T.memberBuilder(1, "value", $7C)
)
.build();
""",
SdkSchema.class,
SchemaUtils.toSchemaName(shape),
ShapeType.class,
shape.toShapeId(),
writer.consumer(w -> writeSchemaTraitBlock(w, shape)),
writer.consumer(w -> SchemaUtils.writeSchemaType(w, keyShape)),
writer.consumer(w -> SchemaUtils.writeSchemaType(w, valueShape))
);
return null;
}

@Override
public Void structureShape(StructureShape shape) {
int idx = 0;
for (var iter = shape.members().iterator(); iter.hasNext(); idx++) {
writeNestedMemberSchema(idx, iter.next());
}
writer.pushState();
// Add the member schema names to the context, so we can iterate through them
writer.putContext(
"memberSchemas",
shape.members()
.stream()
.map(symbolProvider::toMemberName)
.map(SchemaUtils::toMemberSchemaName)
.toList()
);
writer.write(
"""
static final $1T SCHEMA = $1T.builder()
.id(ID)
.type($2T.$3L)
${4C|}
${?memberSchemas}.members(${#memberSchemas}
${value:L}${^key.last},${/key.last}${/memberSchemas}
)
${/memberSchemas}.build();
""",
SdkSchema.class,
ShapeType.class,
shape.getType().toString().toUpperCase(),
writer.consumer(w -> writeSchemaTraitBlock(w, shape))
);
writer.popState();

return null;
}

@Override
public Void memberShape(MemberShape shape) {
throw new UnsupportedOperationException("Member Shapes cannot directly Generate a Schema.");
}

private void writeNestedMemberSchema(int idx, MemberShape member) {
var memberName = symbolProvider.toMemberName(member);
var target = model.expectShape(member.getTarget());
writer.write(
"""
private static final $1T $2L = $1T.memberBuilder($3L, $4S, $5C)
.id(ID)
${6C|}
.build();
""",
SdkSchema.class,
SchemaUtils.toMemberSchemaName(memberName),
idx,
memberName,
writer.consumer(w -> SchemaUtils.writeSchemaType(w, target)),
writer.consumer(w -> writeSchemaTraitBlock(w, member))
);
}

private static void writeSchemaTraitBlock(JavaWriter writer, Shape shape) {
if (shape.getAllTraits().isEmpty()) {
return;
}
writer.openBlock(
".traits(",
")",
() -> writer.injectSection(new SchemaTraitSection(shape)).newLine()
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.java.codegen.generators;

import java.util.EnumSet;
import java.util.function.Consumer;
import software.amazon.smithy.codegen.core.directed.CustomizeDirective;
import software.amazon.smithy.java.codegen.CodeGenerationContext;
import software.amazon.smithy.java.codegen.JavaCodegenSettings;
import software.amazon.smithy.java.codegen.writer.JavaWriter;
import software.amazon.smithy.model.loader.Prelude;
import software.amazon.smithy.model.shapes.ShapeType;
import software.amazon.smithy.utils.SmithyInternalApi;

/**
* Generates a {@code SharedSchemas} utility class that contains all unattached schemas for the model.
*/
@SmithyInternalApi
public final class SharedSchemasGenerator
implements Consumer<CustomizeDirective<CodeGenerationContext, JavaCodegenSettings>> {

// Types that generate their own schemas
private static final EnumSet<ShapeType> EXCLUDED_TYPES = EnumSet.of(
ShapeType.RESOURCE,
ShapeType.SERVICE,
ShapeType.UNION,
ShapeType.ENUM,
ShapeType.INT_ENUM,
ShapeType.STRUCTURE,
ShapeType.MEMBER,
ShapeType.OPERATION
);

@Override
public void accept(CustomizeDirective<CodeGenerationContext, JavaCodegenSettings> directive) {
directive.context()
.writerDelegator()
.useFileWriter(
getFilename(directive.settings()),
directive.settings().packageNamespace() + ".model",
writer -> {
writer.write(
"""
/**
* Defines shared shapes across the model package that are not part of another code-generated type.
*/
final class SharedSchemas {
${C|}
private SharedSchemas() {}
}
""",
writer.consumer(w -> this.generatedSchemas(w, directive))
);
}
);
}

private void generatedSchemas(
JavaWriter writer,
CustomizeDirective<CodeGenerationContext, JavaCodegenSettings> directive
) {
// Loop through service closure and find all shapes that will not generate their own schemas
directive.connectedShapes()
.values()
.stream()
.filter(s -> !EXCLUDED_TYPES.contains(s.getType()))
.filter(s -> !Prelude.isPreludeShape(s))
.forEach(s -> new SchemaGenerator(writer, s, directive.symbolProvider(), directive.model()).run());
}

private static String getFilename(JavaCodegenSettings settings) {
return String.format("./%s/model/SharedSchemas.java", settings.packageNamespace().replace(".", "/"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public final class $T implements $T {
directive.symbol(),
SerializableShape.class,
writer.consumer(w -> w.writeIdString(shape)),
new SchemaGenerator(),
new SchemaGenerator(writer, directive.shape(), directive.symbolProvider(), directive.model()),
new PropertyGenerator(writer, shape, directive.symbolProvider()),
new ConstructorGenerator(writer, shape, directive.symbolProvider()),
new GetterGenerator(writer, shape, directive.symbolProvider(), directive.model()),
Expand Down
Loading

0 comments on commit f3fdbd9

Please sign in to comment.