diff --git a/gren.json b/gren.json index 5b4f531..a14fbdc 100644 --- a/gren.json +++ b/gren.json @@ -9,6 +9,7 @@ "CLI.Parser", "CLI.PrettyPrinter", "Compiler.Backend", + "Compiler.Dependencies", "Compiler.ModuleName", "Compiler.Outline", "Compiler.PackageName", diff --git a/src/Compiler/Dependencies.gren b/src/Compiler/Dependencies.gren new file mode 100644 index 0000000..af39c11 --- /dev/null +++ b/src/Compiler/Dependencies.gren @@ -0,0 +1,86 @@ +module Compiler.Dependencies exposing + ( SimplifiedOutline + , Solution(..) + , solve + ) + + +import Compiler.Outline as Outline exposing (Outline) +import Compiler.PackageName as PackageName exposing (PackageName) +import Dict exposing (Dict) +import SemanticVersion exposing (SemanticVersion) +import SemanticVersionRange exposing (SemanticVersionRange) + + +type alias SimplifiedOutline = + { name : PackageName + , version : SemanticVersionRange + , dependencies : Dict String SemanticVersionRange + } + + +type Solution + = Complete + | Missing { name : PackageName, version : SemanticVersion } + | Conflict { name : PackageName, version1 : SemanticVersionRange, version2 : SemanticVersionRange } + + +solve : Array { name : PackageName, version : SemanticVersionRange } -> Dict String SimplifiedOutline -> Solution +solve rootRequirements loadedOutlines = + solveHelp rootRequirements Dict.empty loadedOutlines + + +solveHelp + : Array { name : PackageName, version : SemanticVersionRange } + -> Dict String SimplifiedOutline + -> Dict String SimplifiedOutline + -> Solution +solveHelp pending solved loaded = + when Array.popFirst pending is + Nothing -> + Complete + + Just { first = { name = packageName, version = packageVersion }, rest } -> + let + packageNameStr = + PackageName.toString packageName + in + when Dict.get packageNameStr solved is + Just outline -> + when SemanticVersionRange.intersect outline.version packageVersion is + Nothing -> + Conflict + { name = outline.name + , version1 = outline.version + , version2 = packageVersion + } + + Just intersectedVersion -> + solveHelp + rest + (Dict.set packageNameStr { outline | version = intersectedVersion } solved) + loaded + + Nothing -> + when Dict.get packageNameStr loaded is + Nothing -> + Missing + { name = packageName + , version = SemanticVersionRange.lowerBound packageVersion + } + + Just outline -> + let + newPending = + outline.dependencies + |> Dict.foldl + (\name version acc -> + Array.pushLast + { name = PackageName.fromString name |> Maybe.withDefault PackageName.example + , version = version + } + acc + ) + rest + in + solveHelp newPending (Dict.set packageNameStr outline solved) loaded diff --git a/src/Compiler/PackageName.gren b/src/Compiler/PackageName.gren index e8b3ecb..37eff84 100644 --- a/src/Compiler/PackageName.gren +++ b/src/Compiler/PackageName.gren @@ -1,5 +1,6 @@ module Compiler.PackageName exposing ( PackageName + , example -- , author , name @@ -24,6 +25,13 @@ import Json.Encode as Json import Json.Decode as Decode exposing (Decoder) +{-| An example package name. Useful for tests. +-} +example : PackageName +example = + PackageName { author = "example", name = "package" } + + {-| A package is identified by a string in the following format: author/name diff --git a/src/SemanticVersion.gren b/src/SemanticVersion.gren index 4a53dbd..e3eef48 100644 --- a/src/SemanticVersion.gren +++ b/src/SemanticVersion.gren @@ -1,5 +1,6 @@ module SemanticVersion exposing ( SemanticVersion + , compare , fromString , cliParser , jsonDecoder @@ -35,6 +36,21 @@ type alias SemanticVersion = } +compare : SemanticVersion -> SemanticVersion -> Order +compare left right = + when Basics.compare left.major right.major is + EQ -> + when Basics.compare left.minor right.minor is + EQ -> + Basics.compare left.patch right.patch + + otherMinor -> + otherMinor + + otherMajor -> + otherMajor + + {-| Convert a `String` into a [SemanticVersion](#SemanticVersion). -} fromString : String -> Maybe SemanticVersion diff --git a/src/SemanticVersionRange.gren b/src/SemanticVersionRange.gren index d536aff..76dea57 100644 --- a/src/SemanticVersionRange.gren +++ b/src/SemanticVersionRange.gren @@ -1,7 +1,10 @@ module SemanticVersionRange exposing ( SemanticVersionRange + , of + , example , lowerBound , upperBound + , intersect , fromString , jsonDecoder , toString @@ -18,6 +21,23 @@ type SemanticVersionRange = SemanticVersionRange { lower : SemanticVersion, upper : SemanticVersion } +of : SemanticVersion -> SemanticVersion -> Maybe SemanticVersionRange +of lower upper = + if SemanticVersion.compare lower upper /= GT then + Just (SemanticVersionRange { lower = lower, upper = upper }) + + else + Nothing + + +example : SemanticVersionRange +example = + SemanticVersionRange + { lower = { major = 1, minor = 0, patch = 0 } + , upper = { major = 2, minor = 0, patch = 0 } + } + + lowerBound : SemanticVersionRange -> SemanticVersion lowerBound (SemanticVersionRange { lower }) = lower @@ -28,6 +48,60 @@ upperBound (SemanticVersionRange { upper }) = upper +intersect : SemanticVersionRange -> SemanticVersionRange -> Maybe SemanticVersionRange +intersect left right = + let + (SemanticVersionRange { lower = lowerLeft, upper = upperLeft }) = + left + + (SemanticVersionRange { lower = lowerRight, upper = upperRight }) = + right + in + when SemanticVersion.compare upperLeft upperRight is + LT -> + if SemanticVersion.compare upperLeft lowerRight /= GT then + Nothing + + else + Just <| + when SemanticVersion.compare lowerLeft lowerRight is + LT -> + SemanticVersionRange { lower = lowerRight, upper = upperLeft } + + EQ -> + SemanticVersionRange { lower = lowerLeft, upper = upperLeft } + + GT -> + SemanticVersionRange { lower = lowerLeft, upper = upperLeft } + + EQ -> + Just <| + when SemanticVersion.compare lowerLeft lowerRight is + LT -> + SemanticVersionRange { lower = lowerRight, upper = upperLeft } + + EQ -> + SemanticVersionRange { lower = lowerLeft, upper = upperLeft } + + GT -> + SemanticVersionRange { lower = lowerLeft, upper = upperLeft } + + GT -> + if SemanticVersion.compare upperRight lowerLeft /= GT then + Nothing + else + Just <| + when SemanticVersion.compare lowerLeft lowerRight is + LT -> + SemanticVersionRange { lower = lowerLeft, upper = upperRight } + + EQ -> + SemanticVersionRange { lower = lowerLeft, upper = upperRight } + + GT -> + SemanticVersionRange { lower = lowerRight, upper = upperRight } + + fromString : String -> Maybe SemanticVersionRange fromString str = when str |> String.keepIf (\char -> char /= ' ') |> String.split "<=v<" is diff --git a/tests/src/Main.gren b/tests/src/Main.gren index 155dfd0..3ee29c3 100644 --- a/tests/src/Main.gren +++ b/tests/src/Main.gren @@ -6,6 +6,7 @@ import Test.CLI.Parser as CLIParser import Test.CLI.PrettyPrinter as PrettyPrinter import Test.SemanticVersion as SemanticVersion import Test.SemanticVersionRange as SemanticVersionRange +import Test.Compiler.Dependencies as Dependencies import Test.Compiler.PackageName as PackageName import Test.Compiler.ModuleName as ModuleName import Test.String.EditDistance as EditDistance @@ -17,6 +18,7 @@ main = TestRunner.run <| Test.describe "Gren Compiler Node tests" [ CLIParser.tests + , Dependencies.tests , PrettyPrinter.tests , SemanticVersion.tests , SemanticVersionRange.tests diff --git a/tests/src/Test/Compiler/Dependencies.gren b/tests/src/Test/Compiler/Dependencies.gren new file mode 100644 index 0000000..57b08d3 --- /dev/null +++ b/tests/src/Test/Compiler/Dependencies.gren @@ -0,0 +1,129 @@ +module Test.Compiler.Dependencies exposing (tests) + +import Expect exposing (Expectation) +import Test exposing (Test, describe, test) +import Compiler.Dependencies as Deps +import Compiler.PackageName as PackageName exposing (PackageName) +import Dict exposing (Dict) +import SemanticVersion exposing (SemanticVersion) +import SemanticVersionRange exposing (SemanticVersionRange) + + +tests : Test +tests = + describe "Compiler.Dependencies" + [ describe "solve" + [ test "returns empty array for a project without dependencies" <| \{} -> + Deps.solve [] Dict.empty + |> Expect.equal Deps.Complete + , test "If root defines a dependency that isn't provided with an outline, that package is returned" <| \{} -> + let + rootDeps = + [ { name = packageName "my/first", version = versionRange 1 1 0 } + ] + in + Deps.solve rootDeps Dict.empty + |> Expect.equal (missing "my/first" 1 1 0) + , test "Finds missing package" <| \{} -> + let + rootDeps = + [ { name = packageName "my/first", version = versionRange 1 1 0 } + , { name = packageName "my/second", version = versionRange 2 0 0 } + , { name = packageName "your/first", version = versionRange 1 0 0 } + ] + + loaded = + Dict.empty + |> insertDep "my/first" 1 1 0 [] + |> insertDep "your/first" 1 0 0 [] + in + Deps.solve rootDeps loaded + |> Expect.equal (missing "my/second" 2 0 0) + , test "Finds missing transitive package" <| \{} -> + let + rootDeps = + [ { name = packageName "my/first", version = versionRange 1 1 0 } + ] + + loaded = + Dict.empty + |> insertDep "my/first" 1 1 0 + [ { name = packageName "your/first", version = versionRange 1 5 0 }] + in + Deps.solve rootDeps loaded + |> Expect.equal (missing "your/first" 1 5 0) + , test "Reports unsolvable conflicts" <| \{} -> + let + rootDeps = + [ { name = packageName "my/first", version = versionRange 1 1 0 } + , { name = packageName "your/first", version = versionRange 2 0 0 } + ] + + loaded = + Dict.empty + |> insertDep "my/first" 1 1 0 + [ { name = packageName "your/first", version = versionRange 1 5 0 }] + |> insertDep "your/first" 2 0 0 [] + in + Deps.solve rootDeps loaded + |> Expect.equal (conflict "your/first" (versionRange 2 0 0) (versionRange 1 5 0)) + ] + ] + + +missing : String -> Int -> Int -> Int -> Deps.Solution +missing name major minor patch = + Deps.Missing + { name = packageName name + , version = { major = major, minor = minor, patch = patch } + } + + +conflict : String -> SemanticVersionRange -> SemanticVersionRange -> Deps.Solution +conflict name versionOne versionTwo = + Deps.Conflict + { name = packageName name + , version1 = versionOne + , version2 = versionTwo + } + + +packageName : String -> PackageName +packageName str = + PackageName.fromString str + |> Maybe.withDefault PackageName.example + + +versionRange : Int -> Int -> Int -> SemanticVersionRange +versionRange major minor patch = + let + upperBound = + { major = major + 1 + , minor = 0 + , patch = 0 + } + in + SemanticVersionRange.of { major = major, minor = minor, patch = patch } upperBound + |> Maybe.withDefault SemanticVersionRange.example + + +insertDep + : String + -> Int + -> Int + -> Int + -> Array { name : PackageName, version : SemanticVersionRange } + -> Dict String Deps.SimplifiedOutline + -> Dict String Deps.SimplifiedOutline +insertDep name major minor patch deps dict = + Dict.set + name + { name = packageName name + , version = versionRange major minor patch + , dependencies = + Array.foldl + (\dep -> Dict.set (PackageName.toString dep.name) dep.version) + Dict.empty + deps + } + dict diff --git a/tests/src/Test/SemanticVersion.gren b/tests/src/Test/SemanticVersion.gren index 606c84a..db5157b 100644 --- a/tests/src/Test/SemanticVersion.gren +++ b/tests/src/Test/SemanticVersion.gren @@ -62,4 +62,24 @@ tests = Nothing -> Expect.pass ] + , describe "compare" + [ test "LT" <| \{} -> + Expect.all + [ SV.compare { major = 1, minor = 0, patch = 0 } >> Expect.equal LT + , SV.compare { major = 1, minor = 1, patch = 0 } >> Expect.equal LT + , SV.compare { major = 1, minor = 5, patch = 0 } >> Expect.equal LT + ] + { major = 1, minor = 5, patch = 1 } + , test "EQ" <| \{} -> + { major = 2, minor = 1, patch = 5 } + |> SV.compare { major = 2, minor = 1, patch = 5 } + |> Expect.equal EQ + , test "GT" <| \{} -> + Expect.all + [ SV.compare { major = 1, minor = 5, patch = 2 } >> Expect.equal GT + , SV.compare { major = 1, minor = 6, patch = 0 } >> Expect.equal GT + , SV.compare { major = 2, minor = 0, patch = 0 } >> Expect.equal GT + ] + { major = 1, minor = 5, patch = 1 } + ] ] diff --git a/tests/src/Test/SemanticVersionRange.gren b/tests/src/Test/SemanticVersionRange.gren index 0304910..22b2bea 100644 --- a/tests/src/Test/SemanticVersionRange.gren +++ b/tests/src/Test/SemanticVersionRange.gren @@ -10,7 +10,19 @@ import SemanticVersionRange as Range tests : Test tests = describe "SemanticVersionRange" - [ describe "fromString" + [ describe "of" + [ test "Simple example" <| \{} -> + Range.of version110 version200 + |> Maybe.withDefault Range.example + |> Expect.all + [ Range.lowerBound >> SV.toString >> Expect.equal "1.1.0" + , Range.upperBound >> SV.toString >> Expect.equal "2.0.0" + ] + , test "Left side must be lower than right side" <| \{} -> + Range.of version200 version110 + |> Expect.equal Nothing + ] + , describe "fromString" [ test "Simple example" <| \{} -> when Range.fromString "1.2.0 <= v < 2.0.0" is Just range -> @@ -41,4 +53,42 @@ tests = Nothing -> Expect.pass ] + , describe "intersect" + [ test "table" <| \{} -> + Expect.all + [ Range.intersect (rangeFrom 1 7 0) >> Expect.equal (Just (rangeFrom 1 7 0)) + , Range.intersect (rangeFrom 1 5 0) >> Expect.equal (Just (rangeFrom 1 5 0)) + , Range.intersect (rangeFrom 1 2 0) >> Expect.equal (Just (rangeFrom 1 5 0)) + , Range.intersect (exact 1 5 0) >> Expect.equal (Just (exact 1 5 0)) + , Range.intersect (exact 1 5 2) >> Expect.equal (Just (exact 1 5 2)) + , Range.intersect (exact 1 9 9) >> Expect.equal (Just (exact 1 9 9)) + , Range.intersect (exact 2 0 0) >> Expect.equal (Nothing) + , Range.intersect (exact 1 4 5) >> Expect.equal (Nothing) + ] + (rangeFrom 1 5 0) + ] ] + + +version110 : SV.SemanticVersion +version110 = { major = 1, minor = 1, patch = 0 } + + +version200 : SV.SemanticVersion +version200 = { major = 2, minor = 0, patch = 0 } + + +exact : Int -> Int -> Int -> Range.SemanticVersionRange +exact major minor patch = + Range.of + { major = major, minor = minor, patch = patch } + { major = major, minor = minor, patch = patch + 1 } + |> Maybe.withDefault Range.example + + +rangeFrom : Int -> Int -> Int -> Range.SemanticVersionRange +rangeFrom major minor patch = + Range.of + { major = major, minor = minor, patch = patch } + { major = major + 1, minor = 0, patch = 0 } + |> Maybe.withDefault Range.example