Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@
*/
package software.amazon.smithy.python.codegen.types;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import software.amazon.smithy.build.FileManifest;
Expand Down Expand Up @@ -56,4 +62,119 @@ public void testCodegenWithoutService(@TempDir Path tempDir) {
.build();
plugin.execute(context);
}

@Test
public void testEveryShapeIsGeneratedAlongsideErrors(@TempDir Path tempDir) throws IOException {
// Error shapes get their own synthetic operation; the shapes next to them must too.
String models = generate(tempDir, errors() + structures(8), settings());

assertEquals(List.of(), missing(models, errorNames(), structureNames(8)), "shapes missing from models.py");
}

@Test
public void testInputsAndOutputsAreGeneratedWhenEnabled(@TempDir Path tempDir) throws IOException {
// @input and @output shapes get synthetic operations too, and are generated when asked for.
String models = generate(
tempDir,
errors() + structures(4) + operations(),
settings().withMember("generateInputsAndOutputs", true));

List<String> expected = new ArrayList<>(structureNames(4));
expected.addAll(ioNames());
assertEquals(List.of(), missing(models, errorNames(), expected), "shapes missing from models.py");
}

@Test
public void testInputsAndOutputsAreGeneratedForServiceWhenEnabled(@TempDir Path tempDir) throws IOException {
String service =
"service IoService { operations: [OpA, OpB, OpC, OpD, OpE, OpF], errors: [ErrorA, ErrorB, ErrorC, ErrorD, ErrorE] }\n";
String models = generate(
tempDir,
errors() + operations() + service,
settings().withMember("service", "example.types#IoService")
.withMember("generateInputsAndOutputs", true));

assertEquals(List.of(), missing(models, errorNames(), ioNames()), "shapes missing from models.py");
}

@Test
public void testInputsAndOutputsAreSkippedByDefault(@TempDir Path tempDir) throws IOException {
String models = generate(tempDir, errors() + structures(4) + operations(), settings());

assertEquals(List.of(), missing(models, errorNames(), structureNames(4)), "shapes missing from models.py");
List<String> generatedIo = ioNames().stream().filter(name -> models.contains("class " + name + ":")).toList();
assertEquals(List.of(), generatedIo, "input/output shapes generated without generateInputsAndOutputs");
}

private static final List<String> OPERATIONS = List.of("OpA", "OpB", "OpC", "OpD", "OpE", "OpF");

private static List<String> errorNames() {
return List.of("ErrorA", "ErrorB", "ErrorC", "ErrorD", "ErrorE");
}

private static List<String> structureNames(int count) {
return List.of("ShapeA", "ShapeB", "ShapeC", "ShapeD", "ShapeE", "ShapeF", "ShapeG", "ShapeH")
.subList(0, count);
}

private static List<String> ioNames() {
List<String> names = new ArrayList<>();
for (String operation : OPERATIONS) {
names.add(operation + "Input");
names.add(operation + "Output");
}
return names;
}

private static String errors() {
StringBuilder source = new StringBuilder();
for (String error : errorNames()) {
source.append("@error(\"client\")\nstructure ").append(error).append(" { message: String }\n");
}
return source.toString();
}

private static String structures(int count) {
StringBuilder source = new StringBuilder();
for (String structure : structureNames(count)) {
source.append("structure ").append(structure).append(" { value: String }\n");
}
return source.toString();
}

private static String operations() {
StringBuilder source = new StringBuilder();
for (String operation : OPERATIONS) {
source.append("operation ")
.append(operation)
.append(" { input := { value: String }, output := { value: String } }\n");
}
return source.toString();
}

private static ObjectNode.Builder settings() {
return ObjectNode.builder().withMember("module", "types_test").withMember("moduleVersion", "0.0.1");
}

