Skip to content

Commit 291c624

Browse files
committed
Serialize constraints as version requirements
1 parent 8648c50 commit 291c624

15 files changed

Lines changed: 141 additions & 5 deletions

lib/hex_solver.ex

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ defmodule HexSolver do
2424
@type result() :: %{package() => {Version.t(), repo()}}
2525
@opaque constraint() :: HexSolver.Requirement.t()
2626

27-
alias HexSolver.{Failure, Requirement, Solver}
27+
alias HexSolver.{Constraint, Failure, Requirement, Solver}
2828

2929
@doc """
3030
Runs the version solver.
@@ -83,4 +83,14 @@ defmodule HexSolver do
8383
def parse_constraint!(string) do
8484
Requirement.to_constraint!(string)
8585
end
86+
87+
@doc """
88+
Serializes an internal solver constraint as an Elixir version requirement.
89+
"""
90+
@spec constraint_to_requirement!(constraint()) :: String.t()
91+
def constraint_to_requirement!(constraint) do
92+
requirement = Constraint.to_requirement(constraint)
93+
Version.parse_requirement!(requirement)
94+
requirement
95+
end
8696
end

lib/hex_solver/constraint.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ defprotocol HexSolver.Constraint do
1010
def intersect(left, right)
1111
def union(left, right)
1212
def compare(left, right)
13+
def to_requirement(constraint)
1314
end

lib/hex_solver/constraints/empty.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ defmodule HexSolver.Constraints.Empty do
3434
clauses: []
3535
end
3636

37+
def to_requirement(%Empty{}) do
38+
"< 0.0.0-0"
39+
end
40+
3741
def to_string(%Empty{}) do
3842
"empty"
3943
end

lib/hex_solver/constraints/impl.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ defmodule HexSolver.Constraints.Impl do
3232

3333
def compare(left, right),
3434
do: unquote(__CALLER__.module).compare(left, right)
35+
36+
def to_requirement(constraint),
37+
do: unquote(__CALLER__.module).to_requirement(constraint)
3538
end
3639
end
3740
end

lib/hex_solver/constraints/range.ex

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,14 @@ defmodule HexSolver.Constraints.Range do
381381
def normalize(%Range{} = range), do: range
382382
def normalize(%Elixir.Version{} = version), do: version
383383

384+
def to_requirement(%Range{min: nil, max: nil}) do
385+
">= 0.0.0-0"
386+
end
387+
388+
def to_requirement(%Range{} = range) do
389+
Range.to_string(range)
390+
end
391+
384392
def to_string(%Range{min: nil, max: nil}) do
385393
"any"
386394
end

lib/hex_solver/constraints/union.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,10 @@ defmodule HexSolver.Constraints.Union do
152152
defp maybe_to_range(%Elixir.Version{} = version), do: Version.to_range(version)
153153
defp maybe_to_range(other), do: other
154154

155+
def to_requirement(%Union{ranges: ranges}) do
156+
Enum.map_join(ranges, " or ", &Constraint.to_requirement/1)
157+
end
158+
155159
def to_string(%Union{ranges: ranges}) do
156160
Enum.map_join(ranges, " or ", &Kernel.to_string/1)
157161
end

lib/hex_solver/constraints/version.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ defmodule HexSolver.Constraints.Version do
9999
end
100100
end
101101

102+
def to_requirement(%Version{} = version) do
103+
Kernel.to_string(version)
104+
end
105+
102106
def max(left, right) do
103107
case compare(left, right) do
104108
:lt -> right

lib/hex_solver/requirement.ex

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
defmodule HexSolver.Requirement do
22
@moduledoc false
33

4-
alias HexSolver.Constraints.{Range, Util}
4+
alias HexSolver.Constraints.{Empty, Range, Util}
55
alias HexSolver.Requirement.Parser
66

77
@allowed_range_ops [:>, :>=, :<, :<=, :~>]
@@ -43,7 +43,9 @@ defmodule HexSolver.Requirement do
4343
end
4444

4545
defp delex([], acc) do
46-
Util.union(acc)
46+
acc
47+
|> Enum.map(&normalize_constraint/1)
48+
|> Util.union()
4749
end
4850

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

149+
defp normalize_constraint(%Range{
150+
max: %Elixir.Version{major: 0, minor: 0, patch: 0, pre: [0]},
151+
include_max: false
152+
}) do
153+
%Empty{}
154+
end
155+
156+
defp normalize_constraint(
157+
%Range{
158+
min: %Elixir.Version{major: 0, minor: 0, patch: 0, pre: [0]},
159+
include_min: true
160+
} = range
161+
) do
162+
%{range | min: nil, include_min: false}
163+
end
164+
165+
defp normalize_constraint(constraint), do: constraint
166+
147167
# Vendored from https://github.com/elixir-lang/elixir/blob/0ff6522/lib/elixir/lib/version.ex#L495
148168
defmodule Parser do
149169
@moduledoc false

test/hex_solver/constraint_test.exs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,4 +179,13 @@ defmodule HexSolver.ConstraintTest do
179179
end
180180
end
181181
end
182+
183+
property "constraint requirements round-trip" do
184+
check all constraint <- constraint() do
185+
requirement = Constraint.to_requirement(constraint)
186+
187+
assert {:ok, _requirement} = Version.parse_requirement(requirement)
188+
assert HexSolver.parse_constraint!(requirement) == constraint
189+
end
190+
end
182191
end

test/hex_solver/constraints/empty_test.exs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ defmodule HexSolver.Constraints.EmptyTest do
1717
assert Empty.to_string(%Empty{}) == "empty"
1818
end
1919

20+
test "to_requirement/1" do
21+
assert Constraint.to_requirement(%Empty{}) == "< 0.0.0-0"
22+
end
23+
2024
test "Kernel.inspect/1" do
2125
assert inspect(%Empty{}) == "#Empty<>"
2226
end

0 commit comments

Comments
 (0)