Skip to content

Commit 46b6ba3

Browse files
Allow generating shapes without a service
1 parent 3b9f6f6 commit 46b6ba3

File tree

5 files changed

+281
-31
lines changed

5 files changed

+281
-31
lines changed

README.md

Lines changed: 70 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -179,35 +179,81 @@ would now look like:
179179
The module with the generated shape classes can be found in
180180
`build/smithy/client/python-shape-codegen` after you run `smithy-build`.
181181

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

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

193-
```smithy
194-
$version: "2.0"
195-
196-
namespace com.example
197-
198-
service ShapeNamespaceService {
199-
version: "2006-03-01"
200-
operations: [ShapeContainer]
201-
}
202-
203-
operation ShapeContainer {
204-
input := {
205-
example: ExampleShape
192+
```json
193+
{
194+
"version": "1.0",
195+
"sources": ["model"],
196+
"maven": {
197+
"dependencies": [
198+
"software.amazon.smithy:smithy-model:[1.54.0,2.0)",
199+
"software.amazon.smithy.python:smithy-python-codegen:0.1.0"
200+
]
201+
},
202+
"projections": {
203+
"shapes": {
204+
"transforms": [
205+
{
206+
"name": "includeShapesBySelector",
207+
"args": {
208+
"selector": "[id|namespace = 'com.example']"
209+
}
210+
}
211+
],
212+
"plugins": {
213+
"python-shape-codegen": {
214+
"module": "echo",
215+
"moduleVersion": "0.0.1"
216+
}
217+
}
218+
}
206219
}
207220
}
221+
```
208222

209-
structure ExampleShape {
210-
intMember: Integer
223+
Input and output shapes (shapes with the `@input` or `@output` traits and
224+
operation inputs / outputs created as part of an operation definition) are not
225+
generated by default. To generate these shapes anyway, remove the traits with
226+
the
227+
[`excludeTraits` transform](https://smithy.io/2.0/guides/smithy-build-json.html#excludetraits):
228+
229+
```json
230+
{
231+
"version": "1.0",
232+
"sources": ["model"],
233+
"maven": {
234+
"dependencies": [
235+
"software.amazon.smithy:smithy-model:[1.54.0,2.0)",
236+
"software.amazon.smithy.python:smithy-python-codegen:0.1.0"
237+
]
238+
},
239+
"projections": {
240+
"shapes": {
241+
"transforms": [
242+
{
243+
"name": "excludeTraits",
244+
"args": {
245+
"traits": ["input", "output"]
246+
}
247+
}
248+
],
249+
"plugins": {
250+
"python-shape-codegen": {
251+
"module": "echo",
252+
"moduleVersion": "0.0.1"
253+
}
254+
}
255+
}
256+
}
211257
}
212258
```
213259

codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/DirectedPythonCodegen.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@
5050
import software.amazon.smithy.model.shapes.ServiceShape;
5151
import software.amazon.smithy.model.shapes.Shape;
5252
import software.amazon.smithy.model.shapes.ShapeId;
53+
import software.amazon.smithy.model.traits.InputTrait;
54+
import software.amazon.smithy.model.traits.OutputTrait;
5355
import software.amazon.smithy.python.codegen.generators.ListGenerator;
5456
import software.amazon.smithy.python.codegen.generators.MapGenerator;
5557
import software.amazon.smithy.python.codegen.integration.ProtocolGenerator;
@@ -133,6 +135,8 @@ private void generateSchemas(GenerationContext context, Collection<Shape> shapes
133135
.filter(shapes::contains)
134136
.filter(shape -> !shape.isOperationShape() && !shape.isResourceShape()
135137
&& !shape.isServiceShape() && !shape.isMemberShape() && !Prelude.isPreludeShape(shape))
138+
// If we're only generating data shapes, there's no need to generate input or output shapes.
139+
.filter(shape -> shape.isStructureShape() && !shouldGenerateStructure(context.settings(), shape))
136140
.forEach(schemaGenerator);
137141
schemaGenerator.finalizeRecursiveShapes();
138142
}
@@ -204,6 +208,11 @@ public void generateResource(GenerateResourceDirective<GenerationContext, Python
204208

205209
@Override
206210
public void generateStructure(GenerateStructureDirective<GenerationContext, PythonSettings> directive) {
211+
// If we're only generating data shapes, there's no need to generate input or output shapes.
212+
if (!shouldGenerateStructure(directive.settings(), directive.shape())) {
213+
return;
214+
}
215+
207216
directive.context().writerDelegator().useShapeWriter(directive.shape(), writer -> {
208217
StructureGenerator generator = new StructureGenerator(
209218
directive.context(),
@@ -215,6 +224,14 @@ public void generateStructure(GenerateStructureDirective<GenerationContext, Pyth
215224
});
216225
}
217226

227+
private boolean shouldGenerateStructure(PythonSettings settings, Shape shape) {
228+
if (shape.getId().getNamespace().equals("smithy.synthetic")) {
229+
return false;
230+
}
231+
return !(settings.artifactType().equals(PythonSettings.ArtifactType.SHAPES)
232+
&& (shape.hasTrait(InputTrait.class) || shape.hasTrait(OutputTrait.class)));
233+
}
234+
218235
@Override
219236
public void generateError(GenerateErrorDirective<GenerationContext, PythonSettings> directive) {
220237
directive.context().writerDelegator().useShapeWriter(directive.shape(), writer -> {

codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/PythonSettings.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public record PythonSettings(
6363
}
6464
}
6565

66-
public PythonSettings(Builder builder) {
66+
private PythonSettings(Builder builder) {
6767
this(
6868
builder.service,
6969
builder.moduleName,

codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/PythonShapeCodegenPlugin.java

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,32 @@
55

66
package software.amazon.smithy.python.codegen;
77

8+
import java.util.Set;
89
import software.amazon.smithy.build.PluginContext;
910
import software.amazon.smithy.build.SmithyBuildPlugin;
1011
import software.amazon.smithy.codegen.core.directed.CodegenDirector;
12+
import software.amazon.smithy.model.Model;
13+
import software.amazon.smithy.model.shapes.OperationShape;
14+
import software.amazon.smithy.model.shapes.ServiceShape;
15+
import software.amazon.smithy.model.shapes.Shape;
16+
import software.amazon.smithy.model.shapes.ShapeId;
17+
import software.amazon.smithy.model.shapes.ShapeType;
18+
import software.amazon.smithy.model.shapes.StructureShape;
19+
import software.amazon.smithy.model.traits.ErrorTrait;
20+
import software.amazon.smithy.model.traits.InputTrait;
21+
import software.amazon.smithy.model.traits.MixinTrait;
22+
import software.amazon.smithy.model.traits.OutputTrait;
23+
import software.amazon.smithy.model.transform.ModelTransformer;
1124
import software.amazon.smithy.python.codegen.integration.PythonIntegration;
1225

1326
public final class PythonShapeCodegenPlugin implements SmithyBuildPlugin {
27+
private static final String SYNTHETIC_NAMESPACE = "smithy.synthetic";
28+
private static final ShapeId SYNTHETIC_SERVICE_ID = ShapeId.fromParts(SYNTHETIC_NAMESPACE, "TypesGenService");
29+
private static final ShapeId SYNTHETIC_OPERATION_ID = ShapeId.fromParts(SYNTHETIC_NAMESPACE, "TypesGenOperation");
30+
private static final ShapeId SYNTHETIC_INPUT_ID = ShapeId.fromParts(SYNTHETIC_NAMESPACE, "TypesGenOperationInput");
31+
private static final Set<ShapeType> GENERATED_TYPES = Set.of(
32+
ShapeType.STRUCTURE, ShapeType.UNION, ShapeType.ENUM, ShapeType.INT_ENUM);
33+
1434
@Override
1535
public String getName() {
1636
return "python-shape-codegen";
@@ -21,17 +41,57 @@ public void execute(PluginContext context) {
2141
CodegenDirector<PythonWriter, PythonIntegration, GenerationContext, PythonSettings> runner
2242
= new CodegenDirector<>();
2343

24-
PythonSettings settings = PythonSettings.fromNode(context.getSettings()).toBuilder()
25-
.artifactType(PythonSettings.ArtifactType.SHAPES)
26-
.build();
44+
var shapeSettings = PythonShapeSettings.fromNode(context.getSettings());
45+
var service = shapeSettings.service().orElse(SYNTHETIC_SERVICE_ID);
46+
var pythonSettings = shapeSettings.toPythonSettings(service);
47+
48+
var model = context.getModel();
49+
if (shapeSettings.service().isEmpty()) {
50+
model = addSyntheticService(model);
51+
}
2752

28-
runner.settings(settings);
53+
runner.settings(pythonSettings);
2954
runner.directedCodegen(new DirectedPythonCodegen());
3055
runner.fileManifest(context.getFileManifest());
31-
runner.service(settings.service());
32-
runner.model(context.getModel());
56+
runner.service(pythonSettings.service());
57+
runner.model(model);
3358
runner.integrationClass(PythonIntegration.class);
3459
runner.performDefaultCodegenTransforms();
3560
runner.run();
3661
}
62+
63+
private Model addSyntheticService(Model model) {
64+
StructureShape.Builder inputBuilder = StructureShape.builder()
65+
.id(SYNTHETIC_INPUT_ID)
66+
.addTrait(new InputTrait());
67+
68+
OperationShape.Builder operationBuilder = OperationShape.builder()
69+
.id(SYNTHETIC_OPERATION_ID)
70+
.input(SYNTHETIC_INPUT_ID);
71+
72+
var index = 0;
73+
for (Shape shape : model.toSet()) {
74+
if (!GENERATED_TYPES.contains(shape.getType())
75+
|| shape.hasTrait(InputTrait.class)
76+
|| shape.hasTrait(OutputTrait.class)
77+
|| shape.hasTrait(MixinTrait.class)) {
78+
continue;
79+
}
80+
81+
if (shape.hasTrait(ErrorTrait.class)) {
82+
operationBuilder.addError(shape.getId());
83+
} else {
84+
inputBuilder.addMember(String.valueOf(index), shape.getId());
85+
index++;
86+
}
87+
}
88+
89+
ServiceShape service = ServiceShape.builder()
90+
.id(SYNTHETIC_SERVICE_ID)
91+
.addOperation(SYNTHETIC_OPERATION_ID)
92+
.build();
93+
94+
ModelTransformer transformer = ModelTransformer.create();
95+
return transformer.replaceShapes(model, Set.of(inputBuilder.build(), operationBuilder.build(), service));
96+
}
3797
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.python.codegen;
7+
8+
import java.util.Arrays;
9+
import java.util.Optional;
10+
import software.amazon.smithy.model.node.ObjectNode;
11+
import software.amazon.smithy.model.node.StringNode;
12+
import software.amazon.smithy.model.shapes.ShapeId;
13+
import software.amazon.smithy.utils.SmithyBuilder;
14+
import software.amazon.smithy.utils.SmithyUnstableApi;
15+
import software.amazon.smithy.utils.ToSmithyBuilder;
16+
17+
/**
18+
* Settings used by {@link PythonClientCodegenPlugin}.
19+
*
20+
* @param service The id of the service that is being generated.
21+
* @param moduleName The name of the module to generate.
22+
* @param moduleVersion The version of the module to generate.
23+
* @param moduleDescription The optional module description for the module that will be generated.
24+
*/
25+
@SmithyUnstableApi
26+
public record PythonShapeSettings(
27+
Optional<ShapeId> service,
28+
String moduleName,
29+
String moduleVersion,
30+
String moduleDescription
31+
) implements ToSmithyBuilder<PythonShapeSettings> {
32+
33+
private static final String SERVICE = "service";
34+
private static final String MODULE_NAME = "module";
35+
private static final String MODULE_DESCRIPTION = "moduleDescription";
36+
private static final String MODULE_VERSION = "moduleVersion";
37+
38+
private PythonShapeSettings(Builder builder) {
39+
this(
40+
Optional.ofNullable(builder.service),
41+
builder.moduleName,
42+
builder.moduleVersion,
43+
builder.moduleDescription
44+
);
45+
}
46+
47+
@Override
48+
public Builder toBuilder() {
49+
Builder builder = builder()
50+
.moduleName(moduleName)
51+
.moduleVersion(moduleVersion)
52+
.moduleDescription(moduleDescription);
53+
service.ifPresent(builder::service);
54+
return builder;
55+
}
56+
57+
public PythonSettings toPythonSettings(ShapeId service) {
58+
return PythonSettings.builder()
59+
.service(service)
60+
.moduleName(moduleName)
61+
.moduleVersion(moduleVersion)
62+
.moduleDescription(moduleDescription)
63+
.artifactType(PythonSettings.ArtifactType.SHAPES)
64+
.build();
65+
}
66+
67+
public PythonSettings toPythonSettings() {
68+
return toPythonSettings(service.get());
69+
}
70+
71+
/**
72+
* Create a settings object from a configuration object node.
73+
*
74+
* @param config Config object to load.
75+
* @return Returns the extracted settings.
76+
*/
77+
public static PythonShapeSettings fromNode(ObjectNode config) {
78+
config.warnIfAdditionalProperties(Arrays.asList(SERVICE, MODULE_NAME, MODULE_DESCRIPTION, MODULE_VERSION));
79+
80+
String moduleName = config.expectStringMember(MODULE_NAME).getValue();
81+
Builder builder = builder()
82+
.moduleName(moduleName)
83+
.moduleVersion(config.expectStringMember(MODULE_VERSION).getValue());
84+
config.getStringMember(SERVICE).map(StringNode::expectShapeId).ifPresent(builder::service);
85+
config.getStringMember(MODULE_DESCRIPTION).map(StringNode::getValue).ifPresent(builder::moduleDescription);
86+
return builder.build();
87+
}
88+
89+
public static Builder builder() {
90+
return new Builder();
91+
}
92+
93+
public static class Builder implements SmithyBuilder<PythonShapeSettings> {
94+
95+
private ShapeId service;
96+
private String moduleName;
97+
private String moduleVersion;
98+
private String moduleDescription;
99+
100+
@Override
101+
public PythonShapeSettings build() {
102+
SmithyBuilder.requiredState("moduleName", moduleName);
103+
SmithyBuilder.requiredState("moduleVersion", moduleVersion);
104+
return new PythonShapeSettings(this);
105+
}
106+
107+
public Builder service(ShapeId service) {
108+
this.service = service;
109+
return this;
110+
}
111+
112+
public Builder moduleName(String moduleName) {
113+
this.moduleName = moduleName;
114+
return this;
115+
}
116+
117+
public Builder moduleVersion(String moduleVersion) {
118+
this.moduleVersion = moduleVersion;
119+
return this;
120+
}
121+
122+
public Builder moduleDescription(String moduleDescription) {
123+
this.moduleDescription = moduleDescription;
124+
return this;
125+
}
126+
}
127+
}

0 commit comments

Comments
 (0)