private static String generate(Path tempDir, String shapes, ObjectNode.Builder settings) throws IOException {
Model model = Model.assembler()
.addUnparsedModel("test.smithy", "$version: \"2.0\"\nnamespace example.types\n" + shapes)
.assemble()
.unwrap();
PluginContext context = PluginContext.builder()
.fileManifest(FileManifest.create(tempDir))
.settings(settings.build())
.model(model)
.build();
new PythonTypeCodegenPlugin().execute(context);
return Files.readString(tempDir.resolve("src/types_test/models.py"));
}

// Errors are declared as `class Name(...)`, other structures as `class Name:`.
private static List<String> missing(String models, List<String> errors, List<String> structures) {
List<String> missing = new ArrayList<>();
errors.stream().filter(name -> !models.contains("class " + name + "(")).forEach(missing::add);
structures.stream().filter(name -> !models.contains("class " + name + ":")).forEach(missing::add);
return missing;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,14 @@ Model transform(ModelTransformer transformer, Model model) {
.addTrait(new InputTrait())
.addMember("member", shape.getId())
.build();
index++;
operationBuilder.input(input);
addedShapes.add(input);
}

serviceBuilder.addOperation(operationId);
addedShapes.add(operationBuilder.build());
// Every shape needs its own operation; a reused ID replaces the earlier one.
index++;
}

addedShapes.add(serviceBuilder.build());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@

final class DirectedPythonTypeCodegen
implements DirectedCodegen<GenerationContext, PythonSettings, PythonIntegration> {

private final boolean generateInputsAndOutputs;

DirectedPythonTypeCodegen(boolean generateInputsAndOutputs) {
this.generateInputsAndOutputs = generateInputsAndOutputs;
}

@Override
public SymbolProvider createSymbolProvider(CreateSymbolProviderDirective<PythonSettings> directive) {
return new PythonSymbolProvider(directive.model(), directive.settings());
Expand Down Expand Up @@ -79,7 +86,7 @@ public void generateService(GenerateServiceDirective<GenerationContext, PythonSe

@Override
public void generateStructure(GenerateStructureDirective<GenerationContext, PythonSettings> directive) {
// If we're only generating data shapes, there's no need to generate input or output shapes.
// Types-only codegen skips input and output shapes unless generateInputsAndOutputs is set.
if (!shouldGenerateStructure(directive.settings(), directive.shape())) {
return;
}
Expand All @@ -98,8 +105,9 @@ private boolean shouldGenerateStructure(PythonSettings settings, Shape shape) {
if (shape.getId().getNamespace().equals("smithy.synthetic")) {
return false;
}
return !(settings.artifactType().equals(PythonSettings.ArtifactType.TYPES)
&& (shape.hasTrait(InputTrait.class) || shape.hasTrait(OutputTrait.class)));
return generateInputsAndOutputs
|| !(settings.artifactType().equals(PythonSettings.ArtifactType.TYPES)
&& (shape.hasTrait(InputTrait.class) || shape.hasTrait(OutputTrait.class)));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public void execute(PluginContext context) {
}

runner.settings(pythonSettings);
runner.directedCodegen(new DirectedPythonTypeCodegen());
runner.directedCodegen(new DirectedPythonTypeCodegen(typeSettings.generateInputsAndOutputs()));
runner.fileManifest(context.getFileManifest());
runner.service(pythonSettings.service());
runner.model(model);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,13 @@ public PythonSettings toPythonSettings() {
* @return Returns the extracted settings.
*/
public static PythonTypeCodegenSettings fromNode(ObjectNode config) {
config.warnIfAdditionalProperties(Arrays.asList(SERVICE, MODULE_NAME, MODULE_DESCRIPTION, MODULE_VERSION));
config.warnIfAdditionalProperties(Arrays.asList(
SERVICE,
MODULE_NAME,
MODULE_DESCRIPTION,
MODULE_VERSION,
SELECTOR,
GENERATE_INPUTS_AND_OUTPUTS));

String moduleName = config.expectStringMember(MODULE_NAME).getValue();
Builder builder = builder()
Expand Down