Skip to content

Commit b8f26d3

Browse files
mtdowlingMichael Dowling
authored and
Michael Dowling
committed
Add DependencyTracker for symbols
This tracker can be used to manage and query dependencies, to track dependencies that need to be used at runtime by generated code, and load dependencies from a JSON file.
1 parent fc1e890 commit b8f26d3

File tree

3 files changed

+438
-0
lines changed

3 files changed

+438
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
/*
2+
* Copyright 2022 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.codegen.core;
17+
18+
import java.io.IOException;
19+
import java.io.InputStream;
20+
import java.io.UncheckedIOException;
21+
import java.net.URL;
22+
import java.util.ArrayList;
23+
import java.util.List;
24+
import java.util.Map;
25+
import java.util.Objects;
26+
import java.util.Set;
27+
import software.amazon.smithy.model.node.Node;
28+
import software.amazon.smithy.model.node.NodeMapper;
29+
import software.amazon.smithy.model.node.ObjectNode;
30+
import software.amazon.smithy.utils.SetUtils;
31+
32+
/**
33+
* A container for all known dependencies of a generator.
34+
*
35+
* <p>A DependencyTracker can include predefined dependencies loaded from a
36+
* file (for example to track versions of runtime dependencies used in the
37+
* generator), or dependencies that are accumulated dynamically as code is
38+
* generated.
39+
*
40+
* <p>Notes:
41+
* <ul>
42+
* <li>Multiple packages of the same name and type can be added to tracker.
43+
* There's no de-duplication.</li>
44+
* <li>Note that this class is mutable and not synchronized.</li>
45+
* </ul>
46+
*
47+
* <h2>Loading from JSON</h2>
48+
*
49+
* <p>Dependencies can be loaded from a JSON file to more easily track
50+
* dependencies used at runtime by generated code. This feature can also
51+
* be used to generate the dependencies tracked by the generated from from
52+
* other dependency graph formats like lockfiles.
53+
*
54+
* <p>The JSON file has the following format:
55+
*
56+
* <pre>
57+
* {@code
58+
* {
59+
* "version": "1.0",
60+
* "dependencies": [
61+
* {
62+
* "packageName": "string",
63+
* "version": "string",
64+
* "dependencyType": "string",
65+
* "properties": {
66+
* "x": true,
67+
* "y": [10],
68+
* "z": "string"
69+
* }
70+
* }
71+
* ]
72+
* }
73+
* }
74+
* </pre>
75+
*
76+
* <ul>
77+
* <li>"version" (string, required): Must be set to "1.0".</li>
78+
* <li>"dependencies" is a list of dependency objects that contain the following
79+
* properties:
80+
* <ul>
81+
* <li>"packageName" (string, required): The required name of the package.</li>
82+
* <li>"version" (string, required): The required dependency version.</li>
83+
* <li>"dependencyType" (string): The optional type of dependency. This value
84+
* is dependent on the package manager of the target environment.</li>
85+
* <li>"properties" (map of string to any value): Properties to assign to
86+
* the symbol. These properties can be any JSON value type other than null.
87+
* List values are converted to a {@link List}, map values are converted to
88+
* a {@link Map}, boolean values to Java's boolean, numeric values to an
89+
* appropriate {@link Number} type, and string values to {@link String}.</li>
90+
* </ul>
91+
* </li>
92+
* </ul>
93+
*/
94+
public final class DependencyTracker implements SymbolDependencyContainer {
95+
96+
private static final String VERSION = "version";
97+
private static final String DEPENDENCIES = "dependencies";
98+
private static final String PACKAGE_NAME = "packageName";
99+
private static final String DEPENDENCY_TYPE = "dependencyType";
100+
private static final String PROPERTIES = "properties";
101+
private static final Set<String> TOP_LEVEL_PROPERTIES = SetUtils.of(VERSION, DEPENDENCIES);
102+
private static final Set<String> ALLOWED_SYMBOL_PROPERTIES = SetUtils.of(
103+
PACKAGE_NAME, DEPENDENCY_TYPE, VERSION, PROPERTIES);
104+
105+
private final List<SymbolDependency> dependencies = new ArrayList<>();
106+
107+
@Override
108+
public List<SymbolDependency> getDependencies() {
109+
return dependencies;
110+
}
111+
112+
/**
113+
* Gets the first found dependency by name.
114+
*
115+
* @param name Package name of the dependency to get.
116+
* @return Returns the dependency.
117+
* @throws IllegalArgumentException if the dependency cannot be found.
118+
*/
119+
public SymbolDependency getByName(String name) {
120+
for (SymbolDependency dependency : dependencies) {
121+
if (dependency.getPackageName().equals(name)) {
122+
return dependency;
123+
}
124+
}
125+
throw new IllegalArgumentException("Unknown dependency '" + name + "'. Known dependencies: " + dependencies);
126+
}
127+
128+
/**
129+
* Gets the first found dependency by name and dependency type.
130+
*
131+
* @param name Package name of the dependency to get.
132+
* @param dependencyType The dependency type of package to find.
133+
* @return Returns the dependency.
134+
* @throws IllegalArgumentException if the dependency cannot be found.
135+
*/
136+
public SymbolDependency getByName(String name, String dependencyType) {
137+
for (SymbolDependency dependency : dependencies) {
138+
if (dependency.getPackageName().equals(name) && dependency.getDependencyType().equals(dependencyType)) {
139+
return dependency;
140+
}
141+
}
142+
throw new IllegalArgumentException("Unknown dependency '" + name + "' of type '" + dependencyType + "'. "
143+
+ "Known dependencies: " + dependencies);
144+
}
145+
146+
/**
147+
* Gets a list of matching dependencies that have a dependency type
148+
* matching {@code dependencyType}.
149+
*
150+
* @param dependencyType Dependency type to find.
151+
* @return Returns the matching dependencies.
152+
*/
153+
public List<SymbolDependency> getByType(String dependencyType) {
154+
List<SymbolDependency> result = new ArrayList<>();
155+
for (SymbolDependency dependency : dependencies) {
156+
if (dependency.getDependencyType().equals(dependencyType)) {
157+
result.add(dependency);
158+
}
159+
}
160+
return result;
161+
}
162+
163+
/**
164+
* Gets a list of matching dependencies that contain a property named
165+
* {@code property}.
166+
*
167+
* @param property Property to find.
168+
* @return Returns the matching dependencies.
169+
*/
170+
public List<SymbolDependency> getByProperty(String property) {
171+
List<SymbolDependency> result = new ArrayList<>();
172+
for (SymbolDependency dependency : dependencies) {
173+
if (dependency.getProperty(property).isPresent()) {
174+
result.add(dependency);
175+
}
176+
}
177+
return result;
178+
}
179+
180+
/**
181+
* Gets a list of matching dependencies that contain a property named
182+
* {@code property} with a value of {@code value}.
183+
*
184+
* @param property Property to find.
185+
* @param value Value to match.
186+
* @return Returns the matching dependencies.
187+
*/
188+
public List<SymbolDependency> getByProperty(String property, Object value) {
189+
List<SymbolDependency> result = new ArrayList<>();
190+
for (SymbolDependency dependency : dependencies) {
191+
if (dependency.getProperty(property).filter(p -> p.equals(value)).isPresent()) {
192+
result.add(dependency);
193+
}
194+
}
195+
return result;
196+
}
197+
198+
/**
199+
* Adds a dependency.
200+
*
201+
* @param dependency Dependency to add.
202+
*/
203+
public void addDependency(SymbolDependency dependency) {
204+
dependencies.add(dependency);
205+
}
206+
207+
/**
208+
* Adds a dependency.
209+
*
210+
* @param packageName Name of the dependency.
211+
* @param version Version of the dependency.
212+
* @param dependencyType Type of dependency (e.g., "dev", "test", "runtime", etc).
213+
* This value wholly depends on the type of dependency graph
214+
* being generated.
215+
*/
216+
public void addDependency(String packageName, String version, String dependencyType) {
217+
SymbolDependency dependency = SymbolDependency.builder()
218+
.packageName(packageName)
219+
.version(version)
220+
.dependencyType(dependencyType)
221+
.build();
222+
addDependency(dependency);
223+
}
224+
225+
/**
226+
* Adds dependencies from a {@link SymbolDependencyContainer}.
227+
*
228+
* @param container Container to copy depdencies from.
229+
*/
230+
public void addDependencies(SymbolDependencyContainer container) {
231+
for (SymbolDependency dependency : container.getDependencies()) {
232+
addDependency(dependency);
233+
}
234+
}
235+
236+
/**
237+
* Loads predefined dependencies from a JSON file (for example, to track
238+
* known dependencies used by generated code at runtime).
239+
*
240+
* <pre>
241+
* {@code
242+
* DependencyTracker tracker = new DependencyTracker();
243+
* tracker.addDependenciesFromJson(getClass().getResource("some-file.json"));
244+
* }
245+
* </pre>
246+
*
247+
* @param jsonFile URL location of the JSON file.
248+
*/
249+
public void addDependenciesFromJson(URL jsonFile) {
250+
Objects.requireNonNull(jsonFile, "Dependency JSON file is null, probably because the file could not be found.");
251+
try (InputStream stream = jsonFile.openConnection().getInputStream()) {
252+
parseDependenciesFromJson(Node.parse(stream));
253+
} catch (IOException e) {
254+
throw new UncheckedIOException("Error loading dependencies from "
255+
+ jsonFile + ": " + e.getMessage(), e);
256+
}
257+
}
258+
259+
private void parseDependenciesFromJson(Node node) {
260+
NodeMapper mapper = new NodeMapper();
261+
ObjectNode root = node.expectObjectNode();
262+
root.warnIfAdditionalProperties(TOP_LEVEL_PROPERTIES);
263+
// Must define a version.
264+
root.expectStringMember(VERSION).expectOneOf("1.0");
265+
// Must define a list of dependencies, each an ObjectNode.
266+
for (ObjectNode value : root.expectArrayMember(DEPENDENCIES).getElementsAs(ObjectNode.class)) {
267+
value.warnIfAdditionalProperties(ALLOWED_SYMBOL_PROPERTIES);
268+
SymbolDependency.Builder builder = SymbolDependency.builder();
269+
builder.packageName(value.expectStringMember(PACKAGE_NAME).getValue());
270+
builder.version(value.expectStringMember(VERSION).getValue());
271+
value.getStringMember(DEPENDENCY_TYPE).ifPresent(v -> builder.dependencyType(v.getValue()));
272+
value.getObjectMember(PROPERTIES).ifPresent(properties -> {
273+
for (Map.Entry<String, Node> entry : properties.getStringMap().entrySet()) {
274+
Object nodeAsJavaValue = mapper.deserialize(entry.getValue(), Object.class);
275+
builder.putProperty(entry.getKey(), nodeAsJavaValue);
276+
}
277+
});
278+
addDependency(builder.build());
279+
}
280+
}
281+
}

0 commit comments

Comments
 (0)