Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion lib/hex_solver.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ defmodule HexSolver do
@type result() :: %{package() => {Version.t(), repo()}}
@opaque constraint() :: HexSolver.Requirement.t()

alias HexSolver.{Failure, Requirement, Solver}
alias HexSolver.{Constraint, Failure, Requirement, Solver}

@doc """
Runs the version solver.
Expand Down Expand Up @@ -83,4 +83,14 @@ defmodule HexSolver do
def parse_constraint!(string) do
Requirement.to_constraint!(string)
end

@doc """
Serializes an internal solver constraint as an Elixir version requirement.
"""
@spec constraint_to_requirement!(constraint()) :: String.t()
def constraint_to_requirement!(constraint) do
requirement = Constraint.to_requirement(constraint)
Version.parse_requirement!(requirement)
requirement
end
end
1 change: 1 addition & 0 deletions lib/hex_solver/constraint.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ defprotocol HexSolver.Constraint do
def intersect(left, right)
def union(left, right)
def compare(left, right)
def to_requirement(constraint)
end
4 changes: 4 additions & 0 deletions lib/hex_solver/constraints/empty.ex
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ defmodule HexSolver.Constraints.Empty do
clauses: []
end

def to_requirement(%Empty{}) do
"< 0.0.0-0"
end

def to_string(%Empty{}) do
"empty"
end
Expand Down
3 changes: 3 additions & 0 deletions lib/hex_solver/constraints/impl.ex
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ defmodule HexSolver.Constraints.Impl do

def compare(left, right),
do: unquote(__CALLER__.module).compare(left, right)

def to_requirement(constraint),
do: unquote(__CALLER__.module).to_requirement(constraint)
end
end
end
Expand Down
8 changes: 8 additions & 0 deletions lib/hex_solver/constraints/range.ex
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,14 @@ defmodule HexSolver.Constraints.Range do
def normalize(%Range{} = range), do: range
def normalize(%Elixir.Version{} = version), do: version

def to_requirement(%Range{min: nil, max: nil}) do
">= 0.0.0-0"
end

def to_requirement(%Range{} = range) do
Range.to_string(range)
end

def to_string(%Range{min: nil, max: nil}) do
"any"
end
Expand Down
4 changes: 4 additions & 0 deletions lib/hex_solver/constraints/union.ex
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ defmodule HexSolver.Constraints.Union do
defp maybe_to_range(%Elixir.Version{} = version), do: Version.to_range(version)
defp maybe_to_range(other), do: other

def to_requirement(%Union{ranges: ranges}) do
Enum.map_join(ranges, " or ", &Constraint.to_requirement/1)
end

def to_string(%Union{ranges: ranges}) do
Enum.map_join(ranges, " or ", &Kernel.to_string/1)
end
Expand Down
4 changes: 4 additions & 0 deletions lib/hex_solver/constraints/version.ex
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ defmodule HexSolver.Constraints.Version do
end
end

def to_requirement(%Version{} = version) do
Kernel.to_string(version)
end

def max(left, right) do
case compare(left, right) do
:lt -> right
Expand Down
24 changes: 22 additions & 2 deletions lib/hex_solver/requirement.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule HexSolver.Requirement do
@moduledoc false

alias HexSolver.Constraints.{Range, Util}
alias HexSolver.Constraints.{Empty, Range, Util}
alias HexSolver.Requirement.Parser

@allowed_range_ops [:>, :>=, :<, :<=, :~>]
Expand Down Expand Up @@ -43,7 +43,9 @@ defmodule HexSolver.Requirement do
end

defp delex([], acc) do
Util.union(acc)
acc
|> Enum.map(&normalize_constraint/1)
|> Util.union()
end

defp delex([op | rest], acc) when op in [:||, :or] do
Expand Down Expand Up @@ -144,6 +146,24 @@ defmodule HexSolver.Requirement do
defp to_version({major, minor, patch, pre, _build}),
do: %Elixir.Version{major: major, minor: minor, patch: patch, pre: pre}

defp normalize_constraint(%Range{
max: %Elixir.Version{major: 0, minor: 0, patch: 0, pre: [0]},
include_max: false
}) do
%Empty{}
end

defp normalize_constraint(
%Range{
min: %Elixir.Version{major: 0, minor: 0, patch: 0, pre: [0]},
include_min: true
} = range
) do
%{range | min: nil, include_min: false}
end

defp normalize_constraint(constraint), do: constraint

# Vendored from https://github.com/elixir-lang/elixir/blob/0ff6522/lib/elixir/lib/version.ex#L495
defmodule Parser do
@moduledoc false
Expand Down
9 changes: 9 additions & 0 deletions test/hex_solver/constraint_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,13 @@ defmodule HexSolver.ConstraintTest do
end
end
end

property "constraint requirements round-trip" do
check all constraint <- constraint() do
requirement = Constraint.to_requirement(constraint)

assert {:ok, _requirement} = Version.parse_requirement(requirement)
assert HexSolver.parse_constraint!(requirement) == constraint
end
end
end
4 changes: 4 additions & 0 deletions test/hex_solver/constraints/empty_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ defmodule HexSolver.Constraints.EmptyTest do
assert Empty.to_string(%Empty{}) == "empty"
end

test "to_requirement/1" do
assert Constraint.to_requirement(%Empty{}) == "< 0.0.0-0"
end

test "Kernel.inspect/1" do
assert inspect(%Empty{}) == "#Empty<>"
end
Expand Down
9 changes: 9 additions & 0 deletions test/hex_solver/constraints/range_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ defmodule HexSolver.Constraints.RangeTest do
use HexSolver.Case, async: true
use ExUnitProperties

alias HexSolver.Constraint
alias HexSolver.Constraints.{Empty, Range, Union, Util, Version}

describe "valid?/1" do
Expand Down Expand Up @@ -724,6 +725,14 @@ defmodule HexSolver.Constraints.RangeTest do
end
end

test "to_string/1 describes any range" do
assert Range.to_string(%Range{}) == "any"
end

test "to_requirement/1 serializes any range" do
assert Constraint.to_requirement(%Range{}) == ">= 0.0.0-0"
end

property "Kernel.inspect/1" do
check all range <- range() do
assert is_binary(inspect(range))
Expand Down
6 changes: 6 additions & 0 deletions test/hex_solver/constraints/union_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ defmodule HexSolver.Constraints.UnionTest do
end
end

test "to_requirement/1" do
union = %Union{ranges: [v("1.0.0"), %HexSolver.Constraints.Range{min: v("2.0.0")}]}

assert Constraint.to_requirement(union) == "1.0.0 or > 2.0.0"
end

property "Kernel.inspect/1" do
check all union <- union() do
assert is_binary(inspect(union))
Expand Down
4 changes: 4 additions & 0 deletions test/hex_solver/constraints/version_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -196,4 +196,8 @@ defmodule HexSolver.Constraints.VersionTest do
assert Range.single_version?(Version.to_range(version))
end
end

test "to_requirement/1" do
assert Constraint.to_requirement(v("1.2.3")) == "1.2.3"
end
end
20 changes: 19 additions & 1 deletion test/hex_solver/requirement_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ defmodule HexSolver.RequirementTest do
use ExUnitProperties

alias HexSolver.Requirement
alias HexSolver.Constraints.{Range, Union}
alias HexSolver.Constraints.{Empty, Range, Union}

describe "to_constraint!/" do
property "always converts" do
Expand Down Expand Up @@ -38,6 +38,24 @@ defmodule HexSolver.RequirementTest do
max: v("2.0.0-0"),
include_min: true
}

assert_raise Version.InvalidRequirementError, fn ->
Requirement.to_constraint!("< 0.0.0-0 and >= 1.0.0")
end
end

test "minimum version range" do
assert Requirement.to_constraint!(">= 0.0.0-0") == %Range{}
assert Requirement.to_constraint!("< 0.0.0-0") == %Empty{}

assert Requirement.to_constraint!(">= 0.0.0-0 and < 1.0.0") == %Range{
max: v("1.0.0")
}

assert Requirement.to_constraint!("< 0.0.0-0 or >= 1.0.0") == %Range{
min: v("1.0.0"),
include_min: true
}
end
end
end
34 changes: 33 additions & 1 deletion test/hex_solver_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule HexSolverTest do
use HexSolver.Case, async: true

alias HexSolver.Registry.Process, as: Registry
alias HexSolver.Constraints.Range
alias HexSolver.Constraints.{Empty, Range}

defp run(dependencies) do
HexSolver.run(Registry, to_dependencies(dependencies), [], [])
Expand Down Expand Up @@ -63,4 +63,36 @@ defmodule HexSolverTest do
HexSolver.parse_constraint!("1.2.3.4")
end
end

describe "constraint_to_requirement!/1" do
test "serializes any constraint as a valid requirement" do
requirement = HexSolver.constraint_to_requirement!(%Range{})

assert requirement == ">= 0.0.0-0"
assert {:ok, _requirement} = Version.parse_requirement(requirement)
assert HexSolver.parse_constraint!(requirement) == %Range{}
end

test "serializes version constraints" do
assert HexSolver.constraint_to_requirement!(Version.parse!("1.2.3")) == "1.2.3"
end

test "serializes bounded range constraints" do
constraint = HexSolver.parse_constraint!("~> 1.2")
assert HexSolver.constraint_to_requirement!(constraint) == "~> 1.2"
end

test "serializes union constraints" do
constraint = HexSolver.parse_constraint!("~> 1.0 or ~> 2.0")
assert HexSolver.constraint_to_requirement!(constraint) == "~> 1.0 or ~> 2.0"
end

test "serializes empty constraint as a valid requirement" do
requirement = HexSolver.constraint_to_requirement!(%Empty{})

assert requirement == "< 0.0.0-0"
assert {:ok, _requirement} = Version.parse_requirement(requirement)
assert HexSolver.parse_constraint!(requirement) == %Empty{}
end
end
end