From 53a8a685be8b9c5ae39df846798ea1d65e5b8db3 Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Thu, 19 Sep 2019 10:24:50 -0700 Subject: [PATCH 1/2] Add a way for symbols to introduce dependencies --- .../amazon/smithy/codegen/core/Symbol.java | 63 +++++- .../smithy/codegen/core/SymbolDependency.java | 201 ++++++++++++++++++ .../codegen/core/SymbolDependencyTest.java | 43 ++++ .../smithy/codegen/core/SymbolTest.java | 16 ++ 4 files changed, 318 insertions(+), 5 deletions(-) create mode 100644 codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/SymbolDependency.java create mode 100644 codegen/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/SymbolDependencyTest.java diff --git a/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/Symbol.java b/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/Symbol.java index a35debb03db..78946cb1237 100644 --- a/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/Symbol.java +++ b/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/Symbol.java @@ -65,6 +65,7 @@ public final class Symbol extends TypedPropertiesBag implements ToSmithyBuilder< private final String definitionFile; private final String declarationFile; private final List references; + private final List dependencies; private Symbol(Builder builder) { super(builder.properties); @@ -74,6 +75,7 @@ private Symbol(Builder builder) { this.declarationFile = builder.declarationFile; this.definitionFile = !builder.definitionFile.isEmpty() ? builder.definitionFile : declarationFile; this.references = ListUtils.copyOf(builder.references); + this.dependencies = ListUtils.copyOf(builder.dependencies); } /** @@ -185,6 +187,20 @@ public List getReferences() { return references; } + /** + * Gets the list of dependencies that this symbol introduces. + * + *

A dependency is a dependency on another package that the symbol + * requires. It is quite different from a reference since a reference + * only refers to a symbol; a reference provides no context as to whether + * or not a dependency is required or the dependency's coordinates. + * + * @return Returns the Symbol's dependencies. + */ + public List getDependencies() { + return dependencies; + } + @Override public Builder toBuilder() { Builder builder = new Builder(); @@ -193,7 +209,8 @@ public Builder toBuilder() { .properties(getProperties()) .definitionFile(definitionFile) .declarationFile(declarationFile) - .references(references); + .references(references) + .dependencies(dependencies); } @Override @@ -216,7 +233,8 @@ public boolean equals(Object o) { && getProperties().equals(symbol.getProperties()) && Objects.equals(declarationFile, symbol.declarationFile) && Objects.equals(definitionFile, symbol.definitionFile) - && references.equals(symbol.references); + && references.equals(symbol.references) + && dependencies.equals(symbol.dependencies); } @Override @@ -236,7 +254,8 @@ public static final class Builder private String namespaceDelimiter = ""; private String definitionFile = ""; private String declarationFile = ""; - private List references = new ArrayList<>(); + private final List references = new ArrayList<>(); + private final List dependencies = new ArrayList<>(); @Override public Symbol build() { @@ -298,9 +317,9 @@ public Builder declarationFile(String declarationFile) { } /** - * Adds and replaces the symbol references to the symbol. + * Replaces the symbol references to the symbol. * - * @param references References to add. + * @param references References to set. * @return Returns the builder. */ public Builder references(List references) { @@ -331,5 +350,39 @@ public Builder addReference(SymbolReference reference) { references.add(Objects.requireNonNull(reference)); return this; } + + /** + * Replaces the symbol dependencies of the symbol. + * + * @param dependencies Dependencies to set. + * @return Returns the builder. + */ + public Builder dependencies(List dependencies) { + this.dependencies.clear(); + dependencies.forEach(this::addDependency); + return this; + } + + /** + * Add a symbol dependency. + * + * @param dependency Symbol dependency to add. + * @return Returns the builder. + */ + public Builder addDependency(SymbolDependency dependency) { + dependencies.add(dependency); + return this; + } + + /** + * Add a symbol dependency. + * + * @param packageName Name of the package to depend on. + * @param version Version to depend on. + * @return Returns the builder. + */ + public Builder addDependency(String packageName, String version) { + return addDependency(SymbolDependency.builder().packageName(packageName).version(version).build()); + } } } diff --git a/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/SymbolDependency.java b/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/SymbolDependency.java new file mode 100644 index 00000000000..d62bb00206b --- /dev/null +++ b/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/SymbolDependency.java @@ -0,0 +1,201 @@ +/* + * Copyright 2019 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.util.Objects; +import software.amazon.smithy.utils.SmithyBuilder; +import software.amazon.smithy.utils.ToSmithyBuilder; + +/** + * Represents a dependency that is introduced by a {@link Symbol}. + * + *

{@link SymbolProvider} implementations sometimes need to refer to + * {@link Symbol} values that require a dependency to be brought in when + * generating code. A dependency can be associated with the Symbol to + * specify the relationship of a Symbol to a dependency. + * + *

This dependency class was designed to be as generic as possible while + * still allowing for some extension points through typed properties. + * If a feature you need is missing (for example, specifying a GitHub + * repository), use {@link TypedPropertiesBag.Builder#putProperty} to add a + * property on the dependency that can be understood by you code generator. + * + *

It's up to code generators to make sense of the values provided in a + * dependency and to aggregate them in a meaningful way. This class uses a + * package + version combination to define the coordinates of a dependency. + * Some dependency managers like Maven use a group + package + version + * combination. In cases like this, it is recommended to specify the + * {@code package} of the symbol as the group + package name (e.g., + * "software.amazon.smithy.model:0.9.3" becomes a package of + * "software.amazon.smithy.model" and a version of "0.9.3"). + * + *

The {@code dependencyType} of a dependency is application and + * target-specific. When omitted, it defaults to an empty string (""). An + * arbitrary string value can be provided and should refer to something that + * makes sense for the target language. For illustrative purposed only: + * a code generator that targets JavaScript and NPM could set the + * {@code dependencyType} of a dependency to "devDependencies" to add the + * dependency to the "devDependencies" property of a generated package.json. + * + *

{@code version} is also an opaque values that is target-specific and + * can even be specific to a {@code dependencyType}. For example, PHP's + * Composer provides a section named "suggest" that is a map of package names + * to a description of the suggestion. A {@code SymbolDependency} that is + * meant to define a "suggest" entry for a composer.json file could set the + * {@code dependencyType} to "suggest", the {@code packageName} to the name + * of the suggested package, and {@code version} to the description of the + * suggestion. + */ +public final class SymbolDependency extends TypedPropertiesBag implements ToSmithyBuilder { + + private final String dependencyType; + private final String packageName; + private final String version; + + private SymbolDependency(Builder builder) { + super(builder.properties); + this.dependencyType = SmithyBuilder.requiredState("dependencyType", builder.dependencyType); + this.packageName = SmithyBuilder.requiredState("packageName", builder.packageName); + this.version = SmithyBuilder.requiredState("version", builder.version); + } + + /** + * @return Returns a builder. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Gets the type of dependency (for example, "dev", "optional", etc). + * + *

This value defaults to an empty string if not explicitly set. + * + * @return Returns the dependency type. + */ + public String getDependencyType() { + return dependencyType; + } + + /** + * Gets the package name referenced by the dependency. + * + * @return Returns the package name. + */ + public String getPackageName() { + return packageName; + } + + /** + * Gets the version string of the dependency. + * + * @return Returns the version. + */ + public String getVersion() { + return version; + } + + @Override + public Builder toBuilder() { + return builder() + .dependencyType(dependencyType) + .packageName(packageName) + .version(version) + .properties(getProperties()); + } + + @Override + public String toString() { + return "SymbolDependency{" + + "dependencyType='" + dependencyType + '\'' + + ", packageName='" + packageName + '\'' + + ", version='" + version + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } else if (!(o instanceof SymbolDependency)) { + return false; + } + + SymbolDependency that = (SymbolDependency) o; + return dependencyType.equals(that.dependencyType) + && packageName.equals(that.packageName) + && version.equals(that.version); + } + + @Override + public int hashCode() { + return Objects.hash(dependencyType, packageName, version); + } + + /** + * Builds a SymbolDependency. + */ + public static final class Builder + extends TypedPropertiesBag.Builder + implements SmithyBuilder { + + private String dependencyType = ""; + private String packageName; + private String version; + + private Builder() {} + + @Override + public SymbolDependency build() { + return new SymbolDependency(this); + } + + /** + * Sets the type of dependency (for example, "dev", "optional", etc). + * + *

Defaults to an empty string if not explicitly set. + * + * @param dependencyType Dependency type to set. + * @return Returns the builder. + */ + public Builder dependencyType(String dependencyType) { + this.dependencyType = dependencyType; + return this; + } + + /** + * Sets the package name of the dependency. + * + * @param packageName Package name to set. + * @return Returns the builder. + */ + public Builder packageName(String packageName) { + this.packageName = packageName; + return this; + } + + /** + * Sets the version string of the dependency. + * + * @param version Opaque version string to set. + * @return Returns the builder. + */ + public Builder version(String version) { + this.version = version; + return this; + } + } +} diff --git a/codegen/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/SymbolDependencyTest.java b/codegen/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/SymbolDependencyTest.java new file mode 100644 index 00000000000..eac1042f786 --- /dev/null +++ b/codegen/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/SymbolDependencyTest.java @@ -0,0 +1,43 @@ +package software.amazon.smithy.codegen.core; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +import org.junit.jupiter.api.Test; + +public class SymbolDependencyTest { + @Test + public void setsDefaultTypeToEmptyString() { + SymbolDependency dependency = SymbolDependency.builder() + .packageName("foo") + .version("10") + .build(); + + assertThat(dependency.getPackageName(), equalTo("foo")); + assertThat(dependency.getVersion(), equalTo("10")); + assertThat(dependency.getDependencyType(), equalTo("")); + } + + @Test + public void convertsToBuilder() { + SymbolDependency dependency = SymbolDependency.builder() + .dependencyType("dev") + .packageName("foo") + .version("10.0.1") + .build(); + + assertThat(dependency.toBuilder().build(), equalTo(dependency)); + } + + @Test + public void hasProperties() { + SymbolDependency dependency = SymbolDependency.builder() + .dependencyType("dev") + .packageName("foo") + .version("10.0.1") + .putProperty("foo", "baz!") + .build(); + + assertThat(dependency.expectProperty("foo", String.class), equalTo("baz!")); + } +} diff --git a/codegen/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/SymbolTest.java b/codegen/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/SymbolTest.java index 7563d1ddfd6..dad10b4a3e5 100644 --- a/codegen/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/SymbolTest.java +++ b/codegen/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/SymbolTest.java @@ -16,6 +16,7 @@ package software.amazon.smithy.codegen.core; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.equalTo; import org.junit.jupiter.api.Assertions; @@ -98,4 +99,19 @@ public void returnsAppropriateDefinitionAndDeclarationFiles() { assertThat(symbol.getDefinitionFile(), equalTo("/foo/bar.baz")); assertThat(symbol.getDeclarationFile(), equalTo("/foo/bar.h")); } + + @Test + public void canAddDependencies() { + SymbolDependency a = SymbolDependency.builder().packageName("a1").version("a2").build(); + SymbolDependency b = SymbolDependency.builder().packageName("b1").version("b2").build(); + + Symbol symbol = Symbol.builder() + .name("foo") + .addDependency("a1", "a2") + .addDependency(SymbolDependency.builder().packageName("b1").version("b2").build()) + .build(); + + assertThat(symbol.getDependencies(), containsInAnyOrder(a, b)); + assertThat(symbol.toBuilder().build(), equalTo(symbol)); + } } From a8a646c5db8da1ec197909b5fdc507182f37d330 Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Thu, 19 Sep 2019 12:53:44 -0700 Subject: [PATCH 2/2] Allow dependency sorting and add helper method --- .../amazon/smithy/codegen/core/Symbol.java | 18 ++++++++++++- .../smithy/codegen/core/SymbolDependency.java | 26 +++++++++++++++++-- .../codegen/core/SymbolDependencyTest.java | 18 +++++++++++++ 3 files changed, 59 insertions(+), 3 deletions(-) diff --git a/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/Symbol.java b/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/Symbol.java index 78946cb1237..aeb2de15891 100644 --- a/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/Symbol.java +++ b/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/Symbol.java @@ -382,7 +382,23 @@ public Builder addDependency(SymbolDependency dependency) { * @return Returns the builder. */ public Builder addDependency(String packageName, String version) { - return addDependency(SymbolDependency.builder().packageName(packageName).version(version).build()); + return addDependency(null, packageName, version); + } + + /** + * Add a symbol dependency. + * + * @param dependencyType Type of dependency. + * @param packageName Name of the package to depend on. + * @param version Version to depend on. + * @return Returns the builder. + */ + public Builder addDependency(String dependencyType, String packageName, String version) { + return addDependency(SymbolDependency.builder() + .dependencyType(dependencyType) + .packageName(packageName) + .version(version) + .build()); } } } diff --git a/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/SymbolDependency.java b/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/SymbolDependency.java index d62bb00206b..345a075db23 100644 --- a/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/SymbolDependency.java +++ b/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/SymbolDependency.java @@ -59,7 +59,8 @@ * of the suggested package, and {@code version} to the description of the * suggestion. */ -public final class SymbolDependency extends TypedPropertiesBag implements ToSmithyBuilder { +public final class SymbolDependency extends TypedPropertiesBag + implements ToSmithyBuilder, Comparable { private final String dependencyType; private final String packageName; @@ -67,7 +68,7 @@ public final class SymbolDependency extends TypedPropertiesBag implements ToSmit private SymbolDependency(Builder builder) { super(builder.properties); - this.dependencyType = SmithyBuilder.requiredState("dependencyType", builder.dependencyType); + this.dependencyType = builder.dependencyType == null ? "" : builder.dependencyType; this.packageName = SmithyBuilder.requiredState("packageName", builder.packageName); this.version = SmithyBuilder.requiredState("version", builder.version); } @@ -145,6 +146,27 @@ public int hashCode() { return Objects.hash(dependencyType, packageName, version); } + /** + * Dependencies can be sorted based on the natural sort order of + * the dependencyType, packageName, and finally the version. + * + * {@inheritDoc} + */ + @Override + public int compareTo(SymbolDependency other) { + int typeResult = dependencyType.compareTo(other.dependencyType); + if (typeResult != 0) { + return typeResult; + } + + int packageResult = packageName.compareTo(other.packageName); + if (packageResult != 0) { + return packageResult; + } + + return version.compareTo(other.version); + } + /** * Builds a SymbolDependency. */ diff --git a/codegen/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/SymbolDependencyTest.java b/codegen/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/SymbolDependencyTest.java index eac1042f786..889b1514f90 100644 --- a/codegen/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/SymbolDependencyTest.java +++ b/codegen/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/SymbolDependencyTest.java @@ -1,8 +1,12 @@ package software.amazon.smithy.codegen.core; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import org.junit.jupiter.api.Test; public class SymbolDependencyTest { @@ -40,4 +44,18 @@ public void hasProperties() { assertThat(dependency.expectProperty("foo", String.class), equalTo("baz!")); } + + @Test + public void canBeSorted() { + SymbolDependency a = SymbolDependency.builder().dependencyType("a").packageName("a").version("1").build(); + SymbolDependency a2 = SymbolDependency.builder().dependencyType("a").packageName("a2").version("1").build(); + SymbolDependency a3 = SymbolDependency.builder().dependencyType("a2").packageName("a").version("1").build(); + SymbolDependency b = SymbolDependency.builder().dependencyType("b").packageName("b").version("1").build(); + SymbolDependency b2 = SymbolDependency.builder().dependencyType("b").packageName("b").version("2").build(); + SymbolDependency c = SymbolDependency.builder().dependencyType("b").packageName("c").version("1").build(); + List dependencies = Arrays.asList(c, b2, b, a3, a2, a); + Collections.sort(dependencies); + + assertThat(dependencies, contains(a, a2, a3, b, b2, c)); + } }