Skip to content

Commit

Permalink
Allow generating shapes without a service
Browse files Browse the repository at this point in the history
  • Loading branch information
JordonPhillips committed Feb 3, 2025
1 parent 3b9f6f6 commit 46b6ba3
Show file tree
Hide file tree
Showing 5 changed files with 281 additions and 31 deletions.
94 changes: 70 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,35 +179,81 @@ would now look like:
The module with the generated shape classes can be found in
`build/smithy/client/python-shape-codegen` after you run `smithy-build`.

Note that a service shape is still required. In this case, it's used for the
purposes of namespacing since all shapes within a service's closure must have a
unique name. The service shape also has the `rename` property to resolve any
conflicts you might encounter.
Unlike when generating a client, a service shape is not required for shape
generation. If a service is not provided then every shape found in the model
will be generated. Any naming conflicts may be resolved by using the
[`renameShapes` transform](https://smithy.io/2.0/guides/smithy-build-json.html#renameshapes)
(or renaming the shapes in the model of course).

The one downside to this is that at time of writing there is no way to add
shapes to a service that aren't connected to it via an operation or error. In
the future, the service shape will have a `shapes` property or some other way of
doing this. For now, it is recommended to just create a dummy operation to add
any shapes needed, like below:
The set of shapes generated can also be constrained by using the
[`includeShapesBySelector` transform](https://smithy.io/2.0/guides/smithy-build-json.html#includeshapesbyselector).
For example, to generate only shapes within the `com.example` namespace:

```smithy
$version: "2.0"
namespace com.example
service ShapeNamespaceService {
version: "2006-03-01"
operations: [ShapeContainer]
}
operation ShapeContainer {
input := {
example: ExampleShape
```json
{
"version": "1.0",
"sources": ["model"],
"maven": {
"dependencies": [
"software.amazon.smithy:smithy-model:[1.54.0,2.0)",
"software.amazon.smithy.python:smithy-python-codegen:0.1.0"
]
},
"projections": {
"shapes": {
"transforms": [
{
"name": "includeShapesBySelector",
"args": {
"selector": "[id|namespace = 'com.example']"
}
}
],
"plugins": {
"python-shape-codegen": {
"module": "echo",
"moduleVersion": "0.0.1"
}
}
}
}
}
```

structure ExampleShape {
intMember: Integer
Input and output shapes (shapes with the `@input` or `@output` traits and
operation inputs / outputs created as part of an operation definition) are not
generated by default. To generate these shapes anyway, remove the traits with
the
[`excludeTraits` transform](https://smithy.io/2.0/guides/smithy-build-json.html#excludetraits):

```json
{
"version": "1.0",
"sources": ["model"],
"maven": {
"dependencies": [
"software.amazon.smithy:smithy-model:[1.54.0,2.0)",
"software.amazon.smithy.python:smithy-python-codegen:0.1.0"
]
},
"projections": {
"shapes": {
"transforms": [
{
"name": "excludeTraits",
"args": {
"traits": ["input", "output"]
}
}
],
"plugins": {
"python-shape-codegen": {
"module": "echo",
"moduleVersion": "0.0.1"
}
}
}
}
}
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
import software.amazon.smithy.model.shapes.ServiceShape;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.traits.InputTrait;
import software.amazon.smithy.model.traits.OutputTrait;
import software.amazon.smithy.python.codegen.generators.ListGenerator;
import software.amazon.smithy.python.codegen.generators.MapGenerator;
import software.amazon.smithy.python.codegen.integration.ProtocolGenerator;
Expand Down Expand Up @@ -133,6 +135,8 @@ private void generateSchemas(GenerationContext context, Collection<Shape> shapes
.filter(shapes::contains)
.filter(shape -> !shape.isOperationShape() && !shape.isResourceShape()
&& !shape.isServiceShape() && !shape.isMemberShape() && !Prelude.isPreludeShape(shape))
// If we're only generating data shapes, there's no need to generate input or output shapes.
.filter(shape -> shape.isStructureShape() && !shouldGenerateStructure(context.settings(), shape))
.forEach(schemaGenerator);
schemaGenerator.finalizeRecursiveShapes();
}
Expand Down Expand Up @@ -204,6 +208,11 @@ public void generateResource(GenerateResourceDirective<GenerationContext, Python

@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.
if (!shouldGenerateStructure(directive.settings(), directive.shape())) {
return;
}

directive.context().writerDelegator().useShapeWriter(directive.shape(), writer -> {
StructureGenerator generator = new StructureGenerator(
directive.context(),
Expand All @@ -215,6 +224,14 @@ public void generateStructure(GenerateStructureDirective<GenerationContext, Pyth
});
}

private boolean shouldGenerateStructure(PythonSettings settings, Shape shape) {
if (shape.getId().getNamespace().equals("smithy.synthetic")) {
return false;
}
return !(settings.artifactType().equals(PythonSettings.ArtifactType.SHAPES)
&& (shape.hasTrait(InputTrait.class) || shape.hasTrait(OutputTrait.class)));
}

@Override
public void generateError(GenerateErrorDirective<GenerationContext, PythonSettings> directive) {
directive.context().writerDelegator().useShapeWriter(directive.shape(), writer -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public record PythonSettings(
}
}

public PythonSettings(Builder builder) {
private PythonSettings(Builder builder) {
this(
builder.service,
builder.moduleName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,32 @@

package software.amazon.smithy.python.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.model.Model;
import software.amazon.smithy.model.shapes.OperationShape;
import software.amazon.smithy.model.shapes.ServiceShape;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.shapes.ShapeType;
import software.amazon.smithy.model.shapes.StructureShape;
import software.amazon.smithy.model.traits.ErrorTrait;
import software.amazon.smithy.model.traits.InputTrait;
import software.amazon.smithy.model.traits.MixinTrait;
import software.amazon.smithy.model.traits.OutputTrait;
import software.amazon.smithy.model.transform.ModelTransformer;
import software.amazon.smithy.python.codegen.integration.PythonIntegration;

public final class PythonShapeCodegenPlugin implements SmithyBuildPlugin {
private static final String SYNTHETIC_NAMESPACE = "smithy.synthetic";
private static final ShapeId SYNTHETIC_SERVICE_ID = ShapeId.fromParts(SYNTHETIC_NAMESPACE, "TypesGenService");
private static final ShapeId SYNTHETIC_OPERATION_ID = ShapeId.fromParts(SYNTHETIC_NAMESPACE, "TypesGenOperation");
private static final ShapeId SYNTHETIC_INPUT_ID = ShapeId.fromParts(SYNTHETIC_NAMESPACE, "TypesGenOperationInput");
private static final Set<ShapeType> GENERATED_TYPES = Set.of(
ShapeType.STRUCTURE, ShapeType.UNION, ShapeType.ENUM, ShapeType.INT_ENUM);

@Override
public String getName() {
return "python-shape-codegen";
Expand All @@ -21,17 +41,57 @@ public void execute(PluginContext context) {
CodegenDirector<PythonWriter, PythonIntegration, GenerationContext, PythonSettings> runner
= new CodegenDirector<>();

PythonSettings settings = PythonSettings.fromNode(context.getSettings()).toBuilder()
.artifactType(PythonSettings.ArtifactType.SHAPES)
.build();
var shapeSettings = PythonShapeSettings.fromNode(context.getSettings());
var service = shapeSettings.service().orElse(SYNTHETIC_SERVICE_ID);
var pythonSettings = shapeSettings.toPythonSettings(service);

var model = context.getModel();
if (shapeSettings.service().isEmpty()) {
model = addSyntheticService(model);
}

runner.settings(settings);
runner.settings(pythonSettings);
runner.directedCodegen(new DirectedPythonCodegen());
runner.fileManifest(context.getFileManifest());
runner.service(settings.service());
runner.model(context.getModel());
runner.service(pythonSettings.service());
runner.model(model);
runner.integrationClass(PythonIntegration.class);
runner.performDefaultCodegenTransforms();
runner.run();
}

private Model addSyntheticService(Model model) {
StructureShape.Builder inputBuilder = StructureShape.builder()
.id(SYNTHETIC_INPUT_ID)
.addTrait(new InputTrait());

OperationShape.Builder operationBuilder = OperationShape.builder()
.id(SYNTHETIC_OPERATION_ID)
.input(SYNTHETIC_INPUT_ID);

var index = 0;
for (Shape shape : model.toSet()) {
if (!GENERATED_TYPES.contains(shape.getType())
|| shape.hasTrait(InputTrait.class)
|| shape.hasTrait(OutputTrait.class)
|| shape.hasTrait(MixinTrait.class)) {
continue;
}

if (shape.hasTrait(ErrorTrait.class)) {
operationBuilder.addError(shape.getId());
} else {
inputBuilder.addMember(String.valueOf(index), shape.getId());
index++;
}
}

ServiceShape service = ServiceShape.builder()
.id(SYNTHETIC_SERVICE_ID)
.addOperation(SYNTHETIC_OPERATION_ID)
.build();

ModelTransformer transformer = ModelTransformer.create();
return transformer.replaceShapes(model, Set.of(inputBuilder.build(), operationBuilder.build(), service));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.python.codegen;

import java.util.Arrays;
import java.util.Optional;
import software.amazon.smithy.model.node.ObjectNode;
import software.amazon.smithy.model.node.StringNode;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.utils.SmithyBuilder;
import software.amazon.smithy.utils.SmithyUnstableApi;
import software.amazon.smithy.utils.ToSmithyBuilder;

/**
* Settings used by {@link PythonClientCodegenPlugin}.
*
* @param service The id of the service that is being generated.
* @param moduleName The name of the module to generate.
* @param moduleVersion The version of the module to generate.
* @param moduleDescription The optional module description for the module that will be generated.
*/
@SmithyUnstableApi
public record PythonShapeSettings(
Optional<ShapeId> service,
String moduleName,
String moduleVersion,
String moduleDescription
) implements ToSmithyBuilder<PythonShapeSettings> {

private static final String SERVICE = "service";
private static final String MODULE_NAME = "module";
private static final String MODULE_DESCRIPTION = "moduleDescription";
private static final String MODULE_VERSION = "moduleVersion";

private PythonShapeSettings(Builder builder) {
this(
Optional.ofNullable(builder.service),
builder.moduleName,
builder.moduleVersion,
builder.moduleDescription
);
}

@Override
public Builder toBuilder() {
Builder builder = builder()
.moduleName(moduleName)
.moduleVersion(moduleVersion)
.moduleDescription(moduleDescription);
service.ifPresent(builder::service);
return builder;
}

public PythonSettings toPythonSettings(ShapeId service) {
return PythonSettings.builder()
.service(service)
.moduleName(moduleName)
.moduleVersion(moduleVersion)
.moduleDescription(moduleDescription)
.artifactType(PythonSettings.ArtifactType.SHAPES)
.build();
}

public PythonSettings toPythonSettings() {
return toPythonSettings(service.get());
}

/**
* Create a settings object from a configuration object node.
*
* @param config Config object to load.
* @return Returns the extracted settings.
*/
public static PythonShapeSettings fromNode(ObjectNode config) {
config.warnIfAdditionalProperties(Arrays.asList(SERVICE, MODULE_NAME, MODULE_DESCRIPTION, MODULE_VERSION));

String moduleName = config.expectStringMember(MODULE_NAME).getValue();
Builder builder = builder()
.moduleName(moduleName)
.moduleVersion(config.expectStringMember(MODULE_VERSION).getValue());
config.getStringMember(SERVICE).map(StringNode::expectShapeId).ifPresent(builder::service);
config.getStringMember(MODULE_DESCRIPTION).map(StringNode::getValue).ifPresent(builder::moduleDescription);
return builder.build();
}

public static Builder builder() {
return new Builder();
}

public static class Builder implements SmithyBuilder<PythonShapeSettings> {

private ShapeId service;
private String moduleName;
private String moduleVersion;
private String moduleDescription;

@Override
public PythonShapeSettings build() {
SmithyBuilder.requiredState("moduleName", moduleName);
SmithyBuilder.requiredState("moduleVersion", moduleVersion);
return new PythonShapeSettings(this);
}

public Builder service(ShapeId service) {
this.service = service;
return this;
}

public Builder moduleName(String moduleName) {
this.moduleName = moduleName;
return this;
}

public Builder moduleVersion(String moduleVersion) {
this.moduleVersion = moduleVersion;
return this;
}

public Builder moduleDescription(String moduleDescription) {
this.moduleDescription = moduleDescription;
return this;
}
}
}

0 comments on commit 46b6ba3

Please sign in to comment.