Current section
Files
Jump to
Current section
Files
lib/utils.ex
defmodule Issuer.Utils do
@version_file Path.join("config", "VERSION")
@interfaces_file Path.join("config", "INTERFACES")
@mix_file "mix.exs"
@re ~r|version:\s*"([-.\w]+)"|
@proper_mix_version ~S|version: File.read!(Path.join("config", "VERSION"))|
@doc """
Returns “sprouts” for VCS tags. Those might be the the next version tag.
iex> ["0.0.1"] |> Issuer.Utils.sprouts
["0.0.2", "0.1.0", "0.1.0-dev", "0.1.0-rc.1", "1.0.0", "1.0.0-dev", "1.0.0-rc.1"]
iex> ["0.1.1-rc2"] |> Issuer.Utils.sprouts
["0.1.1", "0.1.1-rc3"]
iex> tags = ["v1.4.0-rc.1", "v1.4.0-dev",
...> "v1.3.3", "v1.3.2", "v1.3.1", "v1.3.0", "v1.3.0-rc.1", "v1.3.0-rc.0", "v1.2.6",
...> "v1.2.5", "v1.2.4", "v1.2.3", "v1.2.2", "v1.2.1", "v1.2.0", "v1.2.0-rc.1",
...> "v1.2.0-rc.0", "v1.1.1", "v1.1.0", "v1.1.0-rc.0", "v1.0.5", "v1.0.4", "v1.0.3",
...> "v1.0.2", "v1.0.1", "v1.0.0", "v1.0.0-rc2", "v1.0.0-rc1", "v0.15.1", "v0.15.0",
...> "v0.14.3", "v0.14.2", "v0.14.1", "v0.14.0", "v0.13.3", "v0.13.2", "v0.13.1",
...> "v0.13.0", "v0.12.5", "v0.12.4", "v0.12.3", "v0.12.2", "v0.12.1", "v0.12.0",
...> "v0.11.2", "v0.11.1", "v0.11.0", "v0.10.3", "v0.10.2", "v0.10.1", "v0.10.0",
...> "v0.9.3"]
...> tags |> Issuer.Utils.sprouts
["v1.4.0", "v1.4.0-rc.2", "v1.3.4", "v2.0.0", "v0.15.2", "v0.16.0", "v1.0.0"]
"""
def sprouts(tags) when is_list(tags) do
[head | tail] = tags |> leaves |> Enum.map(&sprouts/1)
[head | tail |> Enum.map(fn e -> e |> Enum.reject(&suffixed?/1) end)]
|> List.flatten
|> Enum.uniq
# |> Enum.sort(&(&1 > &2)) # FIXME appropriate sorting?
end
def sprouts(tags) when is_binary(tags) do
tags |> version |> smart_sprouts
end
##############################################################################
@doc """
Returns “leaves” for VCS tags. Those might be the the next version tag.
For instance, current elixir git repo has these leaves:
- `"v0.15.1"`
- `"v1.3.3"`
- `"v1.4.0-rc.1"`
The function will filter out all the existing tags to these:
iex> Issuer.Utils.leaves([]) |> Enum.count
1
iex> tags = ["v1.4.0-rc.1", "v1.4.0-dev",
...> "v1.3.3", "v1.3.2", "v1.3.1", "v1.3.0", "v1.3.0-rc.1", "v1.3.0-rc.0", "v1.2.6",
...> "v1.2.5", "v1.2.4", "v1.2.3", "v1.2.2", "v1.2.1", "v1.2.0", "v1.2.0-rc.1",
...> "v1.2.0-rc.0", "v1.1.1", "v1.1.0", "v1.1.0-rc.0", "v1.0.5", "v1.0.4", "v1.0.3",
...> "v1.0.2", "v1.0.1", "v1.0.0", "v1.0.0-rc2", "v1.0.0-rc1", "v0.15.1", "v0.15.0",
...> "v0.14.3", "v0.14.2", "v0.14.1", "v0.14.0", "v0.13.3", "v0.13.2", "v0.13.1",
...> "v0.13.0", "v0.12.5", "v0.12.4", "v0.12.3", "v0.12.2", "v0.12.1", "v0.12.0",
...> "v0.11.2", "v0.11.1", "v0.11.0", "v0.10.3", "v0.10.2", "v0.10.1", "v0.10.0",
...> "v0.9.3"]
...> tags |> Issuer.Utils.leaves
["v1.4.0-rc.1", "v1.3.3", "v0.15.1"]
"""
def leaves([]) do
{_, version} = version?()
[version]
end
def leaves(tags) do
tops = tags |> Enum.map(&version/1)
tops
|> Enum.reject(fn [the_prefix, the_h, the_m, the_s, the_suffix] ->
tops
|> Enum.any?(fn [a_prefix, a_h, a_m, a_s, a_suffix] ->
[the_prefix, the_h, the_m, the_s, the_suffix] != [a_prefix, a_h, a_m, a_s, a_suffix] &&
the_h == a_h &&
(
(a_m > the_m || a_m == the_m && a_s > the_s) && a_suffix == "" ||
a_m == the_m && a_s == the_s && a_suffix > the_suffix
)
end)
end)
|> Enum.sort(&compare_versions/2)
|> Enum.map(&version/1)
end
##############################################################################
def interface_changes?(neu \\ Issuer.Utils.functions!) do
case File.read(@interfaces_file) do
{:error, :enoent} -> {:not_found, {:+, neu}, {:-, []}}
{:ok, legacy} ->
{legacy, _} = legacy |> Code.eval_string
{:found, {:+, subtract_keywords(neu, legacy)}, {:-, subtract_keywords(legacy, neu)}}
end
end
def interface_changes! do
neu = Issuer.Utils.functions!
result = neu |> interface_changes?
File.write(@interfaces_file, Macro.to_string(neu))
result
end
defp subtract_keywords(l1, l2) do
l1 |> Keyword.drop(l2 |> Keyword.keys)
end
@doc """
## Examples
iex> with {name, _} <- Issuer.Utils.functions! |> List.last, do: name
:"Mix.Tasks.Issuer.Version#run/1"
"""
def functions! do
unless loaded?(), do: Mix.raise("Project structure is not loaded, aborting. Please revise manually.")
modules!()
|> Enum.reduce([], fn mod, acc -> acc |> Keyword.merge(docs(mod)) end)
# |> Enum.sort_by(fn {k, _} -> k end)
end
@doc """
## Examples
iex> Issuer.Utils.modules! |> List.last
Mix.Tasks.Issuer.Version
"""
def modules!, do: with {:ok, list} <- modules(), do: list, else: []
defp project do
Mix.Project.get
|> Module.split
|> List.first
|> String.downcase
|> String.to_atom
end
defp loaded, do: :ets.match(:ac_tab, {{:loaded, :"$1"}, :_})
defp loaded?(project \\ [project()]), do: loaded() |> Enum.find(& &1 == project)
defp modules, do: :application.get_key(project(), :modules)
defp docs(module) do
module_name = "#{module}"
|> String.trim_leading("Elixir.")
module
|> Code.get_docs(:docs) # {{:commit!, 1}, 92, :def, [{:argv, [], nil}], nil}
|> Enum.filter(fn {{name, _}, _, _, _, _} ->
not (name |> Atom.to_string |> String.starts_with?("__"))
end)
|> Enum.reduce([], fn {{name, arity}, line, _kw, _args, doc}, acc ->
acc |> put_in(["#{module_name}##{name}/#{arity}" |> String.to_atom], {line, doc})
end)
end
##############################################################################
def version!(v) when is_binary(v) do
case version_valid?(v) do
false -> {:invalid, v}
version ->
uv = version |> unprefix_version
File.write(@version_file, uv)
# ⇓ FIXME HACK to hot update version
cfg = Mix.ProjectStack.pop |> update_in([:config, :version], fn _ -> uv end)
Mix.ProjectStack.push cfg[:name], cfg[:config], cfg[:file]
# ⇑ FIXME HACK to update version
{:ok, version}
end
end
def version?(v \\ "0.0.1") do
# version: "0.1.0" ⇒ version: File.read!("VERSION")
case version_in_mix?(true) do
{:yes, version} -> # found a version as is in mix :(
File.write!(@version_file, version)
File.write!(@mix_file, Regex.replace(@re, File.read!(@mix_file), @proper_mix_version))
{:mix!, version}
{:no, :ok} ->
case File.read(@version_file) do
{:error, :enoent} ->
File.write!(@version_file, v)
{:version!, v}
{:ok, version} -> {:ok, version |> String.trim}
end
{:no, _} ->
Mix.raise("Found many version strings in `mix`, aborting. Please revise manually.")
end
end
##############################################################################
def prefix_version(v) when is_binary(v) do
prefix_version(version(v))
end
def prefix_version(["", h, m, s, suffix]) do
version(["v", h, m, s, suffix])
end
def prefix_version([prefix, h, m, s, suffix]) do
version([prefix, h, m, s, suffix])
end
def unprefix_version(v) when is_binary(v) do
unprefix_version(version(v))
end
def unprefix_version([_prefix | tail]) do
version(["" | tail])
end
def version_valid?(v) do
try do
version(v)
rescue
MatchError -> false
end
end
##############################################################################
def version_in_mix?(_ \\ false)
def version_in_mix?(false) do
case version_in_mix?(true) do
{_, :ok} -> false
response -> response
end
end
def version_in_mix?(true) do
mix = File.read!(@mix_file)
case Regex.scan(@re, mix, capture: :all_but_first) |> List.flatten do
[version] when is_binary(version) -> {:yes, version}
[] -> {:no, :ok}
multi -> {:no, multi}
end
end
##############################################################################
# returns `["v", 0, 1, 3, "-dev"]`
defp version(v) when is_binary(v) do
[prefix, h, m, s, suffix] = Regex.run(~r/\A(\D*)(\d+)\.(\d+)\.(\d+)(.*)?\z/, v, capture: :all_but_first)
[prefix, String.to_integer(h), String.to_integer(m), String.to_integer(s), suffix]
end
# returns `"v0.1.3-dev"`
defp version([prefix, h, m, s, suffix]) do
"#{prefix}#{h}.#{m}.#{s}#{suffix}"
end
# returns `"v0.1.3-dev"`
defp compare_versions([_, the_h, the_m, the_s, the_sx], [_, a_h, a_m, a_s, a_sx]) do
cond do
the_h > a_h -> true
the_h < a_h -> false
the_m > a_m -> true
the_m < a_m -> false
the_s > a_s -> true
the_s < a_s -> false
the_sx == "" -> true
a_sx == "" -> false
true -> the_sx > a_sx
end
end
defp suffixed?(v) when is_binary(v) do
suffixed?(version(v))
end
defp suffixed?([_prefix, _h, _m, _s, suffix]) do
suffix != ""
end
##############################################################################
# iex> ["0.0.1"] |> Issuer.Utils.sprouts
# ["0.0.2", "0.1.0", "0.1.0-dev", "0.1.0-rc.1", "1.0.0", "1.0.0-dev", "1.0.0-rc.1"]
defp smart_sprouts([prefix, h, m, s, ""]) do
[
version([prefix, h, m, s + 1, ""]),
version([prefix, h, m + 1, 0, ""]),
version([prefix, h, m + 1, 0, "-dev"]),
version([prefix, h, m + 1, 0, "-rc.1"]),
version([prefix, h + 1, 0, 0, ""]),
version([prefix, h + 1, 0, 0, "-dev"]),
version([prefix, h + 1, 0, 0, "-rc.1"]),
]
end
defp smart_sprouts([prefix, h, m, s, suffix]) do
[version([prefix, h, m, s, ""]), version([prefix, h, m, s, inc_suffix(suffix)])]
end
defp inc_suffix(suffix) when is_binary(suffix) do
Regex.replace(
~r|(rc.?)(\d+)|,
String.replace(suffix, "dev", "rc.0"),
fn _, f, s -> "#{f}#{String.to_integer(s) + 1}" end
)
end
end