Packages
git_hub_actions
0.1.2
0.4.2
0.4.1
0.4.0
0.3.17
0.3.16
0.3.15
0.3.14
0.3.13
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.27
0.2.26
0.2.25
0.2.24
0.2.23
0.2.22
0.2.21
0.2.20
0.2.19
0.2.18
0.2.17
0.2.16
0.2.15
0.2.14
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.11
0.1.10
0.1.9
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
A little tool to write GitHub actions in Elixir
Current section
Files
Jump to
Current section
Files
lib/git_hub_actions/version.ex
defmodule GitHubActions.Version do
@moduledoc """
Functions for parsing and matching versions against requirements.
A version is a string in a specific format or a `GitHubActions.Version` generated after
parsing via `GitHubActions.Version.parse/1`.
This module is similar to `Version` except that `minor` and `patch` may be missing
and `pre` are not supported.
The Version module can also parse a range of versions.
## Examples
iex> "2.0/2" |> Version.parse!() |> Enum.map(&to_string/1)
["2.0", "2.1", "2.2"]
iex> "1/3" |> Version.parse!() |> Enum.map(&to_string/1)
["1", "2", "3"]
"""
import Kernel, except: [match?: 2]
alias Elixir.Version.Requirement
@separator "."
@range "/"
@requirement_operators [
">=",
"<=",
"~>",
">",
"<",
"==",
"!=",
"!"
]
@fields [:major, :minor, :patch]
defstruct @fields
@type version :: String.t() | t
@type major :: non_neg_integer | nil
@type minor :: non_neg_integer | nil
@type patch :: non_neg_integer | nil
@type t :: %__MODULE__{major: major, minor: minor, patch: patch}
@type requirement :: String.t() | Requirement.t()
@doc """
Parses a version string into a `GitHubActions.Version` struct.
## Examples
iex> {:ok, version} = Version.parse("1.2")
iex> version
#Version<1.2>
iex> Version.parse("1-2")
:error
iex> {:ok, [v1, v2, v3]} = Version.parse("2.2/4")
iex> v1
#Version<2.2>
iex> v2
#Version<2.3>
iex> v3
#Version<2.4>
iex> {:ok, version} = Version.parse("1.2")
iex> {:ok, version} = Version.parse(version)
iex> version
#Version<1.2>
"""
@spec parse(String.t() | t()) :: {:ok, t()} | :error
def parse(string) when is_binary(string) do
case String.split(string, @range) do
[version] ->
create(version)
[version, last] ->
with {:ok, first} <- create(version) do
range(first, last)
end
end
end
def parse(%__MODULE__{} = version), do: {:ok, version}
@doc """
Parses a version string into a `GitHubActions.Version` struct.
If `string` is an invalid version, a GitHubActions.InvalidVersionError is raised.
## Examples
iex> Version.parse!("1")
#Version<1>
iex> Version.parse!("1.2")
#Version<1.2>
iex> Version.parse!("1.2.3")
#Version<1.2.3>
iex> Version.parse!("invalid")
** (GitHubActions.InvalidVersionError) invalid version: "invalid"
"""
@spec parse!(String.t() | t()) :: t()
def parse!(string) when is_binary(string) do
case parse(string) do
{:ok, version} -> version
:error -> raise GitHubActions.InvalidVersionError, string
end
end
def parse!(%__MODULE__{} = version), do: version
@doc """
Checks if the given version matches the specification.
Returns `true` if `version` satisfies `requirement`, `false` otherwise.
Raises a `Version.InvalidRequirementError` exception if `requirement` is not
parsable, or a `GitHubActions.InvalidVersionError` exception if `version` is
not parsable.
## Examples
iex> Version.match?("2.0", "> 1.0.0")
true
iex> Version.match?("2.0", "== 1.0.0")
false
iex> Version.match?("2.2.6", "~> 2.2.2")
true
iex> Version.match?("2.3", "~> 2.2")
true
iex> Version.match?("2", "~> 2.1.2")
false
iex> Version.match?("a.b.c", "~> 2.1.2")
** (GitHubActions.InvalidVersionError) invalid version: "a.b.c"
iex> Version.match?("2", "~~~> 2.1.2")
** (Version.InvalidRequirementError) invalid requirement: "~~~> 2.1.2"
"""
@spec match?(version(), requirement()) :: boolean
def match?(version, requirement) when is_binary(requirement) do
match?(version, Version.parse_requirement!(requirement))
end
def match?(version, requirement) do
Requirement.match?(requirement, to_matchable(version))
end
def compare(a, b, precision \\ :patch) do
do_compare(to_matchable(a), to_matchable(b), precision)
end
defp do_compare(
{major1, minor1, patch1, _pre1, _build1},
{major2, minor2, patch2, _pre2, _build2},
precision
) do
cond do
major1 > major2 -> :gt
major1 < major2 -> :lt
minor1 > minor2 and precision in [:minor, :patch] -> :gt
minor1 < minor2 and precision in [:minor, :patch] -> :lt
patch1 > patch2 and precision == :patch -> :gt
patch1 < patch2 and precision == :patch -> :lt
true -> :eq
end
end
defp create(string) do
string
|> String.split(@separator)
|> Enum.map(&to_integer/1)
|> create(@fields)
end
defp create(values, keys, data \\ [])
defp create([], _keys, data) do
{:ok, struct!(__MODULE__, Enum.reverse(data))}
end
defp create([{:ok, value} | values], [key | keys], data) when is_integer(value) do
create(values, keys, [{key, value} | data])
end
defp create(_values, _keys, _data), do: :error
defp to_integer(str) do
case Integer.parse(str) do
{int, ""} -> {:ok, int}
_error -> :error
end
end
defp to_matchable(str) when is_binary(str) do
str |> parse!() |> to_matchable()
end
defp to_matchable(%__MODULE__{major: major, minor: minor, patch: patch}) do
# The last two values are in the tuple to make it compatible to the
# Requirement matchable_pattern.
{major || 0, minor || 0, patch || 0, [], false}
end
defp range(%__MODULE__{major: major, minor: minor, patch: patch}, last) do
case Integer.parse(last) do
{int, ""} ->
{:ok, range({major, minor, patch}, int, [])}
_error ->
:error
end
end
defp range({major, nil, nil}, range, versions) when major <= range do
range({major + 1, nil, nil}, range, [
struct!(__MODULE__, major: major, minor: nil, patch: nil) | versions
])
end
defp range({major, minor, nil}, range, versions) when minor <= range do
range({major, minor + 1, nil}, range, [
struct!(__MODULE__, major: major, minor: minor, patch: nil) | versions
])
end
defp range({major, minor, patch}, range, versions) when patch <= range do
range({major, minor, patch + 1}, range, [
struct(__MODULE__, major: major, minor: minor, patch: patch) | versions
])
end
defp range(_version, _range, versions), do: Enum.reverse(versions)
@doc """
Returns the requiement for the given `version` and `operator`.
## Examples
iex> Version.to_requirement("1", "==")
"== 1.0.0"
iex> Version.to_requirement("1.1", ">=")
">= 1.1.0"
iex> Version.to_requirement("1.1", "~>")
"~> 1.1"
iex> Version.to_requirement("1.1.1", "~>")
"~> 1.1.1"
"""
def to_requirement(version, operator)
when is_binary(version) and operator in @requirement_operators do
version |> parse!() |> to_requirement(operator)
end
def to_requirement(%__MODULE__{major: major, minor: minor, patch: patch}, operator)
when operator in @requirement_operators do
case operator do
"~>" ->
"#{operator} #{major}#{next(minor)}#{next(patch)}"
_else ->
"#{operator} #{major}.#{minor || 0}.#{patch || 0}"
end
end
@doc false
def next(nil), do: ""
def next(num) when is_integer(num), do: ".#{num}"
end
defimpl String.Chars, for: GitHubActions.Version do
alias GitHubActions.Version
def to_string(version) do
"#{version.major}#{Version.next(version.minor)}#{Version.next(version.patch)}"
end
end
defimpl Inspect, for: GitHubActions.Version do
def inspect(self, _opts) do
"#Version<#{to_string(self)}>"
end
end
defmodule GitHubActions.InvalidVersionError do
defexception [:version]
@impl true
def exception(version) when is_binary(version) do
%__MODULE__{version: version}
end
@impl true
def message(%{version: version}) do
"invalid version: #{inspect(version)}"
end
end