Files
n8n-openai-adapter/lib/elixir/lib/version.ex
T

548 lines
16 KiB
Elixir

defmodule Version do
@moduledoc %S"""
Functions for parsing and matching versions against requirements.
A version is a string in a specific format or a `Version.Schema`
generated after parsing via `Version.parse/1`.
`Version` parsing and requirements follow
[SemVer 2.0 schema](http://semver.org/) and you will get
the most of Mix's version system by following it. In order
to support integration with projects that may
follow different versioning schemas, Elixir won't choke
on unknown versions, however you won't be able to use
Mix requirements with such unformatted versions.
## Versions
In a nutshell, a version is given by three numbers:
MAJOR.MINOR.PATCH
Pre-releases are supported by appending `-[0-9A-Za-z-\.]`:
"1.0.0-alpha.3"
Build information can be added by appending `+[0-9A-Za-z-\.]`:
"1.0.0-alpha.3+20130417140000"
## Requirements
Requirements allow you to specify which versions of a given
dependency you are willing to work against. It supports common
operators like `>=`, `<=`, `>`, `==` and friends that
work as one would expect:
# Only version 2.0.0
"== 2.0.0"
# Anything later than 2.0.0
"> 2.0.0"
Requirements also support `and` and `or` for complex conditions:
# 2.0.0 and later until 2.1.0
">= 2.0.0 and < 2.1.0"
Since the example above is such a common requirement, it can
be expressed as:
"~> 2.0.0"
"""
@type t :: String.t | Version.Schema.t
@type requirement :: String.t | Version.Requirement.t
@type matchable :: { major :: String.t | non_neg_integer,
minor :: non_neg_integer | nil,
patch :: non_neg_integer | nil,
pre :: [String.t] }
import Kernel, except: [match?: 2]
defrecord Schema, major: 0, minor: 0, patch: 0, pre: nil, build: nil, source: nil
defrecord Requirement, source: nil, matchspec: nil
defexception InvalidRequirement, [:message] do
def exception(opts) do
message =
if is_binary opts[:reason] do
{ first, rest } = String.next_grapheme(opts[:reason])
String.downcase(first) <> rest
else
"invalid version specification"
end
InvalidRequirement[message: message]
end
end
@doc """
Check if the given version matches the specification.
Returns `true` if `version` satisfies `requirement`, `false` otherwise.
Raises a `Version.InvalidRequirement` exception if `requirement` is not parseable.
## Examples
iex> Version.match?("2.0", ">1.0")
true
iex> Version.match?("2.0", "==1.0")
false
"""
@spec match?(t, requirement) :: boolean
def match?(version, requirement) when is_binary(requirement) do
case Version.Parser.parse_requirement(requirement) do
{ :ok, req } ->
match?(version, req)
{ :error, reason } ->
raise InvalidRequirement, reason: reason
end
end
def match?(version, requirement) when is_binary(version) do
match?(parse(version), requirement)
end
def match?(Schema[] = version, Requirement[matchspec: spec]) do
case :ets.test_ms(to_matchable(version), spec) do
{ :ok, result } ->
result != false
{ :error, reason } ->
raise InvalidRequirement, reason: reason
end
end
@doc """
Check if a version string is compatible with [semver](http://semver.org/).
## Examples
iex> Version.valid?("2.0")
true
iex> Version.valid?("invalid")
false
"""
@spec valid?(String.t | Schema.t) :: boolean
def valid?(string) when is_binary(string) do
Version.Parser.valid_version?(string)
end
def valid?(Version.Schema[major: nil]), do: false
def valid?(Version.Schema[]), do: true
@doc """
Parse a version string into a `Version.Schema`.
## Examples
iex> Version.parse("2.0.1-alpha1")
#Version.Schema<2.0.1-alpha1>
"""
@spec parse(String.t) :: Schema.t
def parse(string) when is_binary(string) do
case Version.Parser.parse_version(string) do
{ :ok, matchable } -> from_matchable(matchable).source(string).build(get_build(string))
{ :error, _ } -> Version.Schema[source: string]
end
end
defp get_build(string) do
case Regex.run(%r/\+([^\s]+)$/, string) do
nil ->
nil
[_, build] ->
build
end
end
@doc """
Convert a version to a `Version.matchable`
## Examples
iex> Version.to_matchable("2.0.1-alpha.1")
{2, 0, 1, ["alpha", 1]}
"""
@spec to_matchable(String.t | Schema.t) :: Version.matchable
def to_matchable(Schema[major: nil, source: source]) do
{ source, nil, nil, [] }
end
def to_matchable(Version.Schema[major: major, minor: minor, patch: patch, pre: nil]) do
{ major, minor, patch, [] }
end
def to_matchable(Version.Schema[major: major, minor: minor, patch: patch, pre: pre]) do
{ major, minor, patch, Version.Parser.parse_pre(pre) }
end
def to_matchable(string) do
to_matchable(parse(string))
end
@doc """
Convert a matchable to a `Version.Schema`.
## Examples
iex> Version.from_matchable({2, 0, 1, ["alpha", 1]})
#Version.Schema<2.0.1-alpha.1>
"""
@spec from_matchable(Version.matchable) :: Schema.t
def from_matchable({ source, nil, nil, nil }) when is_binary(source) do
Version.Schema[source: source]
end
def from_matchable({ major, minor, patch, pre }) do
source = "#{major}"
if minor do
source = "#{source}.#{minor}"
if patch do
source = "#{source}.#{patch}"
case pre do
[] ->
pre = nil
list ->
pre = Enum.join(list, ".")
source = "#{source}-#{pre}"
end
end
end
Version.Schema[major: major, minor: minor, patch: patch, pre: pre, source: source]
end
@spec compare(String.t | Schema.t, String.t | Schema.t) :: :gt | :eq | :lt
def compare(vsn1, vsn2) do
{ major1, minor1, patch1, pre1 } = to_matchable(vsn1)
{ major2, minor2, patch2, pre2 } = to_matchable(vsn2)
cond do
{ major1, minor1, patch1 } > { major2, minor2, patch2 } -> :gt
{ major1, minor1, patch1 } < { major2, minor2, patch2 } -> :lt
pre1 == [] and pre2 != [] -> :gt
pre1 != [] and pre2 == [] -> :lt
pre1 > pre2 -> :gt
pre1 < pre2 -> :lt
true -> :eq
end
end
defmodule Parser.DSL do
@moduledoc false
defmacro deflexer(match, do: body) when is_binary(match) do
quote do
def lexer(unquote(match) <> rest, acc) do
lexer(rest, [unquote(body) | acc])
end
end
end
defmacro deflexer(acc, do: body) do
quote do
def lexer("", unquote(acc)) do
unquote(body)
end
end
end
defmacro deflexer(char, acc, do: body) do
quote do
def lexer(<< unquote(char) :: utf8, rest :: binary >>, unquote(acc)) do
unquote(char) = << unquote(char) :: utf8 >>
lexer(rest, unquote(body))
end
end
end
end
defmodule Parser do
@moduledoc false
import Parser.DSL
deflexer ">=", do: :'>='
deflexer "<=", do: :'<='
deflexer "~>", do: :'~>'
deflexer ">", do: :'>'
deflexer "<", do: :'<'
deflexer "==", do: :'=='
deflexer "!=", do: :'!='
deflexer "!", do: :'!='
deflexer " or ", do: :'||'
deflexer " and ", do: :'&&'
deflexer " ", do: :' '
deflexer x, [] do
[x, :'==']
end
deflexer x, [h | acc] do
cond do
is_binary h ->
[h <> x | acc]
h in [:'||', :'&&'] ->
[x, :'==', h | acc]
true ->
[x, h | acc]
end
end
deflexer acc do
Enum.filter(Enum.reverse(acc), &(&1 != :' '))
end
@version_regex %r/^(\d+)(?:\.(\d+)(?:\.(\d+))?)?(?:\-([^\s]+))?(?:\+[^\d]+)?$/
@spec parse_requirement(String.t) :: { :ok, Version.Requirement.t } | { :error, binary | atom }
def parse_requirement(source) do
lexed = lexer(source, [])
if valid_requirement?(lexed) do
spec = to_matchspec(lexed)
case :ets.test_ms({}, spec) do
{ :ok, _ } ->
{ :ok, Requirement[source: source, matchspec: spec] }
{ :error, errors } ->
{ :error, Enum.map(errors, fn { :error, reason } ->
to_string(reason)
end) }
end
else
{ :error, :invalid_requirement }
end
end
defp nillify(""), do: nil
defp nillify(o), do: o
@spec parse_version(String.t) :: { :ok, Version.matchable } | { :error, :invalid_version }
def parse_version(string) when is_binary(string) do
if valid_version?(string) do
destructure [_, major, minor, patch, pre], Regex.run(@version_regex, string)
major = binary_to_integer(major)
minor = binary_to_integer(minor |> nillify || "0")
patch = binary_to_integer(patch |> nillify || "0")
pre = pre && parse_pre(pre) || []
{ :ok, { major, minor, patch, pre } }
else
{ :error, :invalid_version }
end
end
@doc false
def parse_pre(pre) do
String.split(pre, ".") |> Enum.map fn piece ->
if piece =~ %r/^(0|[1-9][0-9]*)$/ do
binary_to_integer(piece)
else
piece
end
end
end
@spec valid_requirement?(list) :: boolean
def valid_requirement?([]) do
false
end
def valid_requirement?([a | next]) do
valid_requirement?(a, next)
end
# it must finish with a version
defp valid_requirement?(a, []) when is_binary(a) do
true
end
# version version
defp valid_requirement?(a, [b | _]) when is_binary(a) and is_binary(b) do
false
end
# or <op> | and <op>
defp valid_requirement?(a, [b | next]) when is_atom(a) and is_atom(b) and a in [:'||', :'&&'] do
valid_requirement?(b, next)
end
# <version> or | <version> and
defp valid_requirement?(a, [b | next]) when is_binary(a) and is_atom(b) and b in [:'||', :'&&'] do
valid_requirement?(b, next)
end
# or <version> | and <version>
defp valid_requirement?(a, [b | next]) when is_atom(a) and is_binary(b) and a in [:'||', :'&&'] do
valid_requirement?(b, next)
end
# <op> <version>; also checks operators work on valid versions
defp valid_requirement?(a, [b | next]) when is_atom(a) and is_binary(b) do
if valid_version?(b) do
valid_requirement?(b, next)
else
if a in [:'==', :'!='] and Regex.match? %r/^\w/, b do
valid_requirement?(b, next)
else
false
end
end
end
defp valid_requirement?(_, _) do
false
end
@spec valid_version?(String.t) :: boolean
def valid_version?(string) do
Regex.match? %r/^\d+(\.\d+(\.\d+)?)?(\-[^\s]+)?(?:\+[^\s]+)?$/, string
end
defp approximate(version) do
Version.from_matchable(case Regex.run(@version_regex, version) do
[_, major] ->
{ binary_to_integer(major) + 1, 0, 0, [0] }
[_, major, _] ->
{ binary_to_integer(major) + 1, 0, 0, [0] }
[_, major, minor, _] ->
{ binary_to_integer(major), binary_to_integer(minor) + 1, 0, [0] }
[_, major, minor, _, _] ->
{ binary_to_integer(major), binary_to_integer(minor) + 1, 0, [0] }
end)
end
defp to_matchspec(lexed) do
first = to_condition(lexed)
rest = Enum.drop(lexed, 2)
[{{ :'$1', :'$2', :'$3', :'$4' }, [to_condition(first, rest)], [:'$_'] }]
end
defp to_condition([:'==', version | _]) do
version = Version.to_matchable(version)
{ :'==', :'$_', { :const, version } }
end
defp to_condition([:'!=', version | _]) do
version = Version.to_matchable(version)
{ :'/=', :'$_', { :const, version } }
end
defp to_condition([:'~>', version | _]) do
from = Version.parse(version)
to = approximate(version)
{ :andalso, to_condition([:'>=', to_string(from)]),
to_condition([:'<', to_string(to)]) }
end
defp to_condition([:'>', version | _]) do
{ major, minor, patch, pre } = Version.to_matchable(version)
{ :andalso, { :not, { :is_binary, :'$1' } },
{ :orelse, { :'>', {{ :'$1', :'$2', :'$3' }},
{ :const, { major, minor, patch } } },
{ :andalso, { :'==', {{ :'$1', :'$2', :'$3' }},
{ :const, { major, minor, patch } } },
{ :orelse, { :andalso, { :'==', { :length, :'$4' }, 0 },
{ :'/=', length(pre), 0 } },
{ :andalso, { :'/=', length(pre), 0 },
{ :orelse, { :'>', { :length, :'$4' }, length(pre) },
{ :andalso, { :'==', { :length, :'$4' }, length(pre) },
{ :'>', :'$4', { :const, pre } } } } } } } } }
end
defp to_condition([:'>=', version | _]) do
matchable = Version.to_matchable(version)
{ :orelse, { :andalso, { :not, { :is_binary, :'$1' } },
{ :'==', :'$_', { :const, matchable } } },
to_condition([:'>', version]) }
end
defp to_condition([:'<', version | _]) do
{ major, minor, patch, pre } = Version.to_matchable(version)
{ :andalso, { :not, { :is_binary, :'$1' } },
{ :orelse, { :'<', {{ :'$1', :'$2', :'$3' }},
{ :const, { major, minor, patch } } },
{ :andalso, { :'==', {{ :'$1', :'$2', :'$3' }},
{ :const, { major, minor, patch } } },
{ :orelse, { :andalso, { :'/=', { :length, :'$4' }, 0 },
{ :'==', length(pre), 0 } },
{ :andalso, { :'/=', { :length, :'$4' }, 0 },
{ :orelse, { :'<', { :length, :'$4' }, length(pre) },
{ :andalso, { :'==', { :length, :'$4' }, length(pre) },
{ :'<', :'$4', { :const, pre } } } } } } } } }
end
defp to_condition([:'<=', version | _]) do
matchable = Version.to_matchable(version)
{ :orelse, { :andalso, { :not, { :is_binary, :'$1' } },
{ :'==', :'$_', { :const, matchable } } },
to_condition([:'<', version]) }
end
defp to_condition(current, []) do
current
end
defp to_condition(current, [:'&&', operator, version | rest]) do
to_condition({ :andalso, current, to_condition([operator, version]) }, rest)
end
defp to_condition(current, [:'||', operator, version | rest]) do
to_condition({ :orelse, current, to_condition([operator, version]) }, rest)
end
end
end
defimpl String.Chars, for: Version.Schema do
def to_string(Version.Schema[source: source]) do
source
end
end
defimpl Inspect, for: Version.Schema do
def inspect(self, _opts) do
"#Version.Schema<" <> to_string(self) <> ">"
end
end
defimpl String.Chars, for: Version.Requirement do
def to_string({ _, source, _ }) do
source
end
end
defimpl Inspect, for: Version.Requirement do
def inspect({ _, source, _ }, _opts) do
"#Version.Requirement<" <> source <> ">"
end
end