diff --git a/.github/workflows/codegen-compatibility.yml b/.github/workflows/codegen-compatibility.yml new file mode 100644 index 000000000..d1dbd4f8f --- /dev/null +++ b/.github/workflows/codegen-compatibility.yml @@ -0,0 +1,58 @@ +name: Codegen Compatibility + +on: + # Run daily at 4 AM UTC + schedule: + - cron: '0 4 * * *' + + # Allow manual trigger + workflow_dispatch: + +permissions: + contents: read + +jobs: + codegen-compatibility: + runs-on: ubuntu-latest + name: AWS Model Codegen Compatibility + + steps: + - uses: actions/checkout@v6 + + - name: Checkout api-models-aws + uses: actions/checkout@v6 + with: + repository: aws/api-models-aws + path: api-models-aws + + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + java-version: 21 + distribution: 'corretto' + + - name: Cache compiled buildscripts + uses: actions/cache@v5 + with: + key: ${{ runner.os }}-gradle-${{ hashFiles('buildSrc/**/*.kts') }} + path: | + ./buildSrc/build + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v5 + with: + cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} + gradle-home-cache-includes: | + caches + + - name: Run codegen compatibility tests + env: + API_MODELS_AWS_DIR: ${{ github.workspace }}/api-models-aws + run: | + ./gradlew :codegen:codegen-plugin:integ --tests "*.AwsModelCodegenCompilationTest" --stacktrace + + - uses: actions/upload-artifact@v7 + if: failure() + with: + name: codegen-compatibility-report + path: '**/build/reports/tests' diff --git a/codegen/codegen-plugin/build.gradle.kts b/codegen/codegen-plugin/build.gradle.kts index 9d1c3ff21..52cd1cfee 100644 --- a/codegen/codegen-plugin/build.gradle.kts +++ b/codegen/codegen-plugin/build.gradle.kts @@ -36,6 +36,14 @@ dependencies { itImplementation(project(":codecs:json-codec", configuration = "shadow")) itImplementation(libs.smithy.aws.traits) itImplementation(libs.smithy.rules) + + // Additional deps for AWS model codegen compilation test + itImplementation(project(":aws:aws-sigv4")) + itImplementation(project(":aws:aws-auth-api")) + itImplementation(project(":aws:client:aws-client-core")) + itImplementation(project(":aws:client:aws-client-http")) + itImplementation(project(":aws:client:aws-client-rulesengine")) + itImplementation(libs.smithy.aws.endpoints) } // Core codegen test runner @@ -64,3 +72,14 @@ listOf("generateSourcesClient", "generateSourcesServer", "generateSourcesTypes") tasks.test { failOnNoDiscoveredTests = false } + +// If API_MODELS_AWS_DIR is set, add it as a task input so Gradle doesn't cache results. +System.getenv("API_MODELS_AWS_DIR")?.let { modelsDir -> + val dir = file(modelsDir) + if (dir.isDirectory) { + tasks.named("integ") { + inputs.dir(dir).withPathSensitivity(PathSensitivity.RELATIVE) + maxHeapSize = "4g" + } + } +} diff --git a/codegen/codegen-plugin/src/it/java/software/amazon/smithy/java/codegen/AwsModelCodegenCompilationTest.java b/codegen/codegen-plugin/src/it/java/software/amazon/smithy/java/codegen/AwsModelCodegenCompilationTest.java new file mode 100644 index 000000000..1adb1fb4d --- /dev/null +++ b/codegen/codegen-plugin/src/it/java/software/amazon/smithy/java/codegen/AwsModelCodegenCompilationTest.java @@ -0,0 +1,230 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.codegen; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assumptions.abort; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.tools.Diagnostic; +import javax.tools.DiagnosticCollector; +import javax.tools.FileObject; +import javax.tools.ForwardingJavaFileManager; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileManager; +import javax.tools.JavaFileObject; +import javax.tools.SimpleJavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.ToolProvider; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.smithy.build.MockManifest; +import software.amazon.smithy.build.PluginContext; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.loader.ModelAssembler; +import software.amazon.smithy.model.node.ArrayNode; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.shapes.ServiceShape; + +/** + * Tests that Java code generation produces compilable code for all AWS service models. + * + *

