Packages
git_hub_actions
0.4.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/versions.ex
defmodule GitHubActions.Versions do
@moduledoc """
Functions to select and filter lists and tables of versions.
The list of versions can have the following two forms.
- A simple list:
```elixir
["1", "2.0", "2.1", "3", "3.1", "3.1.1"]
```
- A table as list of keyword lists with compatible versions:
```elixir
[
[a: ["1.0.0"], b: ["1.0", "1.1", "1.2"]],
[a: ["2.0.0"], b: ["1.2", "2.0"]]
]
```
"""
alias GitHubActions.Config
alias GitHubActions.Version
alias GitHubActions.Versions.Impl
@type versions_list :: [Version.version()]
@type versions_table :: [keyword(Version.version())]
@type versions :: versions_list() | versions_table()
@type key :: atom()
@doc """
Returns the latest version from the configured versions list.
## Examples
iex> Config.config(:versions, ["1.0.0/2", "1.1.0/3"])
iex> Versions.latest()
%GitHubActions.Version{major: 1, minor: 1, patch: 3}
"""
@spec latest :: Version.t()
def latest, do: latest(from_config())
@doc """
Returns the latest version from the configured `versions` table by the given
`key` or from the given `versions` list.
## Examples
iex> Versions.latest(["1.0.0/2", "1.1.0/3"])
%GitHubActions.Version{major: 1, minor: 1, patch: 3}
iex> Config.config(:versions, [
...> [a: ["1.0.0/2", "1.1.0/3"], b: ["2.0/5"]],
...> [a: ["1.2.0/1", "1.3.0/4"], b: ["3.0/5"]]
...> ])
iex> Versions.latest(:a)
%GitHubActions.Version{major: 1, minor: 3, patch: 4}
iex> Versions.latest(["foo"])
** (GitHubActions.InvalidVersionError) invalid version: "foo"
iex> Versions.latest([a: "1"])
** (ArgumentError) latest/1 expected a list or table of versions or a key, got: [a: "1"]
iex> Versions.latest(:elixir)
%GitHubActions.Version{major: 1, minor: 20, patch: 2}
iex> Versions.latest(:otp)
%GitHubActions.Version{major: 29, minor: 0}
"""
@spec latest(versions() | key()) :: Version.t()
def latest(versions_or_key) when is_list(versions_or_key) do
case Impl.type(versions_or_key) do
{:list, versions} ->
Impl.latest(versions)
:error ->
raise ArgumentError,
message: """
latest/1 expected a list or table of versions or a key, \
got: #{inspect(versions_or_key)}\
"""
end
end
def latest(key) when is_atom(key), do: latest(from_config(), key)
@doc """
Returns the latest version from a `versions` table by the given `key`.
## Examples
iex> Versions.latest([
...> [a: ["1.0.0/2"], b: ["1.0.0/3"]],
...> [a: ["1.1.0/3"], b: ["1.1.0/4"]]
...> ], :a)
%GitHubActions.Version{major: 1, minor: 1, patch: 3}
iex> Versions.latest([a: "1"], :a)
** (ArgumentError) latest/1 expected a table of versions, got: [a: "1"]
"""
@spec latest(versions_table(), key()) :: Version.t()
def latest(versions, key) when is_list(versions) and is_atom(key) do
case Impl.type(versions) do
{:table, versions} ->
Impl.latest(versions, key)
:error ->
raise ArgumentError,
message: "latest/1 expected a table of versions, got: #{inspect(versions)}"
end
end
@doc """
Returns the latest minor versions from the configured versions list.
## Examples
iex> Config.config(:versions, ["1.0.0/2", "1.1.0/4", "2.0.0/3"])
iex> Versions.latest_minor() |> Enum.map(&to_string/1)
["1.0.2", "1.1.4", "2.0.3"]
"""
@spec latest_minor :: [Version.t()]
def latest_minor, do: latest_minor(from_config())
@doc """
Returns the latest minor versions from the configured `versions` table by the
given `key` or from the given `versions` list.
## Examples
iex> minor_versions = Versions.latest_minor(["1.0.0/2", "1.1.0/3"])
iex> Enum.map(minor_versions, &to_string/1)
["1.0.2", "1.1.3"]
iex> Config.config(:versions, [
...> [a: ["1.0.0/2", "1.1.0/3"], b: ["2.0/5"]],
...> [a: ["1.2.0/1", "1.3.0/4"], b: ["3.0/5"]]
...> ])
iex> minor_versions = Versions.latest_minor(:a)
iex> Enum.map(minor_versions, &to_string/1)
["1.0.2", "1.1.3", "1.2.1", "1.3.4"]
iex> Versions.latest_minor(["foo"])
** (GitHubActions.InvalidVersionError) invalid version: "foo"
iex> Versions.latest_minor([a: "1"])
** (ArgumentError) latest_minor/1 expected a list or table of versions or a key, got: [a: "1"]
iex> Versions.latest_minor(:elixir) |> Enum.map(&to_string/1)
["1.0.5", "1.1.1", "1.2.6", "1.3.4", "1.4.5", "1.5.3", "1.6.6", "1.7.4",
"1.8.2", "1.9.4", "1.10.4", "1.11.4", "1.12.3", "1.13.4", "1.14.5",
"1.15.8", "1.16.3", "1.17.3", "1.18.4", "1.19.5", "1.20.2"]
iex> Versions.latest_minor(:otp) |> Enum.map(&to_string/1)
["17.0", "17.1", "17.2", "17.3", "17.4", "17.5", "18.0", "18.1", "18.2",
"18.3", "19.0", "19.1", "19.2", "19.3", "20.0", "20.1", "20.2", "20.3",
"21.0", "21.1", "21.2", "21.3", "22.0", "22.1", "22.2", "22.3", "23.0",
"23.1", "23.2", "23.3", "24.0", "24.1", "24.2", "24.3", "25.0", "25.1",
"25.2", "25.3", "26.0", "26.1", "26.2", "27.0", "27.1", "27.2", "27.3",
"28.0", "28.1", "28.2", "28.3", "28.4", "28.5", "29.0"
]
"""
@spec latest_minor(versions_list() | key()) :: [Version.t()]
def latest_minor(versions_or_key) when is_list(versions_or_key) do
case Impl.type(versions_or_key) do
{_type, versions} ->
Impl.latest_minor(versions)
_error ->
raise ArgumentError,
message: """
latest_minor/1 expected a list or table of versions or a key, \
got: #{inspect(versions_or_key)}\
"""
end
end
def latest_minor(key) when is_atom(key), do: latest_minor(from_config(), key)
@doc """
Returns the latest minor versions from a `versions` table by the given `key`.
## Examples
iex> minor_versions = Versions.latest_minor([
...> [a: ["1.0.0/2"], b: ["1.0.0/3"]],
...> [a: ["1.1.0/3"], b: ["1.1.0/4"]]
...> ], :a)
iex> Enum.map(minor_versions, &to_string/1)
["1.0.2", "1.1.3"]
iex> Versions.latest_minor([a: "1"], :a)
** (ArgumentError) latest_minor/1 expected a table of versions, got: [a: "1"]
"""
@spec latest_minor(versions_table(), key()) :: [Version.t()]
def latest_minor(versions, key) when is_list(versions) and is_atom(key) do
case Impl.type(versions) do
{:table, versions} ->
Impl.latest_minor(versions, key)
:error ->
raise ArgumentError,
message: "latest_minor/1 expected a table of versions, got: #{inspect(versions)}"
end
end
@doc """
Returns the latest major versions from the configured versions list.
## Examples
iex> Config.config(:versions, ["1.0.0/2", "1.1.0/4", "2.0.0/3"])
iex> Versions.latest_major() |> Enum.map(&to_string/1)
["1.1.4", "2.0.3"]
"""
@spec latest_major :: [Version.t()]
def latest_major, do: latest_major(from_config())
@doc """
Returns the latest major versions from the configured `versions` table by the
given `key` or from the given `versions` list.
## Examples
iex> major_versions = Versions.latest_major(["1.0.0/2", "1.1.0/3", "2.0.0/2"])
iex> Enum.map(major_versions, &to_string/1)
["1.1.3", "2.0.2"]
iex> Config.config(:versions, [
...> [a: ["1.0.0/2", "1.1.0/3"], b: ["2.0/5"]],
...> [a: ["2.2.0/1", "2.3.0/4"], b: ["3.0/5"]]
...> ])
iex> major_versions = Versions.latest_major(:a)
iex> Enum.map(major_versions, &to_string/1)
["1.1.3", "2.3.4"]
iex> Versions.latest_major(["foo"])
** (GitHubActions.InvalidVersionError) invalid version: "foo"
iex> Versions.latest_major([a: "1"])
** (ArgumentError) latest_major/1 expected a list or table of versions or a key, got: [a: "1"]
iex> Versions.latest_major(:elixir) |> Enum.map(&to_string/1)
["1.20.2"]
iex> Versions.latest_major(:otp) |> Enum.map(&to_string/1)
["17.5", "18.3", "19.3", "20.3", "21.3", "22.3", "23.3", "24.3", "25.3", "26.2", "27.3", "28.5", "29.0"]
"""
@spec latest_major(versions_list() | key()) :: [Version.t()]
def latest_major(versions_or_key) when is_list(versions_or_key) do
case Impl.type(versions_or_key) do
{_type, versions} ->
Impl.latest_major(versions)
:error ->
raise ArgumentError,
message: """
latest_major/1 expected a list or table of versions or a key, \
got: #{inspect(versions_or_key)}\
"""
end
end
def latest_major(key) when is_atom(key), do: latest_major(from_config(), key)
@doc """
Returns the latest major versions from a `versions` table by the given `key`.
## Examples
iex> major_versions = Versions.latest_major([
...> [a: ["1.0.0/2"], b: ["1.0.0/3"]],
...> [a: ["2.0.0/3"], b: ["2.0.0/4"]]
...> ], :a)
iex> Enum.map(major_versions, &to_string/1)
["1.0.2", "2.0.3"]
iex> Versions.latest_major([a: "1"], :a)
** (ArgumentError) latest_major/1 expected a table of versions, got: [a: "1"]
"""
@spec latest_major(versions_table(), key()) :: [Version.t()]
def latest_major(versions, key) when is_list(versions) and is_atom(key) do
case Impl.type(versions) do
{:table, versions} ->
Impl.latest_major(versions, key)
_error ->
raise ArgumentError,
message: "latest_major/1 expected a table of versions, got: #{inspect(versions)}"
end
end
@doc """
Returns all versions for `key` from a list of compatible versions.
This function raises a `GitHubActions.InvalidVersionError` for an invalid
version.
## Examples
iex> versions = [
...> [a: ["1.0.0"], b: ["1.0", "1.1", "1.2"]],
...> [a: ["2.0.0"], b: ["1.2", "2.0"]]
...> ]
iex> versions = Versions.get(versions, :b)
iex> hd versions
%GitHubActions.Version{major: 1, minor: 0}
iex> Enum.map(versions, &to_string/1)
["1.0", "1.1", "1.2", "2.0"]
iex> Versions.get([a: "1"], :a)
** (ArgumentError) get/2 expected a table of versions, got: [a: "1"]
"""
@spec get(versions_table(), key()) :: [Version.t()]
def get(versions \\ from_config(), key) when is_list(versions) do
case Impl.type(versions) do
{:table, versions} ->
Impl.get(versions, key)
_error ->
raise ArgumentError,
message: "get/2 expected a table of versions, got: #{inspect(versions)}"
end
end
@doc """
Returns the versions from the config.
"""
@spec from_config :: versions()
def from_config, do: Config.get(:versions)
@doc """
Sorts the given `versions`.
## Examples
iex> versions = ["1.1", "11.1", "1.0", "2.1", "2.0.1", "2.0.0"]
iex> versions = Versions.sort(versions)
iex> Enum.map(versions, &to_string/1)
["1.0", "1.1", "2.0.0", "2.0.1", "2.1", "11.1"]
iex> Versions.sort([a: ["1", "2"]])
** (ArgumentError) sort/2 expected a list or table of versions, got: [a: ["1", "2"]]
"""
@spec sort([Version.version()]) :: [Version.version()]
def sort(versions) do
case Impl.type(versions) do
{:list, versions} ->
Impl.sort_list(versions)
{:table, versions} ->
Impl.sort_table(versions)
:error ->
raise ArgumentError,
message: "sort/2 expected a list or table of versions, got: #{inspect(versions)}"
end
end
@doc """
Removes all duplicated versions.
## Examples
iex> versions = Versions.expand(["1.0.0/4", "1.0.2/5"])
iex> versions |> Versions.uniq() |> Enum.map(&to_string/1)
["1.0.0", "1.0.1", "1.0.2", "1.0.3", "1.0.4", "1.0.5"]
iex> Versions.uniq([:a])
** (ArgumentError) uniq/1 expected a list or table of versions, got: [:a]
"""
@spec uniq(versions()) :: versions()
def uniq(versions) do
case Impl.type(versions) do
{_type, versions} ->
versions
:error ->
raise ArgumentError,
message: "uniq/1 expected a list or table of versions, got: #{inspect(versions)}"
end
end
@doc """
Filters the list of `versions` by the given `requirement`.
## Examples
iex> versions = ["1", "1.1.0/5", "1.2.0/1", "1.3", "2.0/1"]
iex> Versions.filter(versions, "~> 1.2")
[
%Version{major: 1, minor: 2, patch: 0},
%Version{major: 1, minor: 2, patch: 1},
%Version{major: 1, minor: 3}
]
iex> Versions.filter(versions, ">= 1.3.0")
[
%Version{major: 1, minor: 3},
%Version{major: 2, minor: 0},
%Version{major: 2, minor: 1}
]
iex> Versions.filter([:b, :a], "> 1.0.0")
** (ArgumentError) filter/2 expected a list of versions, got: [:b, :a]
iex> Versions.filter(["1", "2", "3"], "> 1")
** (Version.InvalidRequirementError) invalid requirement: "> 1"
"""
@spec filter(versions_list(), String.t()) :: [Version.t()]
def filter(versions, requirement) when is_binary(requirement) do
case Impl.type(versions) do
{:list, versions} ->
Impl.filter(versions, requirement)
_error ->
raise ArgumentError,
message: "filter/2 expected a list of versions, got: #{inspect(versions)}"
end
end
@doc """
Returns true if `versions` contains the given `version`.
## Examples
iex> versions = ["1.0.0", "1.1.0", "1.1.1"]
iex> Versions.member?(versions, "1.1")
true
iex> Versions.member?(versions, "1.0.1")
false
iex> Versions.member?([a: "1"], "1.0.0")
** (ArgumentError) member?/2 expected a list of versions, got: [a: "1"]
"""
@spec member?(versions_list(), Version.version()) :: boolean
def member?(versions, version) do
case Impl.type(versions) do
{:list, versions} ->
Impl.member?(versions, version)
_error ->
raise ArgumentError,
message: "member?/2 expected a list of versions, got: #{inspect(versions)}"
end
end
@doc """
Returns true if `versions1` has an intersection with `versions2`.
## Examples
iex> Versions.intersection?(["1.0.0/5"], ["1.0.4/7"])
true
iex> Versions.intersection?(["1.0.0/5"], ["2.0.0/7"])
false
iex> Versions.intersection?(["1.0.0/5"], [:a])
** (ArgumentError) intersection?/2 expected two list of versions, got: ["1.0.0/5"], [:a]
"""
@spec intersection?(versions_list(), versions_list()) :: boolean()
def intersection?(versions1, versions2) do
with {:list, versions1} <- Impl.type(versions1),
{:list, versions2} <- Impl.type(versions2) do
Impl.intersection?(versions1, versions2)
else
:error ->
raise ArgumentError,
message: """
intersection?/2 expected two list of versions, \
got: #{inspect(versions1)}, #{inspect(versions2)}\
"""
end
end
@doc """
Returns the versions of `key` that are compatible_to with `to`.
## Examples
iex> otp = Versions.compatible_to(:otp, elixir: "1.6.6")
iex> Enum.map(otp, &to_string/1)
["19.0", "19.1", "19.2", "19.3", "20.0", "20.1", "20.2", "20.3", "21.0",
"21.1", "21.2", "21.3"]
iex> elixir = Versions.compatible_to(:elixir, otp: "20.3")
iex> Enum.map(elixir, &to_string/1)
["1.4.5", "1.5.0", "1.5.1", "1.5.2", "1.5.3", "1.6.0", "1.6.1", "1.6.2",
"1.6.3", "1.6.4", "1.6.5", "1.6.6", "1.7.0", "1.7.1", "1.7.2", "1.7.3",
"1.7.4", "1.8.0", "1.8.1", "1.8.2", "1.9.0", "1.9.1", "1.9.2", "1.9.3",
"1.9.4"]
iex> :otp |> Versions.compatible_to(elixir: "1.10.0") |> Enum.count()
8
iex> :otp |> Versions.compatible_to(elixir: "1.10.0/4") |> Enum.count()
12
iex> :otp |> Versions.compatible_to(elixir: ["1.10.0/4", "1.11.0/4"]) |> Enum.count()
16
iex> Versions.compatible_to([], :otp, elixir: "1.6.6")
** (ArgumentError) compatible_to/3 expected a table of versions as first argument, got: []
"""
@spec compatible_to(versions(), key(), [{key(), Version.version()}]) :: [Version.t()]
def compatible_to(versions \\ from_config(), key, [{to_key, to_versions}])
when is_atom(key) and is_atom(to_key) do
versions =
case Impl.type(versions) do
{:table, versions} ->
versions
_error ->
raise ArgumentError,
message: """
compatible_to/3 expected a table of versions as first argument, \
got: #{inspect(versions)}\
"""
end
to_versions =
case to_versions |> List.wrap() |> Impl.type() do
{:list, versions} ->
versions
_error ->
raise ArgumentError,
message: """
compatible_to/3 expected a list of versions for #{inspect(to_key)}, \
got: #{inspect(to_versions)}\
"""
end
Impl.compatible_to(versions, key, {to_key, to_versions})
end
@doc """
Returns `true` if the given `version1` is compatible to `version2`.
## Examples
iex> Versions.compatible?(elixir: "1.12.3", otp: "24.0")
true
iex> Versions.compatible?(elixir: "1.6.0", otp: "24.0")
false
iex> versions = [
...> [a: ["1.0.0"], b: ["1.0", "1.1", "1.2"]],
...> [a: ["2.0.0"], b: ["1.2", "2.0"]]
...> ]
iex> Versions.compatible?(versions, a: "1", b: "1.2")
true
iex> Versions.compatible?(versions, a: "2", b: "1.2")
true
iex> Versions.compatible?(versions, a: "2", b: "1")
false
iex> Versions.compatible?([], a: "1", b: "2")
** (ArgumentError) compatible?/2 expected a table of versions as first argument, got: []
"""
@spec compatible?(
versions(),
[{key(), Version.version()}]
) :: boolean()
def compatible?(versions \\ from_config(), [{key1, version1}, {key2, version2}])
when is_atom(key1) and is_atom(key2) do
versions =
case Impl.type(versions) do
{:table, versions} ->
versions
_error ->
raise ArgumentError,
message: """
compatible?/2 expected a table of versions as first argument, \
got: #{inspect(versions)}\
"""
end
version1 = Version.parse!(version1)
version2 = Version.parse!(version2)
Impl.compatible?(versions, {key1, version1}, {key2, version2})
end
@doc """
Returns the compatible versions between `versions1` and `versions2`.
## Examples
iex> versions = Versions.compatible(
...> elixir: ["1.9.4", "1.10.4", "1.11.4", "1.12.3"],
...> otp: ["21.3", "22.3", "23.3", "24.0"]
...> )
iex> for [{k1, v1}, {k2, v2}] <- versions do
...> [{k1, to_string(v1)}, {k2, to_string(v2)}]
...> end
[
[elixir: "1.9.4", otp: "21.3"],
[elixir: "1.9.4", otp: "22.3"],
[elixir: "1.10.4", otp: "21.3"],
[elixir: "1.10.4", otp: "22.3"],
[elixir: "1.10.4", otp: "23.3"],
[elixir: "1.11.4", otp: "21.3"],
[elixir: "1.11.4", otp: "22.3"],
[elixir: "1.11.4", otp: "23.3"],
[elixir: "1.11.4", otp: "24.0"],
[elixir: "1.12.3", otp: "22.3"],
[elixir: "1.12.3", otp: "23.3"],
[elixir: "1.12.3", otp: "24.0"]
]
"""
@spec compatible(keyword()) :: [keyword()]
def compatible(versions \\ from_config(), [{key1, versions1}, {key2, versions2}])
when is_atom(key1) and is_atom(key2) do
versions =
case Impl.type(versions) do
{:table, versions} ->
versions
_error ->
raise ArgumentError,
message: """
compatible/2 expected a table of versions as first argument, \
got: #{inspect(versions)}\
"""
end
versions1 =
case Impl.type(versions1) do
{:list, versions} ->
versions
_error ->
raise ArgumentError,
message: """
compatible/2 expected a list of versions for #{inspect(key1)}, \
got: #{inspect(versions1)}\
"""
end
versions2 =
case Impl.type(versions2) do
{:list, versions} ->
versions
_error ->
raise ArgumentError,
message: """
compatible/2 expected a list of versions for #{inspect(key2)}, \
got: #{inspect(versions2)}\
"""
end
Impl.compatible(versions, {key1, versions1}, {key2, versions2})
end
@doc """
Returns the incompatible versions between `versions1` and `versions2`.
## Examples
iex> versions = Versions.incompatible(
...> elixir: ["1.9.4", "1.10.4", "1.11.4", "1.12.3"],
...> otp: ["21.3", "22.3", "23.3", "24.0"]
...> )
iex> for [{k1, v1}, {k2, v2}] <- versions do
...> [{k1, to_string(v1)}, {k2, to_string(v2)}]
...> end
[
[elixir: "1.9.4", otp: "23.3"],
[elixir: "1.9.4", otp: "24.0"],
[elixir: "1.10.4", otp: "24.0"],
[elixir: "1.12.3", otp: "21.3"]
]
"""
@spec incompatible(keyword()) :: [keyword()]
def incompatible(versions \\ from_config(), [{key1, versions1}, {key2, versions2}])
when is_atom(key1) and is_atom(key2) do
versions =
case Impl.type(versions) do
{:table, versions} ->
versions
_error ->
raise ArgumentError,
message: """
incompatible/2 expected a table of versions as first argument, \
got: #{inspect(versions)}\
"""
end
versions1 =
case Impl.type(versions1) do
{:list, versions} ->
versions
_error ->
raise ArgumentError,
message: """
incompatible/2 expected a list of versions for #{inspect(key1)}, \
got: #{inspect(versions1)}\
"""
end
versions2 =
case Impl.type(versions2) do
{:list, versions} ->
versions
_error ->
raise ArgumentError,
message: """
incompatible/2 expected a list of versions for #{inspect(key2)}, \
got: #{inspect(versions2)}\
"""
end
Impl.incompatible(versions, {key1, versions1}, {key2, versions2})
end
@doc """
Returns the versions matrix for the given requirements.
## Options
- `:mode` - accepts `:include` or `:exclude`. Defaults to `:exclude`.
With `:exclude` a keyword list with all versions for the requirements and
a table with the incompatible versions is returned. The table of
incompatible versions can be found under the `:exclude` key.
With `:include` only the compatible versions are returned. The table of
compatible versions can be found under the `:include` key.
## Examples
iex> matrix = Versions.matrix(elixir: ">= 1.12.0", otp: ">= 22.0.0")
iex> Enum.map(matrix[:elixir], &to_string/1)
["1.12.3", "1.13.4", "1.14.5", "1.15.8", "1.16.3", "1.17.3", "1.18.4", "1.19.5", "1.20.2"]
iex> Enum.map(matrix[:otp], &to_string/1)
["22.3", "23.3", "24.3", "25.3", "26.2", "27.3", "28.5", "29.0"]
iex> for [{k1, v1}, {k2, v2}] <- matrix[:exclude] do
...> [{k1, to_string(v1)}, {k2, to_string(v2)}]
...> end
[
[elixir: "1.12.3", otp: "25.3"],
[elixir: "1.12.3", otp: "26.2"],
[elixir: "1.12.3", otp: "27.3"],
[elixir: "1.12.3", otp: "28.5"],
[elixir: "1.12.3", otp: "29.0"],
[elixir: "1.13.4", otp: "26.2"],
[elixir: "1.13.4", otp: "27.3"],
[elixir: "1.13.4", otp: "28.5"],
[elixir: "1.13.4", otp: "29.0"],
[elixir: "1.14.5", otp: "22.3"],
[elixir: "1.14.5", otp: "27.3"],
[elixir: "1.14.5", otp: "28.5"],
[elixir: "1.14.5", otp: "29.0"],
[elixir: "1.15.8", otp: "22.3"],
[elixir: "1.15.8", otp: "23.3"],
[elixir: "1.15.8", otp: "27.3"],
[elixir: "1.15.8", otp: "28.5"],
[elixir: "1.15.8", otp: "29.0"],
[elixir: "1.16.3", otp: "22.3"],
[elixir: "1.16.3", otp: "23.3"],
[elixir: "1.16.3", otp: "27.3"],
[elixir: "1.16.3", otp: "28.5"],
[elixir: "1.16.3", otp: "29.0"],
[elixir: "1.17.3", otp: "22.3"],
[elixir: "1.17.3", otp: "23.3"],
[elixir: "1.17.3", otp: "24.3"],
[elixir: "1.17.3", otp: "28.5"],
[elixir: "1.17.3", otp: "29.0"],
[elixir: "1.18.4", otp: "22.3"],
[elixir: "1.18.4", otp: "23.3"],
[elixir: "1.18.4", otp: "24.3"],
[elixir: "1.18.4", otp: "29.0"],
[elixir: "1.19.5", otp: "22.3"],
[elixir: "1.19.5", otp: "23.3"],
[elixir: "1.19.5", otp: "24.3"],
[elixir: "1.19.5", otp: "25.3"],
[elixir: "1.19.5", otp: "29.0"],
[elixir: "1.20.2", otp: "22.3"],
[elixir: "1.20.2", otp: "23.3"],
[elixir: "1.20.2", otp: "24.3"],
[elixir: "1.20.2", otp: "25.3"],
[elixir: "1.20.2", otp: "26.2"]
]
iex> matrix = Versions.matrix(
...> elixir: ">= 1.16.0 and <= 1.21.0",
...> otp: ">= 22.0.0",
...> mode: :include
...> )
iex> for [{k1, v1}, {k2, v2}] <- matrix[:include] do
...> [{k1, to_string(v1)}, {k2, to_string(v2)}]
...> end
[
[elixir: "1.16.3", otp: "24.3"],
[elixir: "1.16.3", otp: "25.3"],
[elixir: "1.16.3", otp: "26.2"],
[elixir: "1.17.3", otp: "25.3"],
[elixir: "1.17.3", otp: "26.2"],
[elixir: "1.17.3", otp: "27.3"],
[elixir: "1.18.4", otp: "25.3"],
[elixir: "1.18.4", otp: "26.2"],
[elixir: "1.18.4", otp: "27.3"],
[elixir: "1.18.4", otp: "28.5"],
[elixir: "1.19.5", otp: "26.2"],
[elixir: "1.19.5", otp: "27.3"],
[elixir: "1.19.5", otp: "28.5"],
[elixir: "1.20.2", otp: "27.3"],
[elixir: "1.20.2", otp: "28.5"],
[elixir: "1.20.2", otp: "29.0"]
]
iex> Versions.matrix([], elixir: ">= 1.9.0", otp: ">= 22.0.0")
** (ArgumentError) matrix/1 expected a table of versions as first argument, got: []
"""
@spec matrix(keyword(), keyword()) :: keyword()
def matrix(versions \\ from_config(), opts) do
case Impl.type(versions) do
{:table, versions} ->
Impl.matrix(versions, opts)
_error ->
raise ArgumentError,
message: """
matrix/1 expected a table of versions as first argument, \
got: #{inspect(versions)}\
"""
end
end
@doc """
Minimizes the given versions table.
Returns the minimised table where each version of the table is present at least
once in the result. This means that not all possible combinations are present
in the result.
## Examples
iex> versions = [
...> [
...> elixir: %GitHubActions.Version{major: 1, minor: 16, patch: 3},
...> otp: %GitHubActions.Version{major: 24, minor: 3}
...> ],
...> [
...> elixir: %GitHubActions.Version{major: 1, minor: 16, patch: 3},
...> otp: %GitHubActions.Version{major: 25, minor: 3}
...> ],
...> [
...> elixir: %GitHubActions.Version{major: 1, minor: 16, patch: 3},
...> otp: %GitHubActions.Version{major: 26, minor: 2}
...> ],
...> [
...> elixir: %GitHubActions.Version{major: 1, minor: 17, patch: 3},
...> otp: %GitHubActions.Version{major: 25, minor: 3}
...> ],
...> [
...> elixir: %GitHubActions.Version{major: 1, minor: 17, patch: 3},
...> otp: %GitHubActions.Version{major: 26, minor: 2}
...> ],
...> [
...> elixir: %GitHubActions.Version{major: 1, minor: 17, patch: 3},
...> otp: %GitHubActions.Version{major: 27, minor: 2}
...> ],
...> [
...> elixir: %GitHubActions.Version{major: 1, minor: 18, patch: 0},
...> otp: %GitHubActions.Version{major: 25, minor: 3}
...> ],
...> [
...> elixir: %GitHubActions.Version{major: 1, minor: 18, patch: 0},
...> otp: %GitHubActions.Version{major: 26, minor: 2}
...> ],
...> [
...> elixir: %GitHubActions.Version{major: 1, minor: 18, patch: 0},
...> otp: %GitHubActions.Version{major: 27, minor: 2}
...> ]
...> ]
iex> Versions.minimize(versions)
[
[
elixir: %GitHubActions.Version{major: 1, minor: 18, patch: 0},
otp: %GitHubActions.Version{major: 27, minor: 2}
],
[
elixir: %GitHubActions.Version{major: 1, minor: 18, patch: 0},
otp: %GitHubActions.Version{major: 26, minor: 2}
],
[
elixir: %GitHubActions.Version{major: 1, minor: 18, patch: 0},
otp: %GitHubActions.Version{major: 25, minor: 3}
],
[
elixir: %GitHubActions.Version{major: 1, minor: 17, patch: 3},
otp: %GitHubActions.Version{major: 25, minor: 3}
],
[
elixir: %GitHubActions.Version{major: 1, minor: 16, patch: 3},
otp: %GitHubActions.Version{major: 24, minor: 3}
]
]
"""
@spec minimize(list(keyword())) :: list(keyword())
def minimize(versions) do
versions
|> Enum.group_by(
fn versions -> Enum.take(versions, 1) end,
fn versions -> Enum.drop(versions, 1) end
)
|> Enum.to_list()
|> Enum.sort(fn {[{_key1, version1}], _other1}, {[{_key2, version2}], _other2} ->
Version.compare(version1, version2) == :gt
end)
|> minimize([], [])
end
defp minimize([], _seen, acc), do: Enum.reverse(acc)
defp minimize([{version, versions} | rest], seen, acc) do
{unseen, seen} =
Enum.flat_map_reduce(versions, seen, fn otp, seen ->
if otp in seen do
{[], seen}
else
{[otp], [otp | seen]}
end
end)
unseen = if unseen == [], do: Enum.take(versions, 1), else: unseen
acc = Enum.map(unseen, fn otp -> version ++ otp end) ++ acc
minimize(rest, seen, acc)
end
@doc """
Expands the given `versions`.
## Examples
iex> versions = Versions.expand(["1.0/2"])
iex> Enum.map(versions, &to_string/1)
["1.0", "1.1", "1.2"]
iex> versions = Versions.expand([
...> [a: ["1.0/2"], b: ["1.0.0/2"]],
...> [a: ["1.1.0/1"], b: ["2.0.0/2"]]
...> ])
iex> versions |> Enum.at(1) |> Keyword.get(:a) |> Enum.map(&to_string/1)
["1.1.0", "1.1.1"]
iex> versions |> Enum.at(1) |> Keyword.get(:b) |> Enum.map(&to_string/1)
["2.0.0", "2.0.1", "2.0.2"]
iex> Versions.expand([:a])
** (ArgumentError) expand/1 expected a list of versions, or table of versions got: [:a]
"""
@spec expand(versions()) :: versions()
def expand(versions) do
case Impl.type(versions) do
{_type, versions} ->
versions
:error ->
raise ArgumentError,
message: """
expand/1 expected a list of versions, or table of versions \
got: #{inspect(versions)}\
"""
end
end
defmodule Impl do
@moduledoc false
def type(versions) when is_list(versions) do
case {table?(versions), list?(versions)} do
{true, false} ->
{:table, expand(versions) |> uniq() |> sort_table()}
{false, true} ->
{:list, expand(versions) |> Enum.uniq() |> sort_list()}
_else ->
:error
end
end
def type(_versions), do: :error
defp table?(versions) do
Enum.all?(versions, &Keyword.keyword?/1) &&
versions |> Enum.map(&Keyword.keys/1) |> Enum.uniq() |> Enum.count() == 1
end
defp list?(versions) do
Enum.all?(versions, fn version ->
cond do
is_binary(version) -> true
is_struct(version, Version) -> true
true -> false
end
end)
end
def sort_list(versions) do
Enum.sort(versions, fn a, b -> Version.compare(a, b) == :lt end)
end
def sort_table(versions) do
versions
|> Enum.map(&sort_table_row/1)
|> sort_table_rows()
end
defp sort_table_row(row) do
Enum.map(row, fn {key, list} -> {key, sort_list(list)} end)
end
defp sort_table_rows([]), do: []
defp sort_table_rows([[]]), do: [[]]
defp sort_table_rows([[{key, _version} | _versions] | _rows] = rows) do
Enum.sort(rows, fn a, b ->
case {Enum.at(a[key], 0), Enum.at(b[key], 0)} do
{nil, nil} -> false
{_, nil} -> false
{nil, _} -> true
{x, y} -> Version.compare(x, y) == :lt
end
end)
end
defp uniq(versions) do
Enum.map(versions, fn row -> Enum.uniq(row) end)
end
defp expand(versions) when is_list(versions) do
Enum.flat_map(versions, fn version -> do_expand(version) end)
end
defp do_expand(version) when is_binary(version) or is_struct(version) do
version |> Version.parse!() |> List.wrap()
end
defp do_expand(versions) when is_list(versions) do
[Enum.map(versions, fn {key, versions} -> {key, expand(versions)} end)]
end
def latest(versions), do: List.last(versions)
def latest(versions, key), do: versions |> get(key) |> List.last()
def latest_minor(versions), do: do_latest(versions, :minor)
def latest_minor(versions, key), do: versions |> get(key) |> do_latest(:minor)
def latest_major(versions), do: do_latest(versions, :major)
def latest_major(versions, key), do: versions |> get(key) |> do_latest(:major)
defp do_latest(versions, precision) do
versions
|> Enum.reduce([], fn
version, [] ->
[Version.parse!(version)]
version, [current | rest] = acc ->
case Version.compare(version, current, precision) do
:eq -> [Version.parse!(version) | rest]
:gt -> [Version.parse!(version) | acc]
end
end)
|> Enum.reverse()
end
def member?(versions, version) do
Enum.any?(versions, fn item -> Version.compare(item, version) == :eq end)
end
def intersection?(versions1, versions2) do
Enum.any?(versions1, fn version -> member?(versions2, version) end)
end
def filter(versions, requirement) do
Enum.filter(versions, fn version -> Version.match?(version, requirement) end)
end
def compatible_to(versions, key, {to_key, to_versions}) do
Enum.flat_map(versions, fn row ->
case row |> Keyword.get(to_key, []) |> intersection?(to_versions) do
true -> Keyword.get(row, key, [])
false -> []
end
end)
end
def compatible?(versions, {key1, version1}, {key2, version2}) do
versions
|> compatible_to(key2, {key1, List.wrap(version1)})
|> member?(version2)
end
def incompatible(versions, {key1, versions1}, {key2, versions2}) do
compatible(versions, {key1, versions1}, {key2, versions2}, false)
end
def compatible(versions, {key1, versions1}, {key2, versions2}) do
compatible(versions, {key1, versions1}, {key2, versions2}, true)
end
def compatible(versions, {key1, versions1}, {key2, versions2}, compatible?) do
for version1 <- versions1,
version2 <- versions2,
compatible?(versions, {key1, version1}, {key2, version2}) == compatible? do
[{key1, version1}, {key2, version2}]
end
end
def get(versions, key) do
versions
|> Enum.flat_map(fn lists -> Keyword.get(lists, key, []) end)
|> Enum.uniq()
|> sort_list()
end
def matrix(versions, opts) do
elixir =
versions
|> get(:elixir)
|> filter(Keyword.fetch!(opts, :elixir))
|> latest_minor()
otp =
versions
|> compatible_to(:otp, {:elixir, elixir})
|> filter(Keyword.fetch!(opts, :otp))
|> latest_major()
case Keyword.get(opts, :mode, :exclude) do
:exclude ->
exclude = incompatible(versions, {:elixir, elixir}, {:otp, otp})
result = [elixir: elixir, otp: otp]
if Enum.empty?(exclude), do: result, else: result ++ [{:exclude, exclude}]
:include ->
include = compatible(versions, {:elixir, elixir}, {:otp, otp})
[include: include]
end
end
end
end