From e5eeb643dab8c51eaccaa46216eea7faabf7b3ee Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Thu, 19 Sep 2019 16:47:10 -0700 Subject: [PATCH] Add method to gather dependencies This commit adds a couple helper methods used to aggregate symbol dependencies into a map of maps. By default, conflicting package versions will throw when two packages conflict. The merge behavior can be customized by providing a custom merge function (for example, it can de-conflict, throw, implement a better, picking strategy, etc.). --- .../smithy/codegen/core/SymbolDependency.java | 68 +++++++++++++++++++ .../codegen/core/SymbolDependencyTest.java | 43 ++++++++++++ 2 files changed, 111 insertions(+) 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 345a075db23..4f77b70348c 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 @@ -15,7 +15,13 @@ package software.amazon.smithy.codegen.core; +import java.util.Map; import java.util.Objects; +import java.util.TreeMap; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; import software.amazon.smithy.utils.SmithyBuilder; import software.amazon.smithy.utils.ToSmithyBuilder; @@ -80,6 +86,68 @@ public static Builder builder() { return new Builder(); } + /** + * Gets a mapping of all dependencies used by the provided symbols. + * + *

Given a stream of symbols, the dependencies of the symbol are gathered into + * a map of the dependencyType to a map of a package name to package version. + * + *

By default, when two versions conflict, an exception is thrown. In the + * case the a conflict is possible or it is necessary to detect incompatibilities, + * use {@link #gatherDependencies(Stream, BinaryOperator)} and provide a + * custom version merge function. + * + * @param symbolStream Stream of symbols to compute from. + * @return Returns a map of dependency types to a map of package to version. + * @throws CodegenException when two package versions conflict. + */ + public static Map> gatherDependencies( + Stream symbolStream) { + return gatherDependencies(symbolStream, (a, b) -> { + throw new CodegenException(String.format( + "Found a conflicting `%s` dependency for `%s`: `%s` conflicts with `%s`", + a.getDependencyType(), a.getPackageName(), a.getVersion(), b.getVersion())); + }); + } + + /** + * Gets a mapping of all dependencies used by the provided symbols. + * + *

Given a stream of symbols, the dependencies of the symbol are gathered into + * a map of the dependencyType to a map of a package name to package version. + * Dependencies are sorted while they are collected, meaning that newer versions + * of a conflicting dependency typically take precedence over older versions. + * However, this is not always true with a natural sort order + * (e.g., 0.9 and 0.10). + * + *

{@code versionMergeFunction} is invoked each time a package import version + * of a package conflicts with another version of the same package for the + * same dependency type. The function accepts the dependency type, the package + * name, the previous version that was registered, the new conflicting version, + * and is expected to return the version that should be used or can throw in + * the case of an incompatible conflict. It is a target-specific concern to + * determine if two version are compatible or to find an acceptable compromise + * between the two versions. + * + * @param symbolStream Stream of symbols to compute from. + * @param versionMergeFunction Function that determines which two conflicting versions wins. + * @return Returns a map of dependency types to a map of package to version. + */ + public static Map> gatherDependencies( + Stream symbolStream, + BinaryOperator versionMergeFunction + ) { + return symbolStream + .sorted() + .collect(Collectors.groupingBy( + SymbolDependency::getDependencyType, + Collectors.toMap( + SymbolDependency::getPackageName, + Function.identity(), + versionMergeFunction, + TreeMap::new))); + } + /** * Gets the type of dependency (for example, "dev", "optional", etc). * 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 889b1514f90..03135004266 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 @@ -3,11 +3,17 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasKey; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import software.amazon.smithy.utils.Pair; public class SymbolDependencyTest { @Test @@ -58,4 +64,41 @@ public void canBeSorted() { assertThat(dependencies, contains(a, a2, a3, b, b2, c)); } + + @Test + public void gathersDependencies() { + 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(); + + Assertions.assertThrows(CodegenException.class, () -> { + SymbolDependency.gatherDependencies(Stream.of(a, a2, a3, b, b2, c)); + }); + + List> conflicts = new ArrayList<>(); + Map> result = SymbolDependency.gatherDependencies( + Stream.of(a, a2, a3, b, b2, c), + (sa, sb) -> { + conflicts.add(Pair.of(sa, sb)); + return sb; + }); + + assertThat(conflicts, contains(Pair.of(b, b2))); + assertThat(result, hasKey("a")); + assertThat(result, hasKey("a2")); + assertThat(result, hasKey("b")); + assertThat(result.get("a"), hasKey("a")); + assertThat(result.get("a"), hasKey("a2")); + assertThat(result.get("a").get("a"), equalTo(a)); + assertThat(result.get("a").get("a2"), equalTo(a2)); + assertThat(result.get("a2"), hasKey("a")); + assertThat(result.get("a2").get("a"), equalTo(a3)); + assertThat(result.get("b"), hasKey("b")); + assertThat(result.get("b").get("b"), equalTo(b2)); + assertThat(result.get("b"), hasKey("c")); + assertThat(result.get("b").get("c"), equalTo(c)); + } }