Set the {@code API_MODELS_AWS_DIR} environment variable to the root of a local + * checkout of {@code aws/api-models-aws} to enable this test. + */ +@EnabledIfEnvironmentVariable(named = "API_MODELS_AWS_DIR", matches = ".*") +class AwsModelCodegenCompilationTest { + + private static final Set IGNORED_SDK_IDS = Set.of( + "timestream-write", + "timestream-query", + "clouddirectory" + + ); + + private static Path getModelsDir() { + var dir = System.getenv("API_MODELS_AWS_DIR"); + return Paths.get(dir, "models"); + } + + static Stream> awsModels() throws IOException { + var modelsDir = getModelsDir(); + return Files.find(modelsDir, + 4, + (p, a) -> p.toString().endsWith(".json") + && p.getParent().getParent().getFileName().toString().equals("service")) + .map(p -> Named.of(sdkId(modelsDir, p), p)); + } + + @ParameterizedTest(name = "client: {0}") + @MethodSource("awsModels") + @Execution(ExecutionMode.CONCURRENT) + void compileGeneratedCode(Path modelPath) { + generateAndCompile(modelPath, "client", "server"); + } + + private void generateAndCompile(Path modelPath, String... modes) { + // 1. Load model + Model model = Model.assembler() + .addImport(modelPath) + .putProperty(ModelAssembler.ALLOW_UNKNOWN_TRAITS, true) + .disableValidation() + .assemble() + .unwrap(); + + ServiceShape service = model.getServiceShapes() + .stream() + .findFirst() + .orElseThrow(() -> new IllegalStateException("Could not find service shape")); + + // 2. Run codegen with MockManifest (in-memory) + MockManifest manifest = new MockManifest(); + PluginContext context = PluginContext.builder() + .fileManifest(manifest) + .settings(ObjectNode.builder() + .withMember("service", service.getId().toString()) + .withMember("namespace", "test." + sanitize(service.getId().getName())) + .withMember("modes", ArrayNode.fromStrings(modes)) + .build()) + .model(model) + .build(); + try { + new JavaCodegenPlugin().execute(context); + + // 3. Validate all generated files + assertFalse(manifest.getFiles().isEmpty(), "No files generated for " + service.getId()); + + // 4. Collect generated files — Java sources for compilation, others for validation + List sources = new ArrayList<>(); + for (Path p : manifest.getFiles()) { + String content = manifest.expectFileString(p); + assertFalse(content.isBlank(), "Empty generated file: " + p); + if (p.toString().endsWith(".java")) { + sources.add(new InMemoryJavaSource(p.toString(), content)); + } + } + + // 5. In-memory compile all generated Java sources + if (!sources.isEmpty()) { + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + DiagnosticCollector diagnostics = new DiagnosticCollector<>(); + try (var stdFm = compiler.getStandardFileManager(diagnostics, null, null)) { + var fm = new InMemoryFileManager(stdFm); + boolean ok = compiler.getTask(null, + fm, + diagnostics, + List.of("-classpath", System.getProperty("java.class.path")), + null, + sources).call(); + if (!ok) { + String errors = diagnostics.getDiagnostics() + .stream() + .filter(d -> d.getKind() == Diagnostic.Kind.ERROR) + .map(Object::toString) + .collect(Collectors.joining("\n")); + Path dumpFile = dumpGeneratedSources(service.getId().toString(), sources, errors); + fail(Arrays.toString(modes) + " compilation failed for " + service.getId() + + ". Generated sources dumped to: " + dumpFile + "\n" + errors); + } + } + } + } catch (Throwable t) { + if (IGNORED_SDK_IDS.contains(sdkId(getModelsDir(), modelPath))) { + abort("Known failure for " + service.getId() + ": " + t.getMessage()); + } + sneakyThrow(t); + } + } + + @SuppressWarnings("unchecked") + private static void sneakyThrow(Throwable throwable) throws T { + throw (T) throwable; + } + + private static Path dumpGeneratedSources(String serviceId, List sources, String errors) { + try { + var dir = Files.createTempDirectory("codegen-fail-" + sanitize(serviceId) + "-"); + for (var source : sources) { + // Strip the MockManifest "/test/" base dir prefix to get a clean relative path + var fileName = Path.of(source.getName()).getFileName(); + var name = fileName != null ? fileName.toString() : source.getName(); + Files.writeString(dir.resolve(name), source.getCharContent(true)); + } + Files.writeString(dir.resolve("ERRORS.txt"), errors); + return dir; + } catch (IOException e) { + return Path.of(""); + } + } + + private static String sdkId(Path modelsDir, Path modelPath) { + return modelsDir.relativize(modelPath).getName(0).toString(); + } + + private static String sanitize(String name) { + return name.toLowerCase().replaceAll("[^a-z0-9]", ""); + } + + /** + * In-memory source file — wraps generated code string as JavaFileObject. + */ + private static class InMemoryJavaSource extends SimpleJavaFileObject { + private final String code; + + InMemoryJavaSource(String path, String code) { + super(URI.create("string:///" + path.replace('\\', '/')), Kind.SOURCE); + this.code = code; + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) { + return code; + } + } + + /** + * In-memory output manager — discards .class bytes. + */ + private static class InMemoryFileManager extends ForwardingJavaFileManager { + InMemoryFileManager(StandardJavaFileManager delegate) { + super(delegate); + } + + @Override + public JavaFileObject getJavaFileForOutput( + JavaFileManager.Location location, + String className, + JavaFileObject.Kind kind, + FileObject sibling + ) { + return new SimpleJavaFileObject( + URI.create("mem:///" + className.replace('.', '/') + kind.extension), + kind) { + @Override + public OutputStream openOutputStream() { + return new ByteArrayOutputStream(); + } + }; + } + } +} diff --git a/codegen/codegen-plugin/src/it/resources/junit-platform.properties b/codegen/codegen-plugin/src/it/resources/junit-platform.properties new file mode 100644 index 000000000..b10b0e321 --- /dev/null +++ b/codegen/codegen-plugin/src/it/resources/junit-platform.properties @@ -0,0 +1 @@ +junit.jupiter.execution.parallel.enabled = true \ No newline at end of file diff --git a/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/CodeGenerationContext.java b/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/CodeGenerationContext.java index 9b8073a18..a89ff9ac7 100644 --- a/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/CodeGenerationContext.java +++ b/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/CodeGenerationContext.java @@ -54,7 +54,6 @@ import software.amazon.smithy.model.traits.XmlFlattenedTrait; import software.amazon.smithy.model.traits.XmlNameTrait; import software.amazon.smithy.model.traits.XmlNamespaceTrait; -import software.amazon.smithy.utils.SmithyInternalApi; import software.amazon.smithy.utils.SmithyUnstableApi; /** @@ -114,16 +113,6 @@ public class CodeGenerationContext public CodeGenerationContext( CreateContextDirective directive, String plugin - ) { - this(directive, plugin, 64000); - } - - //Visible for testing - @SmithyInternalApi - CodeGenerationContext( - CreateContextDirective directive, - String plugin, - int schemaPartitionThreshold ) { this.model = directive.model(); this.settings = directive.settings(); @@ -137,7 +126,7 @@ public CodeGenerationContext( this.runtimeTraits = collectRuntimeTraits(); this.traitInitializers = collectTraitInitializers(); this.plugin = plugin; - this.schemaFieldOrder = new SchemaFieldOrder(directive, schemaPartitionThreshold, symbolProvider); + this.schemaFieldOrder = new SchemaFieldOrder(directive, this); } @Override diff --git a/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/DirectedJavaCodegen.java b/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/DirectedJavaCodegen.java index d5b25b208..fbb4f7f67 100644 --- a/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/DirectedJavaCodegen.java +++ b/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/DirectedJavaCodegen.java @@ -41,6 +41,7 @@ import software.amazon.smithy.java.codegen.server.generators.ServiceGenerator; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.utils.SmithyInternalApi; import software.amazon.smithy.utils.SmithyUnstableApi; /** @@ -51,6 +52,9 @@ final class DirectedJavaCodegen implements DirectedCodegen { private final Set modes; + //Visible For Testing + @SmithyInternalApi + CodeGenerationContext context; DirectedJavaCodegen(Set modes) { this.modes = modes; @@ -73,7 +77,8 @@ public CodeGenerationContext createContext( CreateContextDirective directive ) { String pluginName = getPluginName(); - return new CodeGenerationContext(directive, pluginName); + this.context = new CodeGenerationContext(directive, pluginName); + return context; } private String getPluginName() { diff --git a/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/client/generators/BddFileGenerator.java b/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/client/generators/BddFileGenerator.java index 8ce8d5f7c..47427e717 100644 --- a/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/client/generators/BddFileGenerator.java +++ b/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/client/generators/BddFileGenerator.java @@ -22,11 +22,12 @@ public class BddFileGenerator implements Consumer> { @Override public void accept(GenerateServiceDirective directive) { - var fileManifest = directive.fileManifest(); var serviceName = directive.service().toShapeId().getName(); var bytecode = compileBytecode(directive.service()); - fileManifest.writeFile(format("resources/META-INF/endpoints/%s.bdd", serviceName), - new ByteArrayInputStream(bytecode.getBytecode())); + directive.fileManifest() + .writeFile( + format("./resources/META-INF/endpoints/%s.bdd", serviceName), + new ByteArrayInputStream(bytecode.getBytecode())); } private Bytecode compileBytecode(ServiceShape serviceShape) { diff --git a/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/generators/SchemaFieldOrder.java b/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/generators/SchemaFieldOrder.java index 3c415effe..b4aa5d915 100644 --- a/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/generators/SchemaFieldOrder.java +++ b/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/generators/SchemaFieldOrder.java @@ -21,6 +21,7 @@ import software.amazon.smithy.codegen.core.SymbolProvider; import software.amazon.smithy.codegen.core.TopologicalIndex; import software.amazon.smithy.codegen.core.directed.Directive; +import software.amazon.smithy.java.codegen.CodeGenerationContext; import software.amazon.smithy.java.codegen.CodegenUtils; import software.amazon.smithy.java.codegen.SymbolProperties; import software.amazon.smithy.java.codegen.writer.JavaWriter; @@ -36,6 +37,11 @@ @SmithyInternalApi public final class SchemaFieldOrder { + private static final int FAST_PATH_THRESHOLD = 500; + //The default JVM limit for a class file is 64KB, we use 50KB as a safe limit + // because there will be other things in the class too like imports, etc. + private static final int SCHEMA_FILE_SIZE_THRESHOLD = 50_000; + private static final EnumSet EXCLUDED_TYPES = EnumSet.of( ShapeType.SERVICE, ShapeType.RESOURCE, @@ -48,7 +54,8 @@ public final class SchemaFieldOrder { private final Map reverseMapping; private final SymbolProvider symbolProvider; - public SchemaFieldOrder(Directive directive, long partitionThreshold, SymbolProvider symbolProvider) { + public SchemaFieldOrder(Directive directive, CodeGenerationContext context) { + this.symbolProvider = context.symbolProvider(); var connectedShapes = new HashSet<>(directive.connectedShapes().values()); var index = TopologicalIndex.of(directive.model()); var allShapes = Stream.concat(index.getOrderedShapes().stream(), index.getRecursiveShapes().stream()) @@ -57,47 +64,70 @@ public SchemaFieldOrder(Directive directive, long partitionThreshold, SymbolP .filter(s -> !EXCLUDED_TYPES.contains(s.getType())) .filter(not(Prelude::isPreludeShape)) .collect(Collectors.toCollection(LinkedHashSet::new)); - List> computedPartitions = new ArrayList<>(); - int curIndex = 0; - var curParition = new ArrayList(); - var curFieldNames = new HashSet(); - computedPartitions.add(curParition); - int curClassNumber = 0; - String curClassName = "Schemas"; + + // Phase 1: Assign field names and default className "Schemas" + var allFields = new ArrayList(); + var usedFieldNames = new HashSet(); for (var shape : allShapes) { - if (curIndex >= partitionThreshold) { - curIndex = 0; - curClassNumber++; - curClassName = "Schemas" + curClassNumber; - curFieldNames.clear(); - curParition = new ArrayList<>(); - computedPartitions.add(curParition); - } var shapeFieldName = toSchemaName(shape); - if (curFieldNames.contains(shapeFieldName) || shape.getId().getName().equals(shapeFieldName)) { + if (usedFieldNames.contains(shapeFieldName) || shape.getId().getName().equals(shapeFieldName)) { shapeFieldName = toFullQualifiedSchemaName(shape); } boolean isShapeRecursive = CodegenUtils.recursiveShape(directive.model(), shape); boolean isExternal = symbolProvider.toSymbol(shape).getProperty(SymbolProperties.EXTERNAL_TYPE).orElse(false); - var shapeField = new SchemaField(shape, shapeFieldName, curClassName, isShapeRecursive, isExternal); - curParition.add(shapeField); - curFieldNames.add(shapeFieldName); - if (isShapeRecursive) { - curIndex++; - } - curIndex++; + allFields.add( + new SchemaField(shape, shapeFieldName, SchemaClassRef.placeholder(), isShapeRecursive, isExternal)); + usedFieldNames.add(shapeFieldName); } - computedPartitions.removeIf(List::isEmpty); - this.partitions = Collections.unmodifiableList(computedPartitions); + + // Build reverseMapping so getSchemaFieldName() works during measurement Map map = new HashMap<>(); - for (var partition : this.partitions) { - for (var schemaField : partition) { - map.put(schemaField.shape.getId(), schemaField); - } + for (var field : allFields) { + map.put(field.shape().getId(), field); } this.reverseMapping = map; - this.symbolProvider = symbolProvider; + + // Phase 2: Partition + if (allFields.size() < FAST_PATH_THRESHOLD) { + // Fast path: keep all shapes in a single "Schemas" partition + this.partitions = allFields.isEmpty() + ? Collections.emptyList() + : List.of(List.copyOf(allFields)); + } else { + // Measure actual generated sizes and partition based on content size + int[] sizes = SchemasGenerator.measureShapeSizes(allFields, directive.model(), context, this); + this.partitions = computePartitions(allFields, sizes); + } + } + + private static List> computePartitions( + List allFields, + int[] sizes + ) { + List> result = new ArrayList<>(); + var currentPartition = new ArrayList(); + result.add(currentPartition); + int currentSize = 0; + int classNumber = 0; + String className = "Schemas"; + + for (int i = 0; i < allFields.size(); i++) { + var field = allFields.get(i); + int shapeSize = sizes[i]; + if (currentSize + shapeSize > SCHEMA_FILE_SIZE_THRESHOLD && !currentPartition.isEmpty()) { + classNumber++; + className = "Schemas" + classNumber; + currentPartition = new ArrayList<>(); + result.add(currentPartition); + currentSize = 0; + } + field.classRef.update(className); + currentPartition.add(field); + currentSize += shapeSize; + } + result.removeIf(List::isEmpty); + return Collections.unmodifiableList(result); } public SchemaField getSchemaField(ShapeId shape) { @@ -115,7 +145,7 @@ public String getSchemaFieldName(Shape shape, JavaWriter writer) { } else if (schemaField.isExternal()) { return writer.format("$L.$$SCHEMA", symbolProvider.toSymbol(shape)); } - return writer.format("$L.$L", schemaField.className(), schemaField.fieldName()); + return writer.format("$L.$L", schemaField.classRef().className(), schemaField.fieldName()); } private static String getSchemaType( @@ -133,7 +163,8 @@ private static String getSchemaType( "$T.$$SCHEMA", provider.toSymbol(shape)); } - return writer.format("Schemas.$L", toSchemaName(shape)); + throw new IllegalStateException( + "Shape " + shape.getId() + " is not in schema field order and has no known schema fallback"); } private static String toSchemaName(Shape shape) { @@ -145,5 +176,50 @@ private static String toFullQualifiedSchemaName(Shape shape) { .toUpperCase(Locale.ENGLISH); } - record SchemaField(Shape shape, String fieldName, String className, boolean isRecursive, boolean isExternal) {} + /** + * Represents a shape's schema field assignment in a Schemas partition class. + * Uses a class instead of a record so className can be updated during partitioning. + */ + public record SchemaField( + Shape shape, + String fieldName, + SchemaClassRef classRef, + boolean isRecursive, + boolean isExternal) {} + + public static final class SchemaClassRef { + private String className; + private boolean isPlaceholder; + + private SchemaClassRef(String className, boolean isPlaceholder) { + this.className = className; + this.isPlaceholder = isPlaceholder; + } + + public static SchemaClassRef placeholder() { + return new SchemaClassRef("Schemas", true); + } + + public static SchemaClassRef of(String className) { + return new SchemaClassRef(className, false); + } + + public void update(String className) { + if (!isPlaceholder) { + throw new IllegalStateException("Trying to update a non-place holder SchemaClassRef. Existing : " + + this.className + ", Update Attempted With : " + className); + } + this.className = className; + this.isPlaceholder = false; + } + + public boolean isPlaceholder() { + return isPlaceholder; + } + + public String className() { + return className; + } + + } } diff --git a/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/generators/SchemaIndexGenerator.java b/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/generators/SchemaIndexGenerator.java index 1a2a70f0e..6879c5bf0 100644 --- a/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/generators/SchemaIndexGenerator.java +++ b/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/generators/SchemaIndexGenerator.java @@ -57,29 +57,53 @@ private void generateSchemaIndexClass( writer.putContext("map", Map.class); writer.putContext("hashMap", HashMap.class); - var template = """ - /** - * Generated SchemaIndex implementation that provides access to all schemas in the model. - */ - public final class ${className:L} extends ${schemaIndex:T} { + // Count total entries for pre-sizing the HashMap + var order = directive.context().schemaFieldOrder(); + int totalCount = 0; + for (var shapeOrder : order.partitions()) { + for (var schemaField : shapeOrder) { + if (!schemaField.isExternal()) { + totalCount++; + } + } + } + for (var shape : directive.connectedShapes().values()) { + if ((shape.getType() == ShapeType.ENUM || shape.getType() == ShapeType.INT_ENUM) + && !Prelude.isPreludeShape(shape) + && !shape.hasTrait(SyntheticTrait.class)) { + var symbol = directive.symbolProvider().toSymbol(shape); + if (!symbol.getProperty(SymbolProperties.EXTERNAL_TYPE).orElse(false)) { + totalCount++; + } + } + } - private static final ${map:T}<${shapeId:T}, ${schema:T}> SCHEMA_MAP = new ${hashMap:T}<>(); + var template = + """ + /** + * Generated SchemaIndex implementation that provides access to all schemas in the model. + */ + public final class ${className:L} extends ${schemaIndex:T} { - static { - ${schemaInitializers:C|} - } + private static final ${map:T}<${shapeId:T}, ${schema:T}> SCHEMA_MAP = new ${hashMap:T}<>(${mapCapacity:L}); - @Override - public ${schema:T} getSchema(${shapeId:T} id) { - return SCHEMA_MAP.get(id); - } + static { + ${staticInitCalls:C|} + } - @Override - public void visit(${consumer:T}<${schema:T}> visitor) { - SCHEMA_MAP.values().forEach(visitor); - } - } - """; + ${initMethods:C|} + + @Override + public ${schema:T} getSchema(${shapeId:T} id) { + return SCHEMA_MAP.get(id); + } + + @Override + public void visit(${consumer:T}<${schema:T}> visitor) { + SCHEMA_MAP.values().forEach(visitor); + } + } + """; writer.pushState(); writer.putContext("className", className); @@ -87,12 +111,17 @@ public void visit(${consumer:T}<${schema:T}> visitor) { writer.putContext("schema", Schema.class); writer.putContext("shapeId", ShapeId.class); writer.putContext("consumer", Consumer.class); - writer.putContext("schemaInitializers", new SchemaInitializersGenerator(writer, directive)); + writer.putContext("mapCapacity", (int) (totalCount / 0.75f) + 1); + writer.putContext("staticInitCalls", new StaticInitCallsGenerator(writer, directive)); + writer.putContext("initMethods", new InitMethodsGenerator(writer, directive)); writer.write(template); writer.popState(); } - private record SchemaInitializersGenerator( + /** + * Generates the calls to partition init methods within the static {} block. + */ + private record StaticInitCallsGenerator( JavaWriter writer, CustomizeDirective directive) implements Runnable { @@ -100,36 +129,80 @@ private record SchemaInitializersGenerator( @Override public void run() { var order = directive.context().schemaFieldOrder(); + var partitions = order.partitions(); + for (int i = 0; i < partitions.size(); i++) { + writer.write("_init$L();", i); + } + // Enums get their own init method + boolean hasEnums = directive.connectedShapes() + .values() + .stream() + .anyMatch(shape -> (shape.getType() == ShapeType.ENUM || shape.getType() == ShapeType.INT_ENUM) + && !Prelude.isPreludeShape(shape) + && !shape.hasTrait(SyntheticTrait.class)); + if (hasEnums) { + writer.write("_initEnums();"); + } + } + } - for (var shapeOrder : order.partitions()) { - for (var schemaField : shapeOrder) { + /** + * Generates the private static init method definitions. + */ + private record InitMethodsGenerator( + JavaWriter writer, + CustomizeDirective directive) + implements Runnable { + + @Override + public void run() { + var order = directive.context().schemaFieldOrder(); + var partitions = order.partitions(); + + // One method per schema partition + for (int i = 0; i < partitions.size(); i++) { + writer.openBlock("private static void _init$L() {", i); + for (var schemaField : partitions.get(i)) { if (schemaField.isExternal()) { continue; } - - var schemaReference = schemaField.className() + "." + schemaField.fieldName(); - + var schemaReference = schemaField.classRef().className() + "." + schemaField.fieldName(); writer.pushState(); writer.putContext("schemaReference", schemaReference); writer.write("SCHEMA_MAP.put(${schemaReference:L}.id(), ${schemaReference:L});"); writer.popState(); } + writer.closeBlock("}"); + writer.write(""); } - // Register enum and intEnum schemas (excluded from SchemaFieldOrder partitions) + // Enums init method + boolean hasEnums = false; for (var shape : directive.connectedShapes().values()) { if ((shape.getType() == ShapeType.ENUM || shape.getType() == ShapeType.INT_ENUM) && !Prelude.isPreludeShape(shape) && !shape.hasTrait(SyntheticTrait.class)) { - var symbol = directive.symbolProvider().toSymbol(shape); - if (symbol.getProperty(SymbolProperties.EXTERNAL_TYPE).orElse(false)) { - continue; + hasEnums = true; + break; + } + } + if (hasEnums) { + writer.openBlock("private static void _initEnums() {"); + for (var shape : directive.connectedShapes().values()) { + if ((shape.getType() == ShapeType.ENUM || shape.getType() == ShapeType.INT_ENUM) + && !Prelude.isPreludeShape(shape) + && !shape.hasTrait(SyntheticTrait.class)) { + var symbol = directive.symbolProvider().toSymbol(shape); + if (symbol.getProperty(SymbolProperties.EXTERNAL_TYPE).orElse(false)) { + continue; + } + writer.pushState(); + writer.putContext("enumClass", symbol); + writer.write("SCHEMA_MAP.put(${enumClass:T}.$$SCHEMA.id(), ${enumClass:T}.$$SCHEMA);"); + writer.popState(); } - writer.pushState(); - writer.putContext("enumClass", symbol); - writer.write("SCHEMA_MAP.put(${enumClass:T}.$$SCHEMA.id(), ${enumClass:T}.$$SCHEMA);"); - writer.popState(); } + writer.closeBlock("}"); } } } diff --git a/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/generators/SchemasGenerator.java b/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/generators/SchemasGenerator.java index d7ee1bd95..8b24359f1 100644 --- a/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/generators/SchemasGenerator.java +++ b/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/generators/SchemasGenerator.java @@ -9,7 +9,6 @@ import java.util.List; import java.util.function.Consumer; -import software.amazon.smithy.codegen.core.directed.ContextualDirective; import software.amazon.smithy.codegen.core.directed.CustomizeDirective; import software.amazon.smithy.java.codegen.CodeGenerationContext; import software.amazon.smithy.java.codegen.CodegenUtils; @@ -53,7 +52,7 @@ public void accept(CustomizeDirective new ResolverGenerator(writer, s)) .toList(); - var schemas = new StaticSchemaFieldsGenerator(directive, shapeOrder, writer); + var schemas = new StaticSchemaFieldsGenerator( + writer, + shapeOrder, + directive.model(), + directive.context(), + directive.context().schemaFieldOrder()); writer.pushState(); writer.putContext("className", className); writer.putContext("schemas", schemas); @@ -100,22 +104,102 @@ final class ${className:L} { } + /** + * Measures the generated code size for each shape in the list. + * Used during partitioning to estimate per-shape contribution to class file size. + * + * @param shapes ordered list of schema fields + * @param model the Smithy model + * @param context code generation context + * @param order the schema field order (may still be under construction) + * @return array of content sizes in characters, one per shape + */ + static int[] measureShapeSizes( + List shapes, + Model model, + CodeGenerationContext context, + SchemaFieldOrder order + ) { + int[] sizes = new int[shapes.size()]; + String namespace = CodegenUtils.getModelNamespace(context.settings()); + for (int i = 0; i < shapes.size(); i++) { + var field = shapes.get(i); + if (field.isExternal()) { + sizes[i] = 0; + continue; + } + var writer = new JavaWriter(context.settings(), namespace, "measurement"); + generateSingleShape(writer, field, model, context, order); + sizes[i] = writer.toContentString().length(); + } + return sizes; + } + + /** + * Generates all code for a single shape (builder + field + resolver) into the given writer. + * Used for measurement during partitioning. + */ + private static void generateSingleShape( + JavaWriter writer, + SchemaField schemaField, + Model model, + CodeGenerationContext context, + SchemaFieldOrder order + ) { + // Builder (if recursive) + if (schemaField.isRecursive()) { + new SchemaBuilderGenerator(writer, schemaField, model, context).run(); + } + // Schema field + writeSchemaField(writer, schemaField, model, context, order); + // Resolver (if recursive) + if (schemaField.isRecursive()) { + new ResolverGenerator(writer, schemaField).run(); + } + } + + private static void writeSchemaField( + JavaWriter writer, + SchemaField schemaField, + Model model, + CodeGenerationContext context, + SchemaFieldOrder order + ) { + writer.pushState(); + writer.putContext("schemaClass", Schema.class); + var originalId = CodegenUtils.getOriginalId(schemaField.shape()); + var isUnit = UnitTypeTrait.UNIT.equals(originalId); + writer.putContext("id", isUnit ? schemaField.shape().getId() : originalId); + writer.putContext("shapeId", ShapeId.class); + writer.putContext("schemaBuilder", SchemaBuilder.class); + writer.putContext("name", schemaField.fieldName()); + List extraTraits = isUnit ? List.of(new UnitTypeTrait()) : List.of(); + var traitGen = new TraitInitializerGenerator(writer, schemaField.shape(), context, extraTraits); + writer.putContext("traits", traitGen); + schemaField.shape().accept(new StaticSchemaFieldGenerator(writer, schemaField, model, context, order)); + writer.popState(); + } + private static final class StaticSchemaFieldsGenerator implements Runnable { private final JavaWriter writer; private final List schemaFields; private final CodeGenerationContext context; - private final ContextualDirective directive; + private final Model model; + private final SchemaFieldOrder order; private boolean insideStaticBlock; private StaticSchemaFieldsGenerator( - ContextualDirective directive, + JavaWriter writer, List schemaFields, - JavaWriter writer + Model model, + CodeGenerationContext context, + SchemaFieldOrder order ) { - this.directive = directive; this.writer = writer; this.schemaFields = schemaFields; - this.context = directive.context(); + this.model = model; + this.context = context; + this.order = order; this.insideStaticBlock = false; } @@ -133,247 +217,236 @@ public void run() { insideStaticBlock = false; writer.closeBlock("}\n"); } - writer.pushState(); - writer.putContext("schemaClass", Schema.class); - var originalId = CodegenUtils.getOriginalId(schemaField.shape()); - var isUnit = UnitTypeTrait.UNIT.equals(originalId); - writer.putContext("id", isUnit ? schemaField.shape().getId() : originalId); - writer.putContext("shapeId", ShapeId.class); - writer.putContext("schemaBuilder", SchemaBuilder.class); - writer.putContext("name", schemaField.fieldName()); - List extraTraits = isUnit ? List.of(new UnitTypeTrait()) : List.of(); - var traitGen = new TraitInitializerGenerator(writer, schemaField.shape(), context, extraTraits); - writer.putContext("traits", traitGen); - schemaField.shape().accept(new StaticSchemaFieldGenerator(writer, schemaField, directive)); - writer.popState(); + writeSchemaField(writer, schemaField, model, context, order); } if (insideStaticBlock) { writer.closeBlock("}\n"); } writer.popState(); } + } - private static final class StaticSchemaFieldGenerator extends ShapeVisitor.Default { - - private final JavaWriter writer; - private final SchemaField schemaField; - private final ContextualDirective directive; - private final Model model; - private final CodeGenerationContext context; - - private StaticSchemaFieldGenerator( - JavaWriter writer, - SchemaField schemaField, - ContextualDirective directive - ) { - this.writer = writer; - this.schemaField = schemaField; - this.directive = directive; - this.model = directive.model(); - this.context = directive.context(); - } + private static final class StaticSchemaFieldGenerator extends ShapeVisitor.Default { - @Override - protected Void getDefault(Shape shape) { - throw new IllegalStateException("Tried to create schema field for invalid shape: " + shape); - } + private final JavaWriter writer; + private final SchemaField schemaField; + private final Model model; + private final CodeGenerationContext context; + private final SchemaFieldOrder order; - @Override - public Void blobShape(BlobShape blobShape) { - writer.write( - "static final ${schemaClass:T} ${name:L} = ${schemaClass:T}.createBlob(${shapeId:T}.from(${id:S})${traits:C});"); - return null; - } + private StaticSchemaFieldGenerator( + JavaWriter writer, + SchemaField schemaField, + Model model, + CodeGenerationContext context, + SchemaFieldOrder order + ) { + this.writer = writer; + this.schemaField = schemaField; + this.model = model; + this.context = context; + this.order = order; + } - @Override - public Void booleanShape(BooleanShape booleanShape) { - writer.write( - "static final ${schemaClass:T} ${name:L} = ${schemaClass:T}.createBoolean(${shapeId:T}.from(${id:S})${traits:C});"); - return null; - } + @Override + protected Void getDefault(Shape shape) { + throw new IllegalStateException("Tried to create schema field for invalid shape: " + shape); + } - @Override - public Void listShape(ListShape shape) { - String template; - if (schemaField.isRecursive()) { - template = """ - ${name:L}_BUILDER - ${C|} - .build(); - """; - } else { - template = - """ - static final ${schemaClass:T} ${name:L} = ${schemaClass:T}.listBuilder(${shapeId:T}.from(${id:S})${traits:C}) - ${C|} - .build(); - """; - } - writer.write(template, (Runnable) () -> shape.getMember().accept(this)); + @Override + public Void blobShape(BlobShape blobShape) { + writer.write( + "static final ${schemaClass:T} ${name:L} = ${schemaClass:T}.createBlob(${shapeId:T}.from(${id:S})${traits:C});"); + return null; + } - return null; - } + @Override + public Void booleanShape(BooleanShape booleanShape) { + writer.write( + "static final ${schemaClass:T} ${name:L} = ${schemaClass:T}.createBoolean(${shapeId:T}.from(${id:S})${traits:C});"); + return null; + } - @Override - public Void mapShape(MapShape shape) { - String template; - if (schemaField.isRecursive()) { - template = """ - ${name:L}_BUILDER - ${C|} + @Override + public Void listShape(ListShape shape) { + String template; + if (schemaField.isRecursive()) { + template = """ + ${name:L}_BUILDER + ${C|} + .build(); + """; + } else { + template = + """ + static final ${schemaClass:T} ${name:L} = ${schemaClass:T}.listBuilder(${shapeId:T}.from(${id:S})${traits:C}) ${C|} .build(); - """; - } else { - template = - """ - static final ${schemaClass:T} ${name:L} = ${schemaClass:T}.mapBuilder(${shapeId:T}.from(${id:S})${traits:C}) - ${C|} - ${C|} - .build(); - """; - } - writer.write(template, - (Runnable) () -> shape.getKey().accept(this), - (Runnable) () -> shape.getValue().accept(this)); - return null; + """; } + writer.write(template, (Runnable) () -> shape.getMember().accept(this)); - @Override - public Void byteShape(ByteShape byteShape) { - writer.write( - "static final ${schemaClass:T} ${name:L} = ${schemaClass:T}.createByte(${shapeId:T}.from(${id:S})${traits:C});"); - return null; - } + return null; + } - @Override - public Void shortShape(ShortShape shortShape) { - writer.write( - "static final ${schemaClass:T} ${name:L} = ${schemaClass:T}.createShort(${shapeId:T}.from(${id:S})${traits:C});"); - return null; + @Override + public Void mapShape(MapShape shape) { + String template; + if (schemaField.isRecursive()) { + template = """ + ${name:L}_BUILDER + ${C|} + ${C|} + .build(); + """; + } else { + template = + """ + static final ${schemaClass:T} ${name:L} = ${schemaClass:T}.mapBuilder(${shapeId:T}.from(${id:S})${traits:C}) + ${C|} + ${C|} + .build(); + """; } + writer.write(template, + (Runnable) () -> shape.getKey().accept(this), + (Runnable) () -> shape.getValue().accept(this)); + return null; + } - @Override - public Void integerShape(IntegerShape integerShape) { - writer.write( - "static final ${schemaClass:T} ${name:L} = ${schemaClass:T}.createInteger(${shapeId:T}.from(${id:S})${traits:C});"); - return null; - } + @Override + public Void byteShape(ByteShape byteShape) { + writer.write( + "static final ${schemaClass:T} ${name:L} = ${schemaClass:T}.createByte(${shapeId:T}.from(${id:S})${traits:C});"); + return null; + } - @Override - public Void longShape(LongShape longShape) { - writer.write( - "static final ${schemaClass:T} ${name:L} = ${schemaClass:T}.createLong(${shapeId:T}.from(${id:S})${traits:C});"); - return null; - } + @Override + public Void shortShape(ShortShape shortShape) { + writer.write( + "static final ${schemaClass:T} ${name:L} = ${schemaClass:T}.createShort(${shapeId:T}.from(${id:S})${traits:C});"); + return null; + } - @Override - public Void floatShape(FloatShape floatShape) { - writer.write( - "static final ${schemaClass:T} ${name:L} = ${schemaClass:T}.createFloat(${shapeId:T}.from(${id:S})${traits:C});"); - return null; - } + @Override + public Void integerShape(IntegerShape integerShape) { + writer.write( + "static final ${schemaClass:T} ${name:L} = ${schemaClass:T}.createInteger(${shapeId:T}.from(${id:S})${traits:C});"); + return null; + } - @Override - public Void documentShape(DocumentShape documentShape) { - writer.write( - "static final ${schemaClass:T} ${name:L} = ${schemaClass:T}.createDocument(${shapeId:T}.from(${id:S})${traits:C});"); - return null; - } + @Override + public Void longShape(LongShape longShape) { + writer.write( + "static final ${schemaClass:T} ${name:L} = ${schemaClass:T}.createLong(${shapeId:T}.from(${id:S})${traits:C});"); + return null; + } - @Override - public Void doubleShape(DoubleShape doubleShape) { - writer.write( - "static final ${schemaClass:T} ${name:L} = ${schemaClass:T}.createDouble(${shapeId:T}.from(${id:S})${traits:C});"); - return null; - } + @Override + public Void floatShape(FloatShape floatShape) { + writer.write( + "static final ${schemaClass:T} ${name:L} = ${schemaClass:T}.createFloat(${shapeId:T}.from(${id:S})${traits:C});"); + return null; + } - @Override - public Void bigIntegerShape(BigIntegerShape bigIntegerShape) { - writer.write( - "static final ${schemaClass:T} ${name:L} = ${schemaClass:T}.createBigInteger(${shapeId:T}.from(${id:S})${traits:C});"); - return null; - } + @Override + public Void documentShape(DocumentShape documentShape) { + writer.write( + "static final ${schemaClass:T} ${name:L} = ${schemaClass:T}.createDocument(${shapeId:T}.from(${id:S})${traits:C});"); + return null; + } - @Override - public Void bigDecimalShape(BigDecimalShape bigDecimalShape) { - writer.write( - "static final ${schemaClass:T} ${name:L} = ${schemaClass:T}.createBigDecimal(${shapeId:T}.from(${id:S})${traits:C});"); - return null; - } + @Override + public Void doubleShape(DoubleShape doubleShape) { + writer.write( + "static final ${schemaClass:T} ${name:L} = ${schemaClass:T}.createDouble(${shapeId:T}.from(${id:S})${traits:C});"); + return null; + } - @Override - public Void structureShape(StructureShape shape) { - generateStructMemberSchemas(shape, "structureBuilder"); - return null; - } + @Override + public Void bigIntegerShape(BigIntegerShape bigIntegerShape) { + writer.write( + "static final ${schemaClass:T} ${name:L} = ${schemaClass:T}.createBigInteger(${shapeId:T}.from(${id:S})${traits:C});"); + return null; + } - @Override - public Void unionShape(UnionShape shape) { - generateStructMemberSchemas(shape, "unionBuilder"); - return null; - } + @Override + public Void bigDecimalShape(BigDecimalShape bigDecimalShape) { + writer.write( + "static final ${schemaClass:T} ${name:L} = ${schemaClass:T}.createBigDecimal(${shapeId:T}.from(${id:S})${traits:C});"); + return null; + } - private void generateStructMemberSchemas(Shape shape, String builderMethod) { - String template; - if (schemaField.isRecursive()) { - template = """ - ${name:L}_BUILDER${?hasMembers} - ${C|} - ${/hasMembers}.builderSupplier(${schemaTypeClass:T}::builder) - .shapeClass(${schemaTypeClass:T}.class) - .build(); - """; - } else { - template = - """ - static final ${schemaClass:T} ${name:L} = ${schemaClass:T}.${builderMethod:L}(${shapeId:T}.from(${id:S})${traits:C})${?hasMembers} - ${C|} - ${/hasMembers}.builderSupplier(${schemaTypeClass:T}::builder) - .shapeClass(${schemaTypeClass:T}.class) - .build(); - """; - } - writer.pushState(); - writer.putContext("hasMembers", !shape.members().isEmpty()); - writer.putContext("builderMethod", builderMethod); - writer.putContext("schemaTypeClass", context.symbolProvider().toSymbol(shape)); + @Override + public Void structureShape(StructureShape shape) { + generateStructMemberSchemas(shape, "structureBuilder"); + return null; + } - writer.write( - template, - (Runnable) () -> shape.members().forEach(m -> m.accept(this))); + @Override + public Void unionShape(UnionShape shape) { + generateStructMemberSchemas(shape, "unionBuilder"); + return null; + } - writer.popState(); + private void generateStructMemberSchemas(Shape shape, String builderMethod) { + String template; + if (schemaField.isRecursive()) { + template = """ + ${name:L}_BUILDER${?hasMembers} + ${C|} + ${/hasMembers}.builderSupplier(${schemaTypeClass:T}::builder) + .shapeClass(${schemaTypeClass:T}.class) + .build(); + """; + } else { + template = + """ + static final ${schemaClass:T} ${name:L} = ${schemaClass:T}.${builderMethod:L}(${shapeId:T}.from(${id:S})${traits:C})${?hasMembers} + ${C|} + ${/hasMembers}.builderSupplier(${schemaTypeClass:T}::builder) + .shapeClass(${schemaTypeClass:T}.class) + .build(); + """; } + writer.pushState(); + writer.putContext("hasMembers", !shape.members().isEmpty()); + writer.putContext("builderMethod", builderMethod); + writer.putContext("schemaTypeClass", context.symbolProvider().toSymbol(shape)); - @Override - public Void memberShape(MemberShape shape) { - var target = model.expectShape(shape.getTarget()); - writer.pushState(); - writer.putContext("memberName", shape.getMemberName()); - writer.putContext("schema", directive.context().schemaFieldOrder().getSchemaFieldName(target, writer)); - writer.putContext("traits", new TraitInitializerGenerator(writer, shape, context)); - writer.putContext("recursive", CodegenUtils.recursiveShape(model, target)); - writer.write(".putMember(${memberName:S}, ${schema:L}${?recursive}_BUILDER${/recursive}${traits:C})"); - writer.popState(); - return null; - } + writer.write( + template, + (Runnable) () -> shape.members().forEach(m -> m.accept(this))); - @Override - public Void timestampShape(TimestampShape timestampShape) { - writer.write( - "static final ${schemaClass:T} ${name:L} = ${schemaClass:T}.createTimestamp(${shapeId:T}.from(${id:S})${traits:C});"); - return null; - } + writer.popState(); + } - @Override - public Void stringShape(StringShape stringShape) { - writer.write( - "static final ${schemaClass:T} ${name:L} = ${schemaClass:T}.createString(${shapeId:T}.from(${id:S})${traits:C});"); - return null; - } + @Override + public Void memberShape(MemberShape shape) { + var target = model.expectShape(shape.getTarget()); + writer.pushState(); + writer.putContext("memberName", shape.getMemberName()); + writer.putContext("schema", order.getSchemaFieldName(target, writer)); + writer.putContext("traits", new TraitInitializerGenerator(writer, shape, context)); + writer.putContext("recursive", CodegenUtils.recursiveShape(model, target)); + writer.write(".putMember(${memberName:S}, ${schema:L}${?recursive}_BUILDER${/recursive}${traits:C})"); + writer.popState(); + return null; } + @Override + public Void timestampShape(TimestampShape timestampShape) { + writer.write( + "static final ${schemaClass:T} ${name:L} = ${schemaClass:T}.createTimestamp(${shapeId:T}.from(${id:S})${traits:C});"); + return null; + } + + @Override + public Void stringShape(StringShape stringShape) { + writer.write( + "static final ${schemaClass:T} ${name:L} = ${schemaClass:T}.createString(${shapeId:T}.from(${id:S})${traits:C});"); + return null; + } } private static final class SchemaBuilderGenerator extends ShapeVisitor.Default implements Runnable { diff --git a/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/server/generators/ServiceGenerator.java b/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/server/generators/ServiceGenerator.java index fef77bd89..65c501e93 100644 --- a/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/server/generators/ServiceGenerator.java +++ b/codegen/codegen-plugin/src/main/java/software/amazon/smithy/java/codegen/server/generators/ServiceGenerator.java @@ -252,10 +252,11 @@ public void run() { Symbol syncOperation = operation.expectProperty(ServerSymbolProperties.STUB_OPERATION); Symbol asyncOperation = operation.expectProperty(ServerSymbolProperties.ASYNC_STUB_OPERATION); writer.putContext("operation", operation); + writer.putContext("operationName", operation.getName()); writer.write(""" public interface ${curStage:L} { - ${nextStage:L} add${operation:T}Operation($T operation); - ${nextStage:L} add${operation:T}Operation($T operation); + ${nextStage:L} add${operationName:L}Operation($T operation); + ${nextStage:L} add${operationName:L}Operation($T operation); } """, syncOperation, asyncOperation); writer.popState(); @@ -303,13 +304,13 @@ private void generateStages(JavaWriter writer, List stages) { var template = """ @Override - public ${nextStage:L} add${operation:T}Operation(${asyncOperationType:T} operation) { + public ${nextStage:L} add${operationName:L}Operation(${asyncOperationType:T} operation) { this.${operationFieldName:L} = s -> ${operationClass:T}.ofAsync("${operation:T}", operation::${operationFieldName:L}, ${apiOperationClass:T}.instance(), s); return this; } @Override - public ${nextStage:L} add${operation:T}Operation(${syncOperationType:T} operation) { + public ${nextStage:L} add${operationName:L}Operation(${syncOperationType:T} operation) { this.${operationFieldName:L} = s -> ${operationClass:T}.of("${operation:T}", operation::${operationFieldName:L}, ${apiOperationClass:T}.instance(), s); return this; } @@ -317,6 +318,7 @@ private void generateStages(JavaWriter writer, List stages) { writer.putContext("operationFieldName", operationFieldName); writer.putContext("nextStage", nextStage); writer.putContext("operation", operation); + writer.putContext("operationName", operation.getName()); writer.putContext("asyncOperationType", asyncOperation); writer.putContext("syncOperationType", syncOperation); writer.putContext("apiOperationClass", apiOperation); diff --git a/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/CodegenContextTest.java b/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/CodegenContextTest.java index 4757fb9ef..6f8a20028 100644 --- a/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/CodegenContextTest.java +++ b/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/CodegenContextTest.java @@ -14,7 +14,6 @@ import org.junit.jupiter.api.Test; import software.amazon.smithy.build.MockManifest; import software.amazon.smithy.build.PluginContext; -import software.amazon.smithy.java.codegen.utils.TestJavaCodegenPlugin; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.node.ArrayNode; import software.amazon.smithy.model.node.Node; diff --git a/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/SchemasTest.java b/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/SchemasTest.java index 16174b770..4771166c7 100644 --- a/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/SchemasTest.java +++ b/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/SchemasTest.java @@ -13,7 +13,6 @@ import org.junit.jupiter.api.Test; import software.amazon.smithy.build.MockManifest; import software.amazon.smithy.build.PluginContext; -import software.amazon.smithy.java.codegen.utils.TestJavaCodegenPlugin; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.node.ObjectNode; @@ -24,7 +23,7 @@ public class SchemasTest { @Test void largeNumberOfSchemasTest() { - int totalOperations = TestJavaCodegen.SCHEMA_PARTITION_THRESHOLD + 12; //Add some random number + int totalOperations = 250; var smithyDefinition = new StringBuilder(""" $version: "2" namespace s.j @@ -65,31 +64,29 @@ void largeNumberOfSchemasTest() { var schemaFiles = files.stream().map(Path::getFileName).map(Path::toString).filter(s -> s.startsWith("Schema")).toList(); - //Verify each Schema class has no more 100 constants. + // Verify multiple Schema partition files were generated assertThat(schemaFiles) - .hasSize(3) - .containsExactlyInAnyOrder("Schemas.java", "Schemas1.java", "Schemas2.java") - .allSatisfy(s -> { - var content = getFileString(s); - long numberOfSchemas = - Pattern.compile("static final Schema ").matcher(content).results().count(); - if ("Schemas2.java".equals(s)) { - assertThat(numberOfSchemas).isLessThan(100); - } else { - assertThat(numberOfSchemas).isEqualTo(100); - } - }); + .hasSizeGreaterThanOrEqualTo(2) + .contains("Schemas.java") + .allMatch(s -> s.matches("Schemas\\d*\\.java")); + + // Verify total schema count across all partition files matches expected + long totalSchemas = schemaFiles.stream() + .mapToLong(s -> Pattern.compile("static final Schema ") + .matcher(getFileString(s)) + .results() + .count()) + .sum(); + assertThat(totalSchemas).isEqualTo(totalOperations * 2L); - //Verify mappings are correct in structure classes - verifySchemaReference("Op001Input", "Schemas"); - verifySchemaReference("Op060Input", "Schemas1"); - verifySchemaReference("Op104Output", "Schemas2"); + // Verify schema references use $N pattern + verifySchemaReference("Op001Input"); + verifySchemaReference("Op100Output"); } - private void verifySchemaReference(String structureName, String expectedSchema) { + private void verifySchemaReference(String structureName) { assertThat(getFileString(structureName + ".java")) - .contains("public static final Schema $SCHEMA = %s.%s".formatted(expectedSchema, - CodegenUtils.toUpperSnakeCase(structureName))); + .containsPattern("public static final Schema \\$SCHEMA = Schemas\\d*\\.[A-Z][A-Z0-9_]+"); } private String getFileString(String fileName) { diff --git a/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/TestJavaCodegen.java b/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/TestJavaCodegen.java deleted file mode 100644 index fe044301f..000000000 --- a/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/TestJavaCodegen.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package software.amazon.smithy.java.codegen; - -import software.amazon.smithy.codegen.core.Symbol; -import software.amazon.smithy.codegen.core.SymbolProvider; -import software.amazon.smithy.codegen.core.directed.CreateContextDirective; -import software.amazon.smithy.codegen.core.directed.CreateSymbolProviderDirective; -import software.amazon.smithy.codegen.core.directed.CustomizeDirective; -import software.amazon.smithy.codegen.core.directed.DirectedCodegen; -import software.amazon.smithy.codegen.core.directed.GenerateEnumDirective; -import software.amazon.smithy.codegen.core.directed.GenerateErrorDirective; -import software.amazon.smithy.codegen.core.directed.GenerateIntEnumDirective; -import software.amazon.smithy.codegen.core.directed.GenerateListDirective; -import software.amazon.smithy.codegen.core.directed.GenerateMapDirective; -import software.amazon.smithy.codegen.core.directed.GenerateOperationDirective; -import software.amazon.smithy.codegen.core.directed.GenerateResourceDirective; -import software.amazon.smithy.codegen.core.directed.GenerateServiceDirective; -import software.amazon.smithy.codegen.core.directed.GenerateStructureDirective; -import software.amazon.smithy.codegen.core.directed.GenerateUnionDirective; -import software.amazon.smithy.java.codegen.generators.ApiServiceGenerator; -import software.amazon.smithy.java.codegen.generators.EnumGenerator; -import software.amazon.smithy.java.codegen.generators.ListGenerator; -import software.amazon.smithy.java.codegen.generators.MapGenerator; -import software.amazon.smithy.java.codegen.generators.OperationGenerator; -import software.amazon.smithy.java.codegen.generators.ResourceGenerator; -import software.amazon.smithy.java.codegen.generators.SchemasGenerator; -import software.amazon.smithy.java.codegen.generators.ServiceExceptionGenerator; -import software.amazon.smithy.java.codegen.generators.SharedSerdeGenerator; -import software.amazon.smithy.java.codegen.generators.StructureGenerator; -import software.amazon.smithy.java.codegen.generators.UnionGenerator; -import software.amazon.smithy.model.shapes.ServiceShape; -import software.amazon.smithy.utils.SmithyUnstableApi; - -@SmithyUnstableApi -public class TestJavaCodegen implements - DirectedCodegen { - - static final int SCHEMA_PARTITION_THRESHOLD = 100; - - public CodeGenerationContext context; - - @Override - public SymbolProvider createSymbolProvider( - CreateSymbolProviderDirective directive - ) { - return new JavaSymbolProvider( - directive.model(), - directive.service(), - directive.settings().packageNamespace(), - directive.settings().name(), - java.util.Set.of()) { - @Override - public Symbol serviceShape(ServiceShape serviceShape) { - var serviceName = directive.settings().name(); - return Symbol.builder() - .name(serviceName) - .namespace(directive.settings().packageNamespace(), ".") - .putProperty(SymbolProperties.SERVICE_API_SERVICE, - CodegenUtils.getServiceApiSymbol(packageNamespace(), serviceName)) - .putProperty(SymbolProperties.SERVICE_EXCEPTION, - CodegenUtils.getServiceExceptionSymbol(packageNamespace(), serviceName)) - .build(); - } - }; - } - - @Override - public CodeGenerationContext createContext( - CreateContextDirective directive - ) { - this.context = new CodeGenerationContext( - directive, - "test", - SCHEMA_PARTITION_THRESHOLD); - return context; - } - - @Override - public void customizeBeforeIntegrations(CustomizeDirective directive) { - new SchemasGenerator().accept(directive); - new SharedSerdeGenerator().accept(directive); - } - - @Override - public void generateService(GenerateServiceDirective directive) { - new ApiServiceGenerator().accept(directive); - new ServiceExceptionGenerator<>().accept(directive); - } - - @Override - public void generateResource(GenerateResourceDirective directive) { - new ResourceGenerator().accept(directive); - } - - @Override - public void generateOperation(GenerateOperationDirective directive) { - new OperationGenerator().accept(directive); - } - - @Override - public void generateStructure(GenerateStructureDirective directive) { - new StructureGenerator<>().accept(directive); - } - - @Override - public void generateError(GenerateErrorDirective directive) { - new StructureGenerator<>().accept(directive); - } - - @Override - public void generateUnion(GenerateUnionDirective directive) { - new UnionGenerator().accept(directive); - } - - @Override - public void generateList(GenerateListDirective directive) { - new ListGenerator().accept(directive); - } - - @Override - public void generateMap(GenerateMapDirective directive) { - new MapGenerator().accept(directive); - } - - @Override - public void generateEnumShape(GenerateEnumDirective directive) { - new EnumGenerator<>().accept(directive); - } - - @Override - public void generateIntEnumShape(GenerateIntEnumDirective directive) { - new EnumGenerator<>().accept(directive); - } -} diff --git a/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/utils/TestJavaCodegenPlugin.java b/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/TestJavaCodegenPlugin.java similarity index 74% rename from codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/utils/TestJavaCodegenPlugin.java rename to codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/TestJavaCodegenPlugin.java index c9c2559ce..3b1a4cbe2 100644 --- a/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/utils/TestJavaCodegenPlugin.java +++ b/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/TestJavaCodegenPlugin.java @@ -3,16 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -package software.amazon.smithy.java.codegen.utils; +package software.amazon.smithy.java.codegen; +import java.util.Set; import software.amazon.smithy.build.PluginContext; import software.amazon.smithy.build.SmithyBuildPlugin; import software.amazon.smithy.codegen.core.directed.CodegenDirector; -import software.amazon.smithy.java.codegen.CodeGenerationContext; -import software.amazon.smithy.java.codegen.DefaultTransforms; -import software.amazon.smithy.java.codegen.JavaCodegenIntegration; -import software.amazon.smithy.java.codegen.JavaCodegenSettings; -import software.amazon.smithy.java.codegen.TestJavaCodegen; import software.amazon.smithy.java.codegen.writer.JavaWriter; public class TestJavaCodegenPlugin implements SmithyBuildPlugin { @@ -30,7 +26,7 @@ public void execute(PluginContext context) { var settings = JavaCodegenSettings.fromNode(context.getSettings()); runner.settings(settings); - TestJavaCodegen directedCodegen = new TestJavaCodegen(); + DirectedJavaCodegen directedCodegen = new DirectedJavaCodegen(Set.of(CodegenMode.CLIENT, CodegenMode.SERVER)); runner.directedCodegen(directedCodegen); runner.fileManifest(context.getFileManifest()); runner.service(settings.service()); diff --git a/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/generators/SnippetGeneratorTest.java b/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/generators/SnippetGeneratorTest.java index 6beeac506..65c764da4 100644 --- a/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/generators/SnippetGeneratorTest.java +++ b/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/generators/SnippetGeneratorTest.java @@ -18,7 +18,7 @@ import software.amazon.smithy.build.MockManifest; import software.amazon.smithy.build.PluginContext; import software.amazon.smithy.java.codegen.CodeGenerationContext; -import software.amazon.smithy.java.codegen.utils.TestJavaCodegenPlugin; +import software.amazon.smithy.java.codegen.TestJavaCodegenPlugin; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.node.ObjectNode; diff --git a/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/utils/AbstractCodegenFileTest.java b/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/utils/AbstractCodegenFileTest.java index a76352a59..3eade9406 100644 --- a/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/utils/AbstractCodegenFileTest.java +++ b/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/utils/AbstractCodegenFileTest.java @@ -14,6 +14,7 @@ import software.amazon.smithy.build.MockManifest; import software.amazon.smithy.build.PluginContext; import software.amazon.smithy.build.SmithyBuildPlugin; +import software.amazon.smithy.java.codegen.TestJavaCodegenPlugin; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.node.ObjectNode; diff --git a/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/utils/TestJavaCodegenRunner.java b/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/utils/TestJavaCodegenRunner.java index aaf432938..1ea26e372 100644 --- a/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/utils/TestJavaCodegenRunner.java +++ b/codegen/codegen-plugin/src/test/java/software/amazon/smithy/java/codegen/utils/TestJavaCodegenRunner.java @@ -8,6 +8,7 @@ import java.nio.file.Paths; import software.amazon.smithy.build.FileManifest; import software.amazon.smithy.build.PluginContext; +import software.amazon.smithy.java.codegen.TestJavaCodegenPlugin; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.node.ObjectNode;