Packages

A CLI tool to manage package dependencies

Current section

Files

Jump to
wand lib mode.ex
Raw

lib/mode.ex

defmodule Wand.Mode do
@type t :: :caret | :tilde | :exact | :custom
@type requirement :: String.t() | {:latest, t}
@type version :: String.t() | :latest | Version.t()
@no_patch ~r/^(\d+)\.(\d+)($|\+.*$|-.*$)/
@moduledoc """
Each requirement in a wand.json follows some type of pattern. An exact pattern is one where the version is exactly specified. A tilde mode allows the patch version to be updated, and the caret mode allows the minor version to be updated.
"""
@doc """
Determine the mode that the requirement is currently using. If the requirement is not one of the types set by wand, it is marked as custom.
## Examples
iex> Wand.Mode.from_requirement("== 2.3.2")
:exact
iex> Wand.Mode.from_requirement("~> 2.3.2")
:tilde
iex> Wand.Mode.from_requirement(">= 2.3.2 and < 3.0.0")
:caret
iex> Wand.Mode.from_requirement(">= 2.3.2 and < 3.0.0 and != 2.3.3")
:custom
"""
@spec from_requirement(requirement) :: t
def from_requirement(:latest), do: :caret
def from_requirement(requirement) do
case Version.Parser.lexer(requirement, []) do
[:==, _] -> :exact
[:~>, _] -> :tilde
[:>=, _, :&&, :<=, _] = p -> parse_caret_patch(p)
[:>=, _, :&&, :<, _] = p -> parse_caret(p)
_ -> :custom
end
end
@doc """
Given a mode and a version, calculate the requirement that combines both part. Throws an exception on failure.
## Examples
iex> Wand.Mode.get_requirement!(:exact, "2.3.1")
"== 2.3.1"
iex> Wand.Mode.get_requirement!(:tilde, "2.3.1")
"~> 2.3.1"
iex> Wand.Mode.get_requirement!(:caret, "2.3.1")
">= 2.3.1 and < 3.0.0"
iex> Wand.Mode.get_requirement!(:caret, "0.3.3")
">= 0.3.3 and < 0.4.0"
"""
@spec get_requirement!(t, version) :: requirement
def get_requirement!(mode, version) do
{:ok, requirement} = get_requirement(mode, version)
requirement
end
@doc """
See `Wand.Mode.get_requirement!`
On success, this returns {:ok, requirement}, and on failure, {:error, :invalid_version} is returned
"""
@spec get_requirement(t, version) :: {:ok, requirement} | {:error, :invalid_version}
def get_requirement(mode, :latest), do: {:ok, {:latest, mode}}
def get_requirement(mode, version) when is_binary(version) do
case parse(version) do
{:ok, version} -> {:ok, get_requirement(mode, version)}
:error -> {:error, :invalid_version}
end
end
def get_requirement(:caret, %Version{major: 0, minor: 0} = version) do
">= #{version} and <= #{version}"
end
def get_requirement(:caret, %Version{major: 0, minor: minor} = version) do
">= #{version} and < 0.#{minor + 1}.0"
end
def get_requirement(:caret, %Version{major: major} = version) do
">= #{version} and < #{major + 1}.0.0"
end
def get_requirement(:exact, %Version{} = version) do
"== #{version}"
end
def get_requirement(:tilde, %Version{} = version) do
"~> #{version}"
end
defp add_missing_patch(version) do
version
|> String.replace(@no_patch, "\\1.\\2.0\\3")
end
defp parse(version) do
add_missing_patch(version) |> Version.parse()
end
defp parse_caret_patch([:>=, old, :&&, :<=, new]) do
case Version.compare(old, new) do
:eq -> :caret
_ -> :custom
end
end
defp parse_caret([:>=, old, :&&, :<, new]) do
old = Version.parse!(old)
new = Version.parse!(new)
cond do
# patches are handled by parse_caret_patch
# So anything that gets to here is not a caret.
new.major == 0 and new.minor == 0 ->
:custom
new.patch != 0 ->
:custom
Version.compare(old, new) != :lt ->
:custom
# check for ^0.1.3
new.major == 0 and old.major == 0 and old.minor + 1 == new.minor ->
:caret
# throw away all other cases of 0.x
new.major == 0 or new.minor != 0 or old.major == 0 ->
:custom
# check for ^1.2.3
new.major == old.major + 1 ->
:caret
true ->
:custom
end
end
end