defmodule BeamMeta.Release.Elixir do
@moduledoc """
Functions for retrieving information related to Elixir releases.
This module does not deal with releases prior to version `1.0.0`.
"""
use BackPort
@typedoc """
A map that information related to a release in GitHub.
This information is originally provided by `BeamLangsMetaData.elixir_releases/0` and is transformed.
"""
@type release_data ::
BeamMeta.nonempty_keyword(
version_key :: atom(),
%{
assets:
nonempty_list(%{
content_type: String.t(),
created_at: DateTime.t(),
id: non_neg_integer(),
json_url: String.t(),
name: String.t(),
size: non_neg_integer(),
state: String.t(),
url: String.t()
}),
created_at: DateTime.t(),
id: non_neg_integer(),
json_url: String.t(),
prerelease?: boolean(),
published_at: DateTime.t(),
tarball_url: String.t(),
url: String.t(),
version: Version.t(),
zipball_url: String.t()
}
)
@typedoc """
A release version.
It could it be a `t:Version.t/0` or a string representation of this one,
for example: `#Version<1.0.0>` or `"1.13.0"`.
"""
@type version :: Version.t() | String.t()
@typedoc """
A release version requirement.
It could it be a `t:Version.Requirement.t/0` or a string representation of this one,
for example: `#Version.Requirement<"~> 1.13">` or `"~> 1.13"`.
"""
@type version_requirement :: Version.Requirement.t() | String.t()
# # Note that if `term` is a string, it does not check whether it is a valid version requirement.
# defguardp is_version_requirement(term)
# when is_struct(term, Version.Requirement) or is_binary(term)
# This is the minimum requirement. We do not retrieve anything prior 1.0.0
@minimal_elixir_version_requirement Version.parse_requirement!(">= 1.0.0")
filter_asset = fn asset when is_map(asset) ->
{:ok, created_at, 0} = DateTime.from_iso8601(asset.created_at)
%{
content_type: asset.content_type,
created_at: created_at,
id: asset.id,
json_url: asset.url,
name: asset.name,
size: asset.size,
state: asset.state,
url: asset.download_url
}
end
flatten_releases = fn releases ->
for {_major_minor, %{latest: _latest, releases: releases_kw}} <- releases,
{_version_atom, release_map} <- releases_kw do
release_map
end
end
release_data =
BeamLangsMetaData.elixir_releases()
|> flatten_releases.()
|> Enum.reduce([], fn elem, acc ->
with tag_name when is_binary(tag_name) <- elem[:tag_name],
version_string <- String.trim_leading(tag_name, "v"),
version <- Version.parse!(version_string),
true <- Version.match?(version, @minimal_elixir_version_requirement),
{:ok, published_at, 0} <- DateTime.from_iso8601(elem.published_at),
{:ok, created_at, 0} <- DateTime.from_iso8601(elem.created_at) do
Keyword.put(acc, String.to_atom(version_string), %{
assets: Enum.map(elem.assets, &filter_asset.(&1)),
created_at: created_at,
id: elem.id,
json_url: elem.url,
prerelease?: elem.prerelease,
published_at: published_at,
tarball_url: elem.tarball_url,
url: elem.release_url,
version: version,
zipball_url: elem.zipball_url
})
else
_ -> acc
end
end)
@release_data release_data
# TODO: Replace with Map.reject/2 when Elixir v1.13 is exclusively supported
release_data_prerelease =
Enum.reduce(release_data, [], fn
{k, map}, acc ->
if map[:prerelease?] == true do
Keyword.put(acc, k, map)
else
acc
end
end)
@release_data_prerelease release_data_prerelease
# TODO: Replace with Map.reject/2 when Elixir v1.13 is exclusively supported
release_data_release =
Enum.reduce(release_data, [], fn
{k, map}, acc ->
if map[:prerelease?] == false do
Keyword.put(acc, k, map)
else
acc
end
end)
@release_data_release release_data_release
@versions Enum.map(release_data, fn {_k, map} -> Map.get(map, :version) end)
|> Enum.sort_by(& &1, {:asc, Version})
@prerelease_versions release_data
|> Enum.filter(fn {_k, map} -> map[:prerelease?] end)
|> Enum.map(fn {_k, map} -> map[:version] end)
|> Enum.sort_by(& &1, {:asc, Version})
@release_versions release_data
|> Enum.reject(fn {_k, map} -> map[:prerelease?] end)
|> Enum.map(fn {_k, map} -> map[:version] end)
|> Enum.sort_by(& &1, {:asc, Version})
@latest_version Enum.max(@release_versions, Version)
@doc """
Returns the latest stable Elixir version.
"""
@spec latest_version() :: Version.t()
def latest_version(), do: @latest_version
@doc """
Returns a map with all the prereleases since Elixir v1.0.0.
## Examples
> BeamMeta.Release.Elixir.prereleases()
%{
"1.10.0-rc.0" => %{
assets: [
%{
content_type: "application/zip",
created_at: ~U[2020-01-07 15:08:43Z],
id: 17188069,
json_url: "https://api.github.com/repos/elixir-lang/elixir/releases/assets/17188069",
name: "Docs.zip",
size: 2119178,
state: "uploaded",
url: "https://github.com/elixir-lang/elixir/releases/download/v1.10.0-rc.0/Docs.zip"
},
%{
content_type: "application/zip",
created_at: ~U[2020-01-07 15:08:47Z],
id: 17188070,
json_url: "https://api.github.com/repos/elixir-lang/elixir/releases/assets/17188070",
name: "Precompiled.zip",
size: 5666120,
state: "uploaded",
url: "https://github.com/elixir-lang/elixir/releases/download/v1.10.0-rc.0/Precompiled.zip"
}
],
created_at: ~U[2020-01-07 14:10:04Z],
id: 22650172,
json_url: "https://api.github.com/repos/elixir-lang/elixir/releases/22650172",
prerelease?: true,
published_at: ~U[2020-01-07 15:09:40Z],
tarball_url: "https://api.github.com/repos/elixir-lang/elixir/tarball/v1.10.0-rc.0",
url: "https://github.com/elixir-lang/elixir/releases/tag/v1.10.0-rc.0",
version: #Version<1.10.0-rc.0>,
zipball_url: "https://api.github.com/repos/elixir-lang/elixir/zipball/v1.10.0-rc.0"
},
...
}
"""
@spec prereleases() :: release_data()
def prereleases(), do: @release_data_prerelease
@doc """
Returns a map with only final releases since Elixir v1.0.0.
## Examples
> BeamMeta.Release.Elixir.releases()
%{
"1.12.1" => %{
assets: [
%{
content_type: "application/zip",
created_at: ~U[2021-05-28 15:51:16Z],
id: 37714034,
json_url: "https://api.github.com/repos/elixir-lang/elixir/releases/assets/37714034",
name: "Docs.zip",
size: 5502033,
state: "uploaded",
url: "https://github.com/elixir-lang/elixir/releases/download/v1.12.1/Docs.zip"
},
%{
content_type: "application/zip",
created_at: ~U[2021-05-28 15:51:27Z],
id: 37714052,
json_url: "https://api.github.com/repos/elixir-lang/elixir/releases/assets/37714052",
name: "Precompiled.zip",
size: 6049663,
state: "uploaded",
url: "https://github.com/elixir-lang/elixir/releases/download/v1.12.1/Precompiled.zip"
}
],
created_at: ~U[2021-05-28 15:34:14Z],
id: 43775368,
json_url: "https://api.github.com/repos/elixir-lang/elixir/releases/43775368",
prerelease?: false,
published_at: ~U[2021-05-28 15:51:54Z],
tarball_url: "https://api.github.com/repos/elixir-lang/elixir/tarball/v1.12.1",
url: "https://github.com/elixir-lang/elixir/releases/tag/v1.12.1",
version: #Version<1.12.1>,
zipball_url: "https://api.github.com/repos/elixir-lang/elixir/zipball/v1.12.1"
},
...
}
"""
@spec final_releases() :: release_data()
def final_releases(), do: @release_data_release
@doc """
Returns a map which contains all the information that we find relevant from releases data.
Includes data from final releases and preseleases starting from Elixir version 1.0.0.
## Examples
> BeamMeta.Release.Elixir.release_data()
%{
"1.12.1" => %{
assets: [
%{
content_type: "application/zip",
created_at: ~U[2021-05-28 15:51:16Z],
id: 37714034,
json_url: "https://api.github.com/repos/elixir-lang/elixir/releases/assets/37714034",
name: "Docs.zip",
size: 5502033,
state: "uploaded",
url: "https://github.com/elixir-lang/elixir/releases/download/v1.12.1/Docs.zip"
},
%{
content_type: "application/zip",
created_at: ~U[2021-05-28 15:51:27Z],
id: 37714052,
json_url: "https://api.github.com/repos/elixir-lang/elixir/releases/assets/37714052",
name: "Precompiled.zip",
size: 6049663,
state: "uploaded",
url: "https://github.com/elixir-lang/elixir/releases/download/v1.12.1/Precompiled.zip"
}
],
created_at: ~U[2021-05-28 15:34:14Z],
id: 43775368,
json_url: "https://api.github.com/repos/elixir-lang/elixir/releases/43775368",
prerelease?: false,
published_at: ~U[2021-05-28 15:51:54Z],
tarball_url: "https://api.github.com/repos/elixir-lang/elixir/tarball/v1.12.1",
url: "https://github.com/elixir-lang/elixir/releases/tag/v1.12.1",
version: #Version<1.12.1>,
zipball_url: "https://api.github.com/repos/elixir-lang/elixir/zipball/v1.12.1"
},
...
}
"""
@spec release_data() :: release_data()
def release_data(), do: @release_data
@doc """
Returns a filtered map from `releases_data/0` that matches the `elixir_version_requirement` and `options`.
`options` are options supported by `Version.match?/3`. Currently the only supported key
is `:allow_pre` which accepts `true` or `false` values. Defaults to `true`.
See `releases_data/0` for more information.
## Examples
> BeamMeta.Release.Elixir.release_data("~> 1.12", allow_pre: false)
%{
"1.12.1" => %{
assets: [
%{
content_type: "application/zip",
created_at: ~U[2021-05-28 15:51:16Z],
id: 37714034,
json_url: "https://api.github.com/repos/elixir-lang/elixir/releases/assets/37714034",
name: "Docs.zip",
size: 5502033,
state: "uploaded",
url: "https://github.com/elixir-lang/elixir/releases/download/v1.12.1/Docs.zip"
},
%{
content_type: "application/zip",
created_at: ~U[2021-05-28 15:51:27Z],
id: 37714052,
json_url: "https://api.github.com/repos/elixir-lang/elixir/releases/assets/37714052",
name: "Precompiled.zip",
size: 6049663,
state: "uploaded",
url: "https://github.com/elixir-lang/elixir/releases/download/v1.12.1/Precompiled.zip"
}
],
created_at: ~U[2021-05-28 15:34:14Z],
id: 43775368,
json_url: "https://api.github.com/repos/elixir-lang/elixir/releases/43775368",
prerelease?: false,
published_at: ~U[2021-05-28 15:51:54Z],
tarball_url: "https://api.github.com/repos/elixir-lang/elixir/tarball/v1.12.1",
url: "https://github.com/elixir-lang/elixir/releases/tag/v1.12.1",
version: #Version<1.12.1>,
zipball_url: "https://api.github.com/repos/elixir-lang/elixir/zipball/v1.12.1"
},
...
}
"""
@spec release_data(version_requirement, keyword) :: release_data()
def release_data(elixir_version_requirement, options \\ [])
def release_data(elixir_version_requirement, options)
when is_binary(elixir_version_requirement) do
elixir_version_requirement
|> Version.parse_requirement!()
|> do_release_data(options)
end
def release_data(elixir_version_requirement, options)
when is_struct(elixir_version_requirement, Version.Requirement) do
do_release_data(elixir_version_requirement, options)
end
defp do_release_data(elixir_version_requirement, options) do
# TODO: replace with Map.filter/2 when we require Elixir 1.13 exclusively
Enum.reduce(release_data(), [], fn
{k, map}, acc ->
if Version.match?(map.version, elixir_version_requirement, options) do
Keyword.put(acc, k, map)
else
acc
end
end)
end
@doc """
Returns a list with all the Elixir versions since v1.0.0.
The list contains the versions in the `t:Version.t/0` format, sorted ascendenly.
## Examples:
> BeamMeta.Release.Elixir.versions()
[#Version<1.0.0>, #Version<1.0.1>, #Version<1.0.2>, #Version<1.0.3>, #Version<1.0.4>,
#Version<1.0.5>, #Version<1.1.0>, #Version<1.1.1>, #Version<1.2.0>, #Version<1.2.1>,
#Version<1.2.2>, #Version<1.2.3>, #Version<1.2.4>, #Version<1.2.5>, #Version<1.2.6>, ...]
"""
@spec versions() :: [Version.t()]
def versions(), do: @versions
@doc """
Returns a list Elixir versions since v1.0.0, according to `kind`.
The list contains the versions in the `t:Version.t/0` format, sorted ascendenly.
`kind` can be:
- `:release`
- `:prerelease`
## Examples
> BeamMeta.Release.Elixir.versions(:release)
[#Version<1.0.0>, #Version<1.0.1>, #Version<1.0.2>, #Version<1.0.3>, #Version<1.0.4>,
#Version<1.0.5>, #Version<1.1.0>, #Version<1.1.1>, #Version<1.2.0>, #Version<1.2.1>,
#Version<1.2.2>, #Version<1.2.3>, #Version<1.2.4>, #Version<1.2.5>, #Version<1.2.6>, ...]
> BeamMeta.Release.Elixir.versions(:prerelease)
[#Version<1.3.0-rc.0>, #Version<1.3.0-rc.1>, #Version<1.4.0-rc.0>, #Version<1.4.0-rc.1>,
#Version<1.5.0-rc.0>, #Version<1.5.0-rc.1>, #Version<1.5.0-rc.2>, #Version<1.6.0-rc.0>,
#Version<1.6.0-rc.1>, #Version<1.7.0-rc.0>, #Version<1.7.0-rc.1>, #Version<1.8.0-rc.0>, ...]
"""
@spec versions(BeamMeta.release_kind()) :: [Version.t()]
def versions(kind)
def versions(:release), do: @release_versions
def versions(:prerelease), do: @prerelease_versions
end