Aleksei-bird 7c9c708c92 Первый фикс
Пачки некоторых позиций увеличены
2024-03-01 20:54:33 +03:00

385 lines
12 KiB
Lua

---
-- Version comparison library for Lua.
--
-- Comparison is simple and straightforward, with basic support for SemVer.
--
-- @usage
-- local version = require("version")
--
-- -- create a version and perform some comparisons
-- local v = version("3.1.0")
-- assert( v == version("3.1")) -- missing elements default to zero, and hence are equal
-- assert( v > version("3.0"))
--
-- -- create a version range, and check whether a version is within that range
-- local r = version.range("2.75", "3.50.3")
-- assert(r:matches(v))
--
-- -- create a set of multiple ranges, adding elements in a chained fashion
-- local compatible = version.set("1.1","1.2"):allowed("2.1", "2.5"):disallowed("2.3")
--
-- assert(compatible:matches("1.1.3"))
-- assert(compatible:matches("1.1.3"))
-- assert(compatible:matches("2.4"))
-- assert(not compatible:matches("2.0"))
-- assert(not compatible:matches("2.3"))
--
-- -- print a formatted set
-- print(compatible) --> "1.1 to 1.2 and 2.1 to 2.5, but not 2.3"
--
-- -- create an upwards compatibility check, allowing all versions 1.x
-- local c = version.set("1.0","2.0"):disallowed("2.0")
-- assert(c:matches("1.4"))
-- assert(not c:matches("2.0"))
--
-- -- default parsing
-- print(version("5.2")) -- "5.2"
-- print(version("Lua 5.2 for me")) -- "5.2"
-- print(version("5..2")) -- nil, "Not a valid version element: '5..2'"
--
-- -- strict parsing
-- print(version.strict("5.2")) -- "5.2"
-- print(version.strict("Lua 5.2 for me")) -- nil, "Not a valid version element: 'Lua 5.2 for me'"
-- print(version.strict("5..2")) -- nil, "Not a valid version element: '5..2'"
--
-- @copyright Kong Inc.
-- @author Thijs Schreijer
-- @license Apache 2.0
local table_insert = table.insert
local table_concat = table.concat
local math_max = math.max
-- Utility split function
local function split(str, pat)
local t = {}
local fpat = "(.-)" .. pat
local last_end = 1
local s, e, cap = str:find(fpat, 1)
while s do
if s ~= 1 or cap ~= "" then
table_insert(t,cap)
end
last_end = e + 1
s, e, cap = str:find(fpat, last_end)
end
if last_end <= #str then
cap = str:sub(last_end)
table_insert(t, cap)
end
return t
end
-- foreward declaration of constructor
local _new, _range, _set
-- Metatables for version, range and set
local mt_version
mt_version = {
__index = {
--- Matches a provider-version on a consumer-version based on the
-- semantic versioning specification.
-- The implementation does not support pre-release and/or build metadata,
-- only the major, minor, and patch levels are compared.
-- @function ver:semver
-- @param v Version (string or `version` object) as served by the provider
-- @return `true` or `false` whether the version matches, or `nil+err`
-- @usage local consumer = "1.2" -- consumer requested version
-- local provider = "1.5.2" -- provider served version
--
-- local compatible = version(consumer):semver(provider)
semver = function(self, v)
-- this function will be called once (in the meta table), it will set
-- the actual function on the version table itself
if self[1] == 0 then
-- major 0 is only compatible when equal
self.semver = function(self2, v2)
if getmetatable(v2) ~= mt_version then
local parsed, err = _new(v2, self2.strict)
if not parsed then return nil, err end
v2 = parsed
end
return self2 == v2
end
elseif self[4] then
-- more than 3 elements, cannot compare
self.semver = function()
return nil, "Version has too many elements (semver max 3)"
end
else
local semver_set = _set(self, self[1] + 1, self.strict):disallowed(self[1] + 1)
self.semver = function(_, v2)
return semver_set:matches(v2)
end
end
return self:semver(v)
end
},
__eq = function(a,b)
local l = math_max(#a, #b)
for i = 1, l do
if (a[i] or 0) ~= (b[i] or 0) then
return false
end
end
return true
end,
__lt = function(a,b)
if getmetatable(a) ~= mt_version or getmetatable(b) ~= mt_version then
local t = getmetatable(a) ~= mt_version and type(a) or type(b)
error("cannot compare a 'version' to a '" .. t .. "'", 2)
end
local l = math_max(#a, #b)
for i = 1, l do
if (a[i] or 0) < (b[i] or 0) then
return true
end
if (a[i] or 0) > (b[i] or 0) then
return false
end
end
return false
end,
__tostring = function(self)
return table_concat(self, ".")
end,
}
local mt_range = {
__index = {
--- Matches a version on a range.
-- @function range:matches
-- @param v Version (string or `version` object) to match
-- @return `true` or `false` whether the version matches the range, or `nil+err`
matches = function(self, v)
if getmetatable(v) ~= mt_version then
local parsed, err = _new(v, self.strict)
if not parsed then return nil, err end
v = parsed
end
return (v >= self.from) and (v <= self.to)
end,
},
__tostring = function(self)
local f, t = tostring(self.from), tostring(self.to)
if f == t then
return f
else
return f .. " to " .. t
end
end,
}
local mt_set = {
__index = {
--- Adds an ALLOWED range to the set.
-- @function set:allowed
-- @param v1 Version or range, if version, the FROM version in either string or `version` object format
-- @param v2 Version (optional), TO version in either string or `version` object format
-- @return The `set` object, to easy chain multiple allowed/disallowed ranges, or `nil+err`
allowed = function(self, v1, v2)
if getmetatable(v1) == mt_range then
assert (v2 == nil, "First parameter was a range, second must be nil.")
table_insert(self.ok, v1)
else
local r, err = _range(v1, v2, self.strict)
if not r then return nil, err end
table_insert(self.ok, r)
end
return self
end,
--- Adds a DISALLOWED range to the set.
-- @function set:disallowed
-- @param v1 Version or range, if version, the FROM version in either string or `version` object format
-- @param v2 Version (optional), TO version in either string or `version` object format
-- @return The `set` object, to easy chain multiple allowed/disallowed ranges, or `nil+err`
disallowed = function(self,v1, v2)
if getmetatable(v1) == mt_range then
assert (v2 == nil, "First parameter was a range, second must be nil.")
table_insert(self.nok, v1)
else
local r, err = _range(v1, v2, self.strict)
if not r then return nil, err end
table_insert(self.nok, r)
end
return self
end,
--- Matches a version against the set of allowed and disallowed versions.
--
-- NOTE: `disallowed` has a higher precedence, so a version that matches the `allowed` set,
-- but also the `disallowed` set, will return `false`.
-- @function set:matches
-- @param v1 Version to match (either string or `version` object).
-- @return `true` or `false` whether the version matches the set, or `nil+err`
matches = function(self, v)
if getmetatable(v) ~= mt_version then
local parsed, err = _new(v, self.strict)
if not parsed then return nil, err end
v = parsed
end
local success
for _, range in pairs(self.ok) do
if range:matches(v) then
success = true
break
end
end
if not success then
return false
end
for _, range in pairs(self.nok) do
if range:matches(v) then
return false
end
end
return true
end,
},
__tostring = function(self)
local ok, nok
if #self.ok == 1 then
ok = tostring(self.ok[1])
elseif #self.ok > 1 then
ok = tostring(self.ok[1])
for i = 2, #self.ok - 1 do
ok = ok .. ", " ..tostring(self.ok[i])
end
ok = ok .. ", and " .. tostring(self.ok[#self.ok])
end
if #self.nok == 1 then
nok = tostring(self.nok[1])
elseif #self.nok > 1 then
nok = tostring(self.nok[1])
for i = 2, #self.nok - 1 do
nok = nok .. ", " ..tostring(self.nok[i])
end
nok = nok .. ", and " .. tostring(self.nok[#self.nok])
end
if ok and nok then
return ok .. ", but not " .. nok
else
return ok
end
end,
}
_new = function(v, strict)
v = tostring(v)
if strict then
-- edge case: do not allow trailing dot
if v:sub(-1,-1) == "." then
return nil, "Not a valid version element: '"..tostring(v).."'"
end
else
local m = v:match("(%d[%d%.]*)")
if not m then
return nil, "Not a valid version element: '"..tostring(v).."'"
end
v = m
end
local t = split(v, "%.")
for i, s in ipairs(t) do
local n = tonumber(s)
if not n then
return nil, "Not a valid version element: '"..tostring(v).."'"
end
t[i] = n
end
t.strict = strict
return setmetatable(t, mt_version)
end
_range = function(v1,v2, strict)
local err
assert (v1 or v2, "At least one parameter is required")
v1 = v1 or "0"
v2 = v2 or v1
if getmetatable(v1) ~= mt_version then
v1, err = _new(v1, strict)
if not v1 then return nil, err end
end
if getmetatable(v2) ~= mt_version then
v2, err = _new(v2, strict)
if not v2 then return nil, err end
end
if v1 > v2 then
return nil, "FROM version must be less than or equal to the TO version"
end
return setmetatable({
from = v1,
to = v2,
strict = strict,
}, mt_range)
end
_set = function(v1, v2, strict)
return setmetatable({
ok = {},
nok = {},
strict = strict,
}, mt_set):allowed(v1, v2)
end
local make_module = function(strict)
return setmetatable({
--- Creates a new version object from a string.
-- The returned table will have
-- comparison operators, eg. LT, EQ, GT. For all comparisons, any missing numbers
-- will be assumed to be "0" on the least significant side of the version string.
--
-- Calling on the module table is a shortcut to `new`.
-- @param v String formatted as numbers separated by dots (no limit on number of elements).
-- @return `version` object, or `nil+err`
-- @usage local v = version.new("0.1")
-- -- is identical to
-- local v = version("0.1")
--
-- print(v) --> "0.1"
-- print(v[1]) --> 0
-- print(v[2]) --> 1
new = function(v) return _new(v, strict) end,
--- Creates a version range. A `range` object represents a range of versions.
-- @param v1 The FROM version of the range (string or `version` object). If `nil`, assumed to be 0.
-- @param v2 (optional) The TO version of the range (string or `version` object). Defaults to `v1`.
-- @return range object with `from` and `to` fields and `set:matches` method, or `nil+err`.
-- @usage local r = version.range("0.1"," 2.4")
--
-- print(v.from) --> "0.1"
-- print(v.to[1]) --> 2
-- print(v.to[2]) --> 4
range = function(v1,v2) return _range(v1, v2, strict) end,
--- Creates a version set.
-- A `set` is an object that contains a number of allowed and disallowed version `range` objects.
-- @param v1 initial version/range to allow, see `set:allowed` for parameter descriptions
-- @param v2 initial version/range to allow, see `set:allowed` for parameter descriptions
-- @return a `set` object, with `ok` and `nok` lists and a `set:matches` method, or `nil+err`
set = function(v1, v2) return _set(v1, v2, strict) end,
}, {
__call = function(self, ...)
return self.new(...)
end
})
end
local _M = make_module(false)
--- Similar module, but with stricter parsing rules.
-- `version.strict` is identical to the `version` module itself, but it requires
-- exact version strings, where as the regular parser will simply grab the
-- first sequence of numbers and dots from the string.
-- @field strict same module, but for stricter parsing.
_M.strict = make_module(true)
return _M