From f376da6c949e8354996c0e9ebbd1b0b335078ead Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Thu, 24 Feb 2022 23:09:23 -0800 Subject: [PATCH] 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. --- .../codegen/core/DependencyTracker.java | 281 ++++++++++++++++++ .../codegen/core/DependencyTrackerTest.java | 126 ++++++++ .../codegen/core/dependencies-test.json | 31 ++ 3 files changed, 438 insertions(+) create mode 100644 smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/DependencyTracker.java create mode 100644 smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/DependencyTrackerTest.java create mode 100644 smithy-codegen-core/src/test/resources/software/amazon/smithy/codegen/core/dependencies-test.json diff --git a/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/DependencyTracker.java b/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/DependencyTracker.java new file mode 100644 index 00000000000..3544e69ff54 --- /dev/null +++ b/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/DependencyTracker.java @@ -0,0 +1,281 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.codegen.core; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.NodeMapper; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.utils.SetUtils; + +/** + * A container for all known dependencies of a generator. + * + *

A DependencyTracker can include predefined dependencies loaded from a + * file (for example to track versions of runtime dependencies used in the + * generator), or dependencies that are accumulated dynamically as code is + * generated. + * + *

Notes: + *

+ * + *

Loading from JSON

+ * + *

Dependencies can be loaded from a JSON file to more easily track + * dependencies used at runtime by generated code. This feature can also + * be used to generate the dependencies tracked by the generated from from + * other dependency graph formats like lockfiles. + * + *

The JSON file has the following format: + * + *

+ * {@code
+ * {
+ *     "version": "1.0",
+ *     "dependencies": [
+ *         {
+ *             "packageName": "string",
+ *             "version": "string",
+ *             "dependencyType": "string",
+ *             "properties": {
+ *                 "x": true,
+ *                 "y": [10],
+ *                 "z": "string"
+ *             }
+ *         }
+ *     ]
+ * }
+ * }
+ * 
+ * + * + */ +public final class DependencyTracker implements SymbolDependencyContainer { + + private static final String VERSION = "version"; + private static final String DEPENDENCIES = "dependencies"; + private static final String PACKAGE_NAME = "packageName"; + private static final String DEPENDENCY_TYPE = "dependencyType"; + private static final String PROPERTIES = "properties"; + private static final Set TOP_LEVEL_PROPERTIES = SetUtils.of(VERSION, DEPENDENCIES); + private static final Set ALLOWED_SYMBOL_PROPERTIES = SetUtils.of( + PACKAGE_NAME, DEPENDENCY_TYPE, VERSION, PROPERTIES); + + private final List dependencies = new ArrayList<>(); + + @Override + public List getDependencies() { + return dependencies; + } + + /** + * Gets the first found dependency by name. + * + * @param name Package name of the dependency to get. + * @return Returns the dependency. + * @throws IllegalArgumentException if the dependency cannot be found. + */ + public SymbolDependency getByName(String name) { + for (SymbolDependency dependency : dependencies) { + if (dependency.getPackageName().equals(name)) { + return dependency; + } + } + throw new IllegalArgumentException("Unknown dependency '" + name + "'. Known dependencies: " + dependencies); + } + + /** + * Gets the first found dependency by name and dependency type. + * + * @param name Package name of the dependency to get. + * @param dependencyType The dependency type of package to find. + * @return Returns the dependency. + * @throws IllegalArgumentException if the dependency cannot be found. + */ + public SymbolDependency getByName(String name, String dependencyType) { + for (SymbolDependency dependency : dependencies) { + if (dependency.getPackageName().equals(name) && dependency.getDependencyType().equals(dependencyType)) { + return dependency; + } + } + throw new IllegalArgumentException("Unknown dependency '" + name + "' of type '" + dependencyType + "'. " + + "Known dependencies: " + dependencies); + } + + /** + * Gets a list of matching dependencies that have a dependency type + * matching {@code dependencyType}. + * + * @param dependencyType Dependency type to find. + * @return Returns the matching dependencies. + */ + public List getByType(String dependencyType) { + List result = new ArrayList<>(); + for (SymbolDependency dependency : dependencies) { + if (dependency.getDependencyType().equals(dependencyType)) { + result.add(dependency); + } + } + return result; + } + + /** + * Gets a list of matching dependencies that contain a property named + * {@code property}. + * + * @param property Property to find. + * @return Returns the matching dependencies. + */ + public List getByProperty(String property) { + List result = new ArrayList<>(); + for (SymbolDependency dependency : dependencies) { + if (dependency.getProperty(property).isPresent()) { + result.add(dependency); + } + } + return result; + } + + /** + * Gets a list of matching dependencies that contain a property named + * {@code property} with a value of {@code value}. + * + * @param property Property to find. + * @param value Value to match. + * @return Returns the matching dependencies. + */ + public List getByProperty(String property, Object value) { + List result = new ArrayList<>(); + for (SymbolDependency dependency : dependencies) { + if (dependency.getProperty(property).filter(p -> p.equals(value)).isPresent()) { + result.add(dependency); + } + } + return result; + } + + /** + * Adds a dependency. + * + * @param dependency Dependency to add. + */ + public void addDependency(SymbolDependency dependency) { + dependencies.add(dependency); + } + + /** + * Adds a dependency. + * + * @param packageName Name of the dependency. + * @param version Version of the dependency. + * @param dependencyType Type of dependency (e.g., "dev", "test", "runtime", etc). + * This value wholly depends on the type of dependency graph + * being generated. + */ + public void addDependency(String packageName, String version, String dependencyType) { + SymbolDependency dependency = SymbolDependency.builder() + .packageName(packageName) + .version(version) + .dependencyType(dependencyType) + .build(); + addDependency(dependency); + } + + /** + * Adds dependencies from a {@link SymbolDependencyContainer}. + * + * @param container Container to copy depdencies from. + */ + public void addDependencies(SymbolDependencyContainer container) { + for (SymbolDependency dependency : container.getDependencies()) { + addDependency(dependency); + } + } + + /** + * Loads predefined dependencies from a JSON file (for example, to track + * known dependencies used by generated code at runtime). + * + *
+     * {@code
+     * DependencyTracker tracker = new DependencyTracker();
+     * tracker.addDependenciesFromJson(getClass().getResource("some-file.json"));
+     * }
+     * 
+ * + * @param jsonFile URL location of the JSON file. + */ + public void addDependenciesFromJson(URL jsonFile) { + Objects.requireNonNull(jsonFile, "Dependency JSON file is null, probably because the file could not be found."); + try (InputStream stream = jsonFile.openConnection().getInputStream()) { + parseDependenciesFromJson(Node.parse(stream)); + } catch (IOException e) { + throw new UncheckedIOException("Error loading dependencies from " + + jsonFile + ": " + e.getMessage(), e); + } + } + + private void parseDependenciesFromJson(Node node) { + NodeMapper mapper = new NodeMapper(); + ObjectNode root = node.expectObjectNode(); + root.warnIfAdditionalProperties(TOP_LEVEL_PROPERTIES); + // Must define a version. + root.expectStringMember(VERSION).expectOneOf("1.0"); + // Must define a list of dependencies, each an ObjectNode. + for (ObjectNode value : root.expectArrayMember(DEPENDENCIES).getElementsAs(ObjectNode.class)) { + value.warnIfAdditionalProperties(ALLOWED_SYMBOL_PROPERTIES); + SymbolDependency.Builder builder = SymbolDependency.builder(); + builder.packageName(value.expectStringMember(PACKAGE_NAME).getValue()); + builder.version(value.expectStringMember(VERSION).getValue()); + value.getStringMember(DEPENDENCY_TYPE).ifPresent(v -> builder.dependencyType(v.getValue())); + value.getObjectMember(PROPERTIES).ifPresent(properties -> { + for (Map.Entry entry : properties.getStringMap().entrySet()) { + Object nodeAsJavaValue = mapper.deserialize(entry.getValue(), Object.class); + builder.putProperty(entry.getKey(), nodeAsJavaValue); + } + }); + addDependency(builder.build()); + } + } +} diff --git a/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/DependencyTrackerTest.java b/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/DependencyTrackerTest.java new file mode 100644 index 00000000000..206de33af25 --- /dev/null +++ b/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/DependencyTrackerTest.java @@ -0,0 +1,126 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.codegen.core; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.utils.ListUtils; +import software.amazon.smithy.utils.MapUtils; + +public class DependencyTrackerTest { + @Test + public void canAddAndQueryDependencies() { + DependencyTracker tracker = new DependencyTracker(); + tracker.addDependency("a", "b", "c"); + tracker.addDependency("d", "e", "f"); + tracker.addDependency(SymbolDependency.builder().packageName("h").version("i").dependencyType("j").build()); + + assertThat(tracker.getDependencies(), hasSize(3)); + assertThat(tracker.getByName("a").getPackageName(), equalTo("a")); + assertThat(tracker.getByName("d").getPackageName(), equalTo("d")); + assertThat(tracker.getByName("h").getPackageName(), equalTo("h")); + + assertThat(tracker.getByName("a", "c").getPackageName(), equalTo("a")); + assertThat(tracker.getByName("d", "f").getPackageName(), equalTo("d")); + assertThat(tracker.getByName("h", "j").getPackageName(), equalTo("h")); + + assertThat(tracker.getByProperty("nope"), empty()); + assertThat(tracker.getByProperty("nope", 10), empty()); + + assertThat(tracker.getByType("c"), hasSize(1)); + assertThat(tracker.getByType("f"), hasSize(1)); + assertThat(tracker.getByType("j"), hasSize(1)); + } + + @Test + public void throwsWhenNotFoundByName() { + DependencyTracker tracker = new DependencyTracker(); + + Assertions.assertThrows(IllegalArgumentException.class, () -> tracker.getByName("foo")); + } + + @Test + public void throwsWhenNotFoundByNameAndType() { + DependencyTracker tracker = new DependencyTracker(); + + Assertions.assertThrows(IllegalArgumentException.class, () -> tracker.getByName("foo", "baz")); + } + + @Test + public void canQueryByProperty() { + DependencyTracker tracker = new DependencyTracker(); + SymbolDependency a = SymbolDependency.builder().packageName("a").version("1").putProperty("x", 1).build(); + SymbolDependency b = SymbolDependency.builder().packageName("b").version("1").putProperty("x", 2).build(); + SymbolDependency c = SymbolDependency.builder().packageName("c").version("1").putProperty("y", true).build(); + tracker.addDependency(a); + tracker.addDependency(b); + tracker.addDependency(c); + + assertThat(tracker.getByProperty("x"), contains(a, b)); + assertThat(tracker.getByProperty("y"), contains(c)); + assertThat(tracker.getByProperty("x", 1), contains(a)); + assertThat(tracker.getByProperty("x", 2), contains(b)); + assertThat(tracker.getByProperty("x", 3), empty()); + assertThat(tracker.getByProperty("y", true), contains(c)); + assertThat(tracker.getByProperty("y", false), empty()); + } + + @Test + public void canAddFromContainer() { + DependencyTracker tracker = new DependencyTracker(); + SymbolDependency a = SymbolDependency.builder().packageName("a").version("1").putProperty("x", 1).build(); + SymbolDependency b = SymbolDependency.builder().packageName("b").version("1").putProperty("x", 2).build(); + tracker.addDependency(a); + tracker.addDependency(b); + + DependencyTracker tracker2 = new DependencyTracker(); + SymbolDependency c = SymbolDependency.builder().packageName("c").version("1").putProperty("y", true).build(); + tracker2.addDependency(c); + + tracker.addDependencies(tracker2); + + assertThat(tracker.getByName("c").getPackageName(), equalTo("c")); + assertThat(tracker.getDependencies(), hasSize(3)); + assertThat(tracker2.getDependencies(), hasSize(1)); + } + + @Test + public void canLoadDependenciesFromJsonFiles() { + DependencyTracker tracker = new DependencyTracker(); + tracker.addDependenciesFromJson(getClass().getResource("dependencies-test.json")); + + // Multiple dependencies can appear with the same name. + assertThat(tracker.getDependencies(), hasSize(4)); + + // First by this name is "test". + assertThat(tracker.getByName("Test1").getDependencyType(), equalTo("test")); + assertThat(tracker.getByName("Test1").getVersion(), equalTo("1")); + assertThat(tracker.getByName("Test1", "runtime").getVersion(), equalTo("1-prod")); + + assertThat(tracker.getByName("Test2").getDependencyType(), equalTo("")); + + assertThat(tracker.getByName("Test3").getProperty("foo").get(), equalTo(true)); + assertThat(tracker.getByName("Test3").getProperty("foo").get(), equalTo(true)); + assertThat(tracker.getByName("Test3").getProperty("bar").get(), equalTo(ListUtils.of("a"))); + assertThat(tracker.getByName("Test3").getProperty("baz").get(), equalTo(MapUtils.of("greeting", "hi"))); + } +} diff --git a/smithy-codegen-core/src/test/resources/software/amazon/smithy/codegen/core/dependencies-test.json b/smithy-codegen-core/src/test/resources/software/amazon/smithy/codegen/core/dependencies-test.json new file mode 100644 index 00000000000..634aba1bfe0 --- /dev/null +++ b/smithy-codegen-core/src/test/resources/software/amazon/smithy/codegen/core/dependencies-test.json @@ -0,0 +1,31 @@ +{ + "version": "1.0", + "dependencies": [ + { + "packageName": "Test1", + "version": "1", + "dependencyType": "test" + }, + { + "packageName": "Test1", + "version": "1-prod", + "dependencyType": "runtime" + }, + { + "packageName": "Test2", + "version": "2" + }, + { + "packageName": "Test3", + "version": "3", + "dependencyType": "runtime", + "properties": { + "foo": true, + "bar": ["a"], + "baz": { + "greeting": "hi" + } + } + } + ] +}