defmodule HexSolver.Term do @moduledoc false alias HexSolver.{Constraint, PackageRange, Term} alias HexSolver.Constraints.Empty defstruct positive: true, package_range: nil, optional: false def relation(left, right, opts \\ []) do if Keyword.get(opts, :optionals, true) do case do_relation(left, right) do :disjoint -> if left.optional and not right.optional, do: :overlapping, else: :disjoint other -> if left.optional and right.optional, do: :disjoint, else: other end else do_relation(left, right) end end def do_relation(%Term{} = left, %Term{} = right) do left_constraint = constraint(left) right_constraint = constraint(right) cond do right.positive and left.positive -> cond do not compatible_package?(left, right) -> :disjoint Constraint.allows_all?(right_constraint, left_constraint) -> :subset not Constraint.allows_any?(left_constraint, right_constraint) -> :disjoint true -> :overlapping end right.positive and not left.positive -> cond do not compatible_package?(left, right) -> :overlapping Constraint.allows_all?(left_constraint, right_constraint) -> :disjoint true -> :overlapping end not right.positive and left.positive -> cond do not compatible_package?(left, right) -> :subset not Constraint.allows_any?(right_constraint, left_constraint) -> :subset Constraint.allows_all?(right_constraint, left_constraint) -> :disjoint true -> :overlapping end not right.positive and not left.positive -> cond do not compatible_package?(left, right) -> :overlapping Constraint.allows_all?(left_constraint, right_constraint) -> :subset true -> :overlapping end end end def intersect(%Term{} = left, %Term{} = right) do if compatible_package?(left, right) do optional = left.optional and right.optional cond do left.positive != right.positive -> positive = if left.positive, do: left, else: right negative = if left.positive, do: right, else: left constraint = Constraint.difference(constraint(positive), constraint(negative)) non_empty_term(left, constraint, optional, true) left.positive and right.positive -> constraint = Constraint.intersect(constraint(left), constraint(right)) non_empty_term(left, constraint, optional, true) not left.positive and not right.positive -> constraint = Constraint.union(constraint(left), constraint(right)) non_empty_term(left, constraint, optional, false) end else nil end end def difference(%Term{} = left, %Term{} = right) do intersect(left, inverse(right)) end def satisfies?(%Term{} = left, %Term{} = right, opts \\ []) do compatible_package?(left, right) and relation(left, right, opts) == :subset end def inverse(%Term{} = term) do %{term | positive: not term.positive} end def compatible_package?(%Term{package_range: %PackageRange{name: "$root"}}, %Term{}) do true end def compatible_package?(%Term{}, %Term{package_range: %PackageRange{name: "$root"}}) do true end def compatible_package?(%Term{package_range: left}, %Term{package_range: right}) do {left.repo, left.name} == {right.repo, right.name} end defp constraint(%Term{package_range: %PackageRange{constraint: constraint}}) do constraint end defp non_empty_term(_term, %Empty{}, _optional, _positive) do nil end defp non_empty_term(term, constraint, optional, positive) do %Term{ package_range: %{term.package_range | constraint: constraint}, positive: positive, optional: optional } end def to_string(%Term{package_range: package_range, positive: positive}) do "#{positive(positive)}#{package_range}" end defp positive(true), do: "" defp positive(false), do: "not " defimpl String.Chars do defdelegate to_string(term), to: HexSolver.Term end defimpl Inspect do def inspect( %Term{package_range: package_range, positive: positive, optional: optional}, _opts ) do "#Term<#{positive(positive)}#{package_range}#{optional(optional)}>" end defp positive(true), do: "" defp positive(false), do: "not " defp optional(true), do: " (optional)" defp optional(false), do: "" end end