Skip to content

Commit 085d870

Browse files
committed
Move CodeWriter delegation into its own abstraction
1 parent 5d9fe3b commit 085d870

File tree

3 files changed

+258
-0
lines changed

3 files changed

+258
-0
lines changed
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.smithy.typescript.codegen;
17+
18+
import java.util.Collections;
19+
import java.util.HashMap;
20+
import java.util.HashSet;
21+
import java.util.Map;
22+
import java.util.Objects;
23+
import java.util.Set;
24+
import java.util.function.BiFunction;
25+
import software.amazon.smithy.build.FileManifest;
26+
import software.amazon.smithy.codegen.core.Symbol;
27+
import software.amazon.smithy.codegen.core.SymbolProvider;
28+
import software.amazon.smithy.model.Model;
29+
import software.amazon.smithy.model.shapes.Shape;
30+
import software.amazon.smithy.utils.CodeWriter;
31+
import software.amazon.smithy.utils.SmithyBuilder;
32+
33+
/**
34+
* Handles creating CodeWriters for filenames.
35+
*
36+
* <p>This abstraction is used to make it so that multiple shapes can generate
37+
* code into the same file. All CodeWriters created by this class are stored
38+
* in memory until a call to {@link #writeFiles()}, which causes each
39+
* CodeWriter to dump their contents to the provided {@code FileManifest}
40+
* in the mapped filename provided by the given {@code SymbolProvider}.
41+
*
42+
* @param <T> The type of CodeWriter that is vended.
43+
*
44+
* TODO: Make this public and move into core when it's stable and proven.
45+
*/
46+
final class CodeWriterDelegator<T extends CodeWriter> {
47+
48+
private final Model model;
49+
private final FileManifest fileManifest;
50+
private final SymbolProvider symbolProvider;
51+
private final Map<String, T> writers = new HashMap<>();
52+
private final Map<String, Set<Shape>> shapesPerFile = new HashMap<>();
53+
private final BiFunction<Shape, Symbol, T> factory;
54+
private final BeforeWrite<T> beforeWrite;
55+
private final String addSeparator;
56+
57+
CodeWriterDelegator(Builder<T> builder) {
58+
this.model = SmithyBuilder.requiredState("model", builder.model);
59+
this.fileManifest = SmithyBuilder.requiredState("fileManifest", builder.fileManifest);
60+
this.symbolProvider = SmithyBuilder.requiredState("symbolProvider", builder.symbolProvider);
61+
this.factory = SmithyBuilder.requiredState("factory", builder.factory);
62+
this.beforeWrite = builder.beforeWrite;
63+
this.addSeparator = builder.addSeparator;
64+
}
65+
66+
public static <T extends CodeWriter> Builder<T> builder() {
67+
return new Builder<>();
68+
}
69+
70+
/**
71+
* Gets a previously created writer or creates a new one if needed.
72+
*
73+
* <p>Any imports requires by the given symbol are automatically registered
74+
* with the writer. Shapes referenced by the given shape are crawled to
75+
* collect all contained shapes and the imports they also require.
76+
*
77+
* @param shape Shape to create the writer for.
78+
* @return Returns the create writer.
79+
*/
80+
public T createWriter(Shape shape) {
81+
Symbol symbol = symbolProvider.toSymbol(shape);
82+
String filename = symbol.getDefinitionFile();
83+
84+
boolean needsNewline = writers.containsKey(filename);
85+
T writer = writers.computeIfAbsent(filename, f -> factory.apply(shape, symbol));
86+
87+
// Add newlines/separators between types in the same file.
88+
if (needsNewline && !addSeparator.isEmpty()) {
89+
writer.write(addSeparator);
90+
}
91+
92+
shapesPerFile.computeIfAbsent(filename, f -> new HashSet<>()).add(shape);
93+
94+
return writer;
95+
}
96+
97+
public void writeFiles() {
98+
writers.forEach((filename, writer) -> {
99+
beforeWrite.apply(filename, writer, shapesPerFile.getOrDefault(filename, Collections.emptySet()));
100+
fileManifest.writeFile(filename, writer.toString());
101+
});
102+
writers.clear();
103+
}
104+
105+
public interface BeforeWrite<T> {
106+
void apply(String filename, T writer, Set<Shape> shapes);
107+
}
108+
109+
public static final class Builder<T extends CodeWriter> {
110+
111+
private Model model;
112+
private FileManifest fileManifest;
113+
private SymbolProvider symbolProvider;
114+
private BiFunction<Shape, Symbol, T> factory;
115+
private BeforeWrite<T> beforeWrite = (filename, writer, shapes) -> { };
116+
private String addSeparator = "\n";
117+
118+
public CodeWriterDelegator<T> build() {
119+
return new CodeWriterDelegator<>(this);
120+
}
121+
122+
public Builder<T> model(Model model) {
123+
this.model = model;
124+
return this;
125+
}
126+
127+
public Builder<T> fileManifest(FileManifest fileManifest) {
128+
this.fileManifest = fileManifest;
129+
return this;
130+
}
131+
132+
public Builder<T> symbolProvider(SymbolProvider symbolProvider) {
133+
this.symbolProvider = symbolProvider;
134+
return this;
135+
}
136+
137+
public Builder<T> factory(BiFunction<Shape, Symbol, T> factory) {
138+
this.factory = factory;
139+
return this;
140+
}
141+
142+
public Builder<T> beforeWrite(BeforeWrite<T> beforeWrite) {
143+
this.beforeWrite = Objects.requireNonNull(beforeWrite);
144+
return this;
145+
}
146+
147+
public Builder<T> addSeparator(String addSeparator) {
148+
this.addSeparator = Objects.requireNonNull(addSeparator);
149+
return this;
150+
}
151+
}
152+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package software.amazon.smithy.typescript.codegen;
2+
3+
import static org.hamcrest.MatcherAssert.assertThat;
4+
import static org.hamcrest.Matchers.containsInAnyOrder;
5+
import static org.hamcrest.Matchers.equalTo;
6+
7+
import java.util.ArrayList;
8+
import java.util.List;
9+
import java.util.Set;
10+
import org.junit.jupiter.api.Test;
11+
import software.amazon.smithy.build.FileManifest;
12+
import software.amazon.smithy.build.MockManifest;
13+
import software.amazon.smithy.codegen.core.Symbol;
14+
import software.amazon.smithy.codegen.core.SymbolProvider;
15+
import software.amazon.smithy.model.Model;
16+
import software.amazon.smithy.model.shapes.Shape;
17+
import software.amazon.smithy.model.shapes.ShapeId;
18+
import software.amazon.smithy.utils.SetUtils;
19+
import software.amazon.smithy.utils.Triple;
20+
21+
public class CodeWriterDelegatorTest {
22+
@Test
23+
public void vendsWritersForShapes() {
24+
Model model = createModel();
25+
Shape fooShape = model.getShapeIndex().getShape(ShapeId.from("smithy.example#Foo")).get();
26+
SymbolProvider provider = createProvider();
27+
MockManifest manifest = new MockManifest();
28+
CodeWriterDelegator<TypeScriptWriter> delegator = createBuilder(model, provider, manifest).build();
29+
30+
TypeScriptWriter writer = delegator.createWriter(fooShape);
31+
writer.write("Hello!");
32+
delegator.writeFiles();
33+
34+
assertThat(manifest.getFileString("Foo.txt").get(), equalTo("Hello!\n"));
35+
}
36+
37+
@Test
38+
public void appendsToOpenedWriterWithCustomSeparator() {
39+
Model model = createModel();
40+
Shape fooShape = model.getShapeIndex().getShape(ShapeId.from("smithy.example#Foo")).get();
41+
SymbolProvider provider = createProvider();
42+
MockManifest manifest = new MockManifest();
43+
CodeWriterDelegator<TypeScriptWriter> delegator = createBuilder(model, provider, manifest)
44+
.addSeparator("\n// yap")
45+
.build();
46+
47+
delegator.createWriter(fooShape).write("Hello!");
48+
delegator.createWriter(fooShape).write("Goodbye!");
49+
delegator.writeFiles();
50+
51+
assertThat(manifest.getFileString("Foo.txt").get(), equalTo("Hello!\n\n// yap\nGoodbye!\n"));
52+
}
53+
54+
@Test
55+
public void emitsBeforeWriting() {
56+
Model model = createModel();
57+
Shape fooShape = model.getShapeIndex().getShape(ShapeId.from("smithy.example#Foo")).get();
58+
SymbolProvider provider = createProvider();
59+
MockManifest manifest = new MockManifest();
60+
List<Triple<String, TypeScriptWriter, Set<Shape>>> before = new ArrayList<>();
61+
CodeWriterDelegator<TypeScriptWriter> delegator = createBuilder(model, provider, manifest)
62+
.beforeWrite((filename, writer, shapes) -> {
63+
before.add(Triple.of(filename, writer, shapes));
64+
})
65+
.build();
66+
67+
TypeScriptWriter vended = delegator.createWriter(fooShape);
68+
vended.write("Hello!");
69+
delegator.writeFiles();
70+
71+
assertThat(before, containsInAnyOrder(Triple.of("Foo.txt", vended, SetUtils.of(fooShape))));
72+
}
73+
74+
private static CodeWriterDelegator.Builder<TypeScriptWriter> createBuilder(
75+
Model model,
76+
SymbolProvider provider,
77+
FileManifest manifest
78+
) {
79+
return CodeWriterDelegator.<TypeScriptWriter>builder()
80+
.model(model)
81+
.symbolProvider(provider)
82+
.fileManifest(manifest)
83+
.factory((shape, symbol) -> new TypeScriptWriter(symbol.getNamespace()));
84+
}
85+
86+
private static Model createModel() {
87+
return Model.assembler()
88+
.addImport(CodeWriterDelegatorTest.class.getResource("testmodel.smithy"))
89+
.assemble()
90+
.unwrap();
91+
}
92+
93+
private static SymbolProvider createProvider() {
94+
return shape -> Symbol.builder()
95+
.name(shape.getId().getName())
96+
.namespace(shape.getId().getNamespace(), "/")
97+
.definitionFile(shape.getId().getName() + ".txt")
98+
.build();
99+
}
100+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace smithy.example
2+
3+
structure Foo {
4+
@required
5+
foo: String,
6+
}

0 commit comments

Comments
 (0)