Packages

Duskmoon fork of npm_ex — resolve, fetch, and manage npm dependencies with Mix tasks.

Current section

Files

Jump to
duskmoon_npm lib npm_semver.ex
Raw

lib/npm_semver.ex

defmodule NPMSemver do
@moduledoc """
npm-compatible semantic versioning.
Parses and matches version ranges using npm's semver syntax:
`^1.2.3`, `~1.2.3`, `>=1.0.0 <2.0.0`, `1.x`, `1.0.0 - 2.0.0`, `||` unions.
## Examples
iex> NPMSemver.matches?("1.2.3", "^1.0.0")
true
iex> NPMSemver.matches?("2.0.0", "^1.0.0")
false
iex> NPMSemver.matches?("1.5.0", ">=1.2.3 <2.0.0")
true
iex> NPMSemver.matches?("1.2.4", "~1.2.3")
true
iex> NPMSemver.matches?("2.1.3", "2.x.x")
true
Test fixtures ported from [node-semver](https://github.com/npm/node-semver).
"""
alias NPMSemver.{Range, Version}
@doc """
Check if a version satisfies a range.
iex> NPMSemver.matches?("1.8.1", "^1.2.3")
true
iex> NPMSemver.matches?("0.1.2", "^0.1")
true
"""
@spec matches?(String.t(), String.t(), keyword()) :: boolean()
def matches?(version_string, range_string, opts \\ []) do
with {:ok, version} <- Version.parse(version_string, opts),
{:ok, range} <- Range.parse(range_string, opts) do
Range.satisfies?(range, version, opts)
else
_ -> false
end
end
@doc """
Parse a version string into a `NPMSemver.Version` struct.
iex> {:ok, v} = NPMSemver.parse_version("1.2.3-beta.1")
iex> {v.major, v.minor, v.patch, v.pre}
{1, 2, 3, ["beta", 1]}
"""
@spec parse_version(String.t(), keyword()) :: {:ok, Version.t()} | :error
def parse_version(string, opts \\ []) do
Version.parse(string, opts)
end
@doc """
Parse a range string into a `NPMSemver.Range` struct.
iex> {:ok, _range} = NPMSemver.parse_range("^1.2.3")
"""
@spec parse_range(String.t(), keyword()) :: {:ok, Range.t()} | :error
def parse_range(string, opts \\ []) do
Range.parse(string, opts)
end
@doc """
Find the highest version in a list that satisfies the range.
iex> NPMSemver.max_satisfying(["1.0.0", "1.5.0", "2.0.0"], "^1.0.0")
"1.5.0"
iex> NPMSemver.max_satisfying(["0.1.0", "0.2.0"], "^1.0.0")
nil
"""
@spec max_satisfying([String.t()], String.t(), keyword()) :: String.t() | nil
def max_satisfying(versions, range_string, opts \\ []) do
case Range.parse(range_string, opts) do
{:ok, range} -> do_max_satisfying(versions, range, opts)
_ -> nil
end
end
defp do_max_satisfying(versions, range, opts) do
versions
|> Enum.filter(&version_satisfies?(&1, range, opts))
|> Enum.sort(fn a, b ->
{:ok, va} = Version.parse(a, opts)
{:ok, vb} = Version.parse(b, opts)
Version.compare(va, vb) == :gt
end)
|> List.first()
end
defp version_satisfies?(v, range, opts) do
case Version.parse(v, opts) do
{:ok, ver} -> Range.satisfies?(range, ver, opts)
_ -> false
end
end
@doc """
Convert an npm range string to a `HexSolver` constraint.
Returns an opaque constraint value that can be used with `HexSolver.run/4`
and returned from `HexSolver.Registry` callbacks.
iex> {:ok, _constraint} = NPMSemver.to_hex_constraint("^1.2.3")
"""
@spec to_hex_constraint(String.t(), keyword()) :: {:ok, HexSolver.constraint()} | :error
def to_hex_constraint(range_string, opts \\ []) do
case to_elixir_requirement(range_string, opts) do
{:ok, req_string} -> HexSolver.parse_constraint(req_string)
:error -> :error
end
end
@doc """
Convert an npm range string to a `hex_solver`-compatible Elixir requirement string.
iex> NPMSemver.to_elixir_requirement("^1.2.3")
{:ok, ">= 1.2.3 and < 2.0.0-0"}
iex> NPMSemver.to_elixir_requirement("~1.2.3")
{:ok, ">= 1.2.3 and < 1.3.0-0"}
iex> NPMSemver.to_elixir_requirement(">=1.0.0 <2.0.0 || >=3.0.0")
{:ok, ">= 1.0.0 and < 2.0.0 or >= 3.0.0"}
"""
@spec to_elixir_requirement(String.t(), keyword()) :: {:ok, String.t()} | :error
def to_elixir_requirement(range_string, opts \\ []) do
case Range.parse(range_string, opts) do
{:ok, range} -> {:ok, Range.to_elixir_string(range)}
:error -> :error
end
end
end