Compare commits

...

No commits in common. "v0.1.0" and "main" have entirely different histories.
v0.1.0 ... main

16 changed files with 763 additions and 267 deletions

29
.github/actions/setup-pesde/action.yml vendored Normal file
View file

@ -0,0 +1,29 @@
name: Install pesde
description: Installs pesde CLI and authenticates with the registry
inputs:
pesde-token:
description: "Token for publishing to the pesde registry"
required: false
runs:
using: composite
steps:
- name: Download pesde
shell: bash
run: |
latest_release=$(curl -s https://api.github.com/repos/pesde-pkg/pesde/releases | jq '[.[] | select(.prerelease == true or .prerelease == false)][0]')
download_url=$(echo "$latest_release" | jq -r '.assets[] | select(.name | endswith("linux-x86_64.tar.gz")) | .browser_download_url')
curl -L -o /tmp/pesde.tar.gz "$download_url"
tar -xzvf /tmp/pesde.tar.gz
chmod +x pesde
./pesde self-install
rm ./pesde
echo "$HOME/.pesde/bin" >> $GITHUB_PATH
- name: Authenticate into pesde registry
if: inputs.pesde-token != ''
shell: bash
run: pesde auth login --token "${{ inputs.pesde-token }}"

82
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,82 @@
name: CI
on:
push:
pull_request:
workflow_dispatch:
defaults:
run:
shell: bash
jobs:
fmt:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install tooling
uses: CompeyDev/setup-rokit@v0.1.2
with:
cache: true
- name: Install pesde
uses: ./.github/actions/setup-pesde
with:
pesde-token: ${{ secrets.PESDE_TOKEN }}
- name: Install dependencies
run: pesde install
- name: Check formatting
run: lune run fmt -- --check
typecheck:
needs: ["fmt"]
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install tooling
uses: CompeyDev/setup-rokit@v0.1.2
with:
cache: true
- name: Install pesde
uses: ./.github/actions/setup-pesde
with:
pesde-token: ${{ secrets.PESDE_TOKEN }}
- name: Install dependencies
run: pesde install
- name: Setup lune typedefs
run: lune setup
- name: Typecheck
run: lune run typecheck
test:
needs: ["typecheck"]
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install tooling
uses: CompeyDev/setup-rokit@v0.1.2
with:
cache: true
- name: Install pesde
uses: ./.github/actions/setup-pesde
with:
pesde-token: ${{ secrets.PESDE_TOKEN }}
- name: Install dependencies
run: pesde install
- name: Run tests
run: lune run test

24
.github/workflows/publish.yml vendored Normal file
View file

@ -0,0 +1,24 @@
name: Publish
on:
push:
tags:
- "v*"
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install pesde
uses: ./.github/actions/setup-pesde
with:
pesde-token: ${{ secrets.PESDE_TOKEN }}
- name: Install dependencies
run: pesde install
- name: Publish
run: pesde publish -y

110
.lune/exec.luau Normal file
View file

@ -0,0 +1,110 @@
--> lib: Builder pattern class to spawn child processes
local stdio = require("@lune/stdio")
local process = require("@lune/process")
local Option = require("../luau_packages/option")
type Option<T> = Option.Option<T>
local CommandBuilder = {}
export type CommandBuilder = typeof(setmetatable({} :: CommandBuilderFields, { __index = CommandBuilder }))
type CommandBuilderFields = {
program: string,
args: { string },
stdioStrategy: Option<IoStrategyMapping>,
}
export type StdioStrategy = "pipe" | "forward" | "none"
export type IoStrategyMapping = {
stdout: Option<StdioStrategy>,
stderr: Option<StdioStrategy>,
}
-- FIXME: remove unknown usage
local DEFAULT_STDIO_STRATEGY: IoStrategyMapping = {
stdout = Option.Some("pipe" :: StdioStrategy) :: Option<unknown>,
stderr = Option.Some("pipe" :: StdioStrategy) :: Option<unknown>,
}
function CommandBuilder.new(program: string)
return setmetatable(
{
program = program,
args = {},
stdioStrategy = Option.None :: Option<IoStrategyMapping>,
} :: CommandBuilderFields,
{
__index = CommandBuilder,
}
)
end
function CommandBuilder.withArg(self: CommandBuilder, arg: string): CommandBuilder
table.insert(self.args, arg)
return self
end
function CommandBuilder.withArgs(self: CommandBuilder, args: { string }): CommandBuilder
for _, arg in args do
self:withArg(arg)
end
return self
end
function CommandBuilder.withStdioStrategy(
self: CommandBuilder,
strategy: StdioStrategy | IoStrategyMapping
): CommandBuilder
-- FIXME: remove unknown usage
self.stdioStrategy = Option.Some(if typeof(strategy) == "string"
then {
stdout = Option.Some(strategy) :: Option<unknown>,
stderr = Option.Some(strategy) :: Option<unknown>,
}
else strategy) :: Option<IoStrategyMapping>
return self
end
local function intoSpawnOptionsStdioKind(strategy: StdioStrategy): process.SpawnOptionsStdioKind
if strategy == "pipe" then
return "default"
end
if strategy == "forward" then
return "forward"
end
if strategy == "none" then
return "none"
end
error(`Non-strategy provided: {strategy}`)
end
function CommandBuilder.exec(self: CommandBuilder): process.SpawnResult
print("$", self.program, table.concat(self.args, " "))
local child = process.spawn(self.program, self.args, {
shell = true,
stdio = self
.stdioStrategy
-- FIXME: remove unknown usage
:orOpt(Option.Some(DEFAULT_STDIO_STRATEGY) :: Option<unknown>)
:map(function(mappings: IoStrategyMapping)
local translatedMappings: process.SpawnOptionsStdio = {}
for field, value in mappings do
translatedMappings[field] = intoSpawnOptionsStdioKind((value :: Option<StdioStrategy>):unwrap())
end
return translatedMappings
end)
:unwrap(),
})
if not child.ok then
print(`\n{stdio.color("red")}[luau-lsp]{stdio.color("reset")} Exited with code`, child.code)
end
return child
end
return CommandBuilder

7
.lune/fmt.luau Normal file
View file

@ -0,0 +1,7 @@
--> Run stylua to check for formatting errors
local process = require("@lune/process")
local CommandBuilder = require("./exec")
process.exit(CommandBuilder.new("stylua"):withArg("."):withArgs(process.args):withStdioStrategy("forward"):exec().code)

View file

@ -4,21 +4,54 @@ local fs = require("@lune/fs")
local process = require("@lune/process")
local frktest = require("@pkg/frktest")
local reporter = require("../tests/_reporter")
-- HACK: Cast require to allow for dynamic paths in strict mode
-- A more proper solution would be to use luau.load instead, but
-- frktest requires its global state to be modified by test suites
local require = require :: (path: string) -> () -> ()
local require = require :: (
path: string
) -> (
test: typeof(setmetatable(
{} :: {
case: (name: string, fn: () -> nil) -> (),
suite: (name: string, fn: () -> ()) -> (),
},
{ __index = frktest.test }
))
) -> ()
if process.args[1] ~= nil then
require("../tests/" .. process.args[1])()
else
for _, test in fs.readDir("tests") do
require("../tests/" .. test)()
end
local allowed_tests = process.args
for _, test in fs.readDir("tests") do
-- If we are given any arguments, we only run those tests, otherwise,
-- we run all the tests
-- So, to include only a certain set of test files, you can provide either
-- the full path to the test file (with or without the extension) or the test
-- file name
local withoutExt = string.sub(test, 1, -6)
local is_allowed = #process.args == 0
or table.find(allowed_tests, `tests/{test}`)
or table.find(allowed_tests, withoutExt)
or table.find(allowed_tests, `tests/{withoutExt}`)
local constructors = {
case = frktest.test.case,
suite = frktest.test.suite,
}
if not is_allowed then
constructors.case = frktest.test.skip.case
constructors.suite = frktest.test.skip.suite
end
-- Ignore files starting with underscores, eg: _reporter.luau
if string.sub(test, 1, 1) == "_" then
continue
end
require("../tests/" .. test)(setmetatable(constructors, { __index = frktest.test }))
end
frktest._reporters.lune_console_reporter.init()
if not frktest.run() then
process.exit(1)
end
reporter.init()
process.exit(tonumber(frktest.run()))

15
.lune/typecheck.luau Normal file
View file

@ -0,0 +1,15 @@
--> Run luau-lsp analysis to check for type errors
local process = require("@lune/process")
local CommandBuilder = require("./exec")
process.exit(
CommandBuilder.new("luau-lsp")
:withArg("analyze")
:withArgs({ "--settings", ".vscode/settings.json" })
:withArgs({ "--ignore", "'**/.pesde/**'" })
:withArg(".")
:withStdioStrategy("forward")
:exec().code
)

View file

@ -28,17 +28,17 @@ Invalid versions should return a [`Result`] with the respective error. If not, t
filing an issue [here](https://github.com/0x5eal/semver-luau/issues).
```luau
local semver = require("semver")
local Semver = require("semver")
-- Parse version strings
local v1 = semver.parse("1.2.3"):unwrap() --[[
local v1 = Semver.parse("1.2.3"):unwrap() --[[
major = 1,
minor = 2,
patch = 3,
buildMetadata = Option::None,
prerelease = Option::None
]]
local v2 = semver.parse("5.12.0+build.1731248766"):unwrap() --[[
local v2 = Semver.parse("5.12.0+build.1731248766"):unwrap() --[[
major = 5,
minor = 12,
patch = 0,

View file

@ -145,7 +145,7 @@ export type SemverErrorKind = ParseError | {}
* `InvalidPrereleaseOrdinalType`: When prerelease ordinal is not a number
]=]
export type ParseError =
{ id: "MandatoryComponentMissing", components: { string } }
| { id: "MandatoryComponentMissing", components: { string } }
| {
id: "InvalidComponentType",
component: "major" | "minor" | "patch",
@ -170,6 +170,19 @@ local PRERELEASE_LEX_ORDER: { [PreleaseType]: number } = table.freeze({
rc = 3,
})
--[=[
@within Semver
@function new
Creates a new Semver instance from a [Version]
@param ver Version -- The version to create a Semver instance from
@return SemverImpl -- The new Semver instance
]=]
function Semver.new(ver: Version): SemverImpl
return setmetatable(ver, Semver)
end
--[=[
@within Semver
@function parse
@ -293,7 +306,7 @@ function Semver.parse(ver: string): SemverResult<SemverImpl>
-- b) The component has build metadata after prerelease info
-- Here, we handle both those cases
local potentialOrdinalNumber, potentialBuildMetadata = string.match(components[4], "(%d)+(.*)")
local potentialOrdinalNumber, potentialBuildMetadata = string.match(components[4], "(.*)+(.*)")
if potentialOrdinalNumber == nil then
return badPrereleaseType(components[4])
end
@ -313,6 +326,14 @@ function Semver.parse(ver: string): SemverResult<SemverImpl>
return badPrereleaseType(components[4])
end
end
elseif #components > 4 then
-- The ordinal component was bad, we should error
local badPrerelease = ""
for i = 4, #components do
badPrerelease ..= "." .. components[i]
end
return badPrereleaseType(badPrerelease)
end
prereleaseOrdinal = Option.Some(ordinalNum :: number) :: Option<number>
@ -336,7 +357,7 @@ function Semver.parse(ver: string): SemverResult<SemverImpl>
end
end
return Result.Ok(setmetatable(parsed :: Version, Semver))
return Result.Ok(Semver.new(parsed))
end
local function prereleaseEq(leftPrerelease: PrereleaseVersion?, rightPrerelease: PrereleaseVersion?): boolean
@ -395,4 +416,24 @@ function Semver.__le(left: SemverImpl, right: SemverImpl): boolean
else prereleaseLe(left.prerelease:unwrapOrNil(), right.prerelease:unwrapOrNil())
end
function Semver.__tostring(self: SemverImpl): string
local base = `{self.major}.{self.minor}.{self.patch}`
local prerelease = self.prerelease
:map(function(prerelease: PrereleaseVersion)
return `-{prerelease.type}{prerelease.ordinal
:map(function(ord: number)
return `.{ord}`
end)
:unwrapOr("")}`
end)
:unwrapOr("")
local build = self.buildMetadata
:map(function(buildMetadata: string)
return `+{buildMetadata}`
end)
:unwrapOr("")
return base .. prerelease .. build
end
return Semver

View file

@ -1,15 +1,10 @@
name = "0x5eal/semver"
version = "0.1.0"
version = "0.1.1"
description = "Strongly typed semver (https://semver.org) parser for Luau"
authors = ["Erica Marigold <hi@devcomp.xyz>"]
repository = "https://github.com/0x5eal/semver-luau.git"
license = "MIT"
includes = [
"lib",
"pesde.toml",
"README.md",
"LICENSE.md",
]
includes = ["lib", "pesde.toml", "README.md", "LICENSE.md"]
[scripts]
test = ".lune/test"

4
rokit.toml Normal file
View file

@ -0,0 +1,4 @@
[tools]
lune = "lune-org/lune@0.8.9"
stylua = "JohnnyMorganz/StyLua@2.0.2"
luau-lsp = "JohnnyMorganz/luau-lsp@1.35.0"

55
tests/_reporter.luau Normal file
View file

@ -0,0 +1,55 @@
local stdio = require("@lune/stdio")
local frktest = require("@pkg/frktest")
local Reporter = frktest._reporters.lune_console_reporter
local STYLE = table.freeze({
suite = function(name: string)
return `{stdio.style("bold")}{stdio.color("purple")}SUITE{stdio.style("reset")} {name}`
end,
report = function(name: string, state: "run" | "success" | "error" | "skip")
local state_color: stdio.Color = if state == "run"
then "white"
elseif state == "success" then "green"
elseif state == "error" then "red"
elseif state == "skip" then "yellow"
else error("Invalid test state")
return ` {stdio.style("bold")}{stdio.color(state_color)}{if state == "skip" then "SKIP" else "TEST"}{stdio.style(
"reset"
)} {name}`
end,
})
--- Clears a the previous line, and moves to its beginning
local function clear_last_line(): ()
return stdio.write("\x1b[A\x1b[K\x1b[0G")
end
local ReporterExt = {}
function ReporterExt.init()
frktest.test.on_suite_enter(function(suite)
print(STYLE.suite(suite.name))
end)
frktest.test.on_suite_leave(function()
stdio.write("\n")
end)
frktest.test.on_test_enter(function(test)
print(STYLE.report(test.name, "run"))
end)
frktest.test.on_test_leave(function(test)
clear_last_line()
print(STYLE.report(test.name, if test.failed then "error" else "success"))
end)
frktest.test.on_test_skipped(function(test)
print(STYLE.report(test.name, "skip"))
end)
Reporter.init()
end
return setmetatable(ReporterExt, { __index = Reporter })

View file

@ -1,5 +1,4 @@
local frktest = require("@pkg/frktest")
local test = frktest.test
local check = frktest.assert.check
local Option = require("../luau_packages/option")
@ -7,96 +6,96 @@ type Option<T> = Option.Option<T>
local Semver = require("../lib")
return function()
test.suite("Semver comparison tests", function()
test.case("Basic version comparisons", function()
local v1 = Semver.parse("1.2.3"):unwrap()
local v2 = Semver.parse("1.2.4"):unwrap()
local v3 = Semver.parse("1.3.0"):unwrap()
local v4 = Semver.parse("2.0.0"):unwrap()
local v5 = Semver.parse("2.1.0"):unwrap()
local v6 = Semver.parse("3.0.0"):unwrap()
return function(test: typeof(frktest.test))
test.suite("Semver comparison tests", function()
test.case("Basic version comparisons", function()
local v1 = Semver.parse("1.2.3"):unwrap()
local v2 = Semver.parse("1.2.4"):unwrap()
local v3 = Semver.parse("1.3.0"):unwrap()
local v4 = Semver.parse("2.0.0"):unwrap()
local v5 = Semver.parse("2.1.0"):unwrap()
local v6 = Semver.parse("3.0.0"):unwrap()
check.is_true(v1 < v2)
check.is_true(v2 < v3)
check.is_true(v3 < v4)
check.is_true(v4 < v5)
check.is_true(v5 < v6)
check.is_true(v1 <= v2)
check.is_true(v2 > v1)
check.is_true(v4 >= v3)
check.is_true(v6 > v1)
end)
check.is_true(v1 < v2)
check.is_true(v2 < v3)
check.is_true(v3 < v4)
check.is_true(v4 < v5)
check.is_true(v5 < v6)
check.is_true(v1 <= v2)
check.is_true(v2 > v1)
check.is_true(v4 >= v3)
check.is_true(v6 > v1)
end)
test.case("Equal version comparisons", function()
local v1 = Semver.parse("1.2.3"):unwrap()
local v2 = Semver.parse("1.2.3"):unwrap()
local v3 = Semver.parse("1.2.3"):unwrap()
local v4 = Semver.parse("1.2.3"):unwrap()
test.case("Equal version comparisons", function()
local v1 = Semver.parse("1.2.3"):unwrap()
local v2 = Semver.parse("1.2.3"):unwrap()
local v3 = Semver.parse("1.2.3"):unwrap()
local v4 = Semver.parse("1.2.3"):unwrap()
check.is_true(v1 == v2)
check.is_true(v2 == v3)
check.is_true(v3 == v4)
check.is_true(v1 <= v2)
check.is_true(v1 >= v2)
check.is_false(v1 < v2)
check.is_false(v1 > v2)
check.is_false(v3 > v4)
end)
check.is_true(v1 == v2)
check.is_true(v2 == v3)
check.is_true(v3 == v4)
check.is_true(v1 <= v2)
check.is_true(v1 >= v2)
check.is_false(v1 < v2)
check.is_false(v1 > v2)
check.is_false(v3 > v4)
end)
test.case("Prerelease version comparisons", function()
local v1 = Semver.parse("1.2.3-alpha.1"):unwrap()
local v2 = Semver.parse("1.2.3-alpha.2"):unwrap()
local v3 = Semver.parse("1.2.3-beta.1"):unwrap()
local v4 = Semver.parse("1.2.3"):unwrap()
local v5 = Semver.parse("1.2.3-rc.1"):unwrap()
local v6 = Semver.parse("1.2.3-rc.2"):unwrap()
test.case("Prerelease version comparisons", function()
local v1 = Semver.parse("1.2.3-alpha.1"):unwrap()
local v2 = Semver.parse("1.2.3-alpha.2"):unwrap()
local v3 = Semver.parse("1.2.3-beta.1"):unwrap()
local v4 = Semver.parse("1.2.3"):unwrap()
local v5 = Semver.parse("1.2.3-rc.1"):unwrap()
local v6 = Semver.parse("1.2.3-rc.2"):unwrap()
check.is_true(v1 < v2)
check.is_true(v2 < v3)
check.is_true(v3 < v4)
check.is_true(v3 < v5)
check.is_true(v5 < v6)
check.is_true(v6 < v4)
check.is_false(v4 < v1)
check.is_true(v4 > v3)
end)
check.is_true(v1 < v2)
check.is_true(v2 < v3)
check.is_true(v3 < v4)
check.is_true(v3 < v5)
check.is_true(v5 < v6)
check.is_true(v6 < v4)
check.is_false(v4 < v1)
check.is_true(v4 > v3)
end)
test.case("Build metadata comparisons", function()
local v1 = Semver.parse("1.2.3+build.1"):unwrap()
local v2 = Semver.parse("1.2.3+build.2"):unwrap()
local v3 = Semver.parse("1.2.3+build.123"):unwrap()
local v4 = Semver.parse("1.2.3+20230615"):unwrap()
local v5 = Semver.parse("1.2.3+exp.sha.5114f85"):unwrap()
test.case("Build metadata comparisons", function()
local v1 = Semver.parse("1.2.3+build.1"):unwrap()
local v2 = Semver.parse("1.2.3+build.2"):unwrap()
local v3 = Semver.parse("1.2.3+build.123"):unwrap()
local v4 = Semver.parse("1.2.3+20230615"):unwrap()
local v5 = Semver.parse("1.2.3+exp.sha.5114f85"):unwrap()
-- Build metadata should be ignored in comparisons
check.is_true(v1 == v2)
check.is_true(v2 == v3)
check.is_true(v3 == v4)
check.is_true(v4 == v5)
check.is_false(v1 < v2)
check.is_false(v2 > v3)
check.is_false(v4 > v5)
end)
-- Build metadata should be ignored in comparisons
check.is_true(v1 == v2)
check.is_true(v2 == v3)
check.is_true(v3 == v4)
check.is_true(v4 == v5)
check.is_false(v1 < v2)
check.is_false(v2 > v3)
check.is_false(v4 > v5)
end)
test.case("Complex version comparisons", function()
local v1 = Semver.parse("2.0.0-alpha.1+build.123"):unwrap()
local v2 = Semver.parse("2.0.0-beta.1+build.123"):unwrap()
local v3 = Semver.parse("2.0.0+build.123"):unwrap()
local v4 = Semver.parse("2.1.0-alpha.1"):unwrap()
local v5 = Semver.parse("2.1.0"):unwrap()
local v6 = Semver.parse("2.1.0-rc.1+build.999"):unwrap()
local v7 = Semver.parse("2.1.1-alpha.1+sha.xyz"):unwrap()
test.case("Complex version comparisons", function()
local v1 = Semver.parse("2.0.0-alpha.1+build.123"):unwrap()
local v2 = Semver.parse("2.0.0-beta.1+build.123"):unwrap()
local v3 = Semver.parse("2.0.0+build.123"):unwrap()
local v4 = Semver.parse("2.1.0-alpha.1"):unwrap()
local v5 = Semver.parse("2.1.0"):unwrap()
local v6 = Semver.parse("2.1.0-rc.1+build.999"):unwrap()
local v7 = Semver.parse("2.1.1-alpha.1+sha.xyz"):unwrap()
check.is_true(v1 < v2)
check.is_true(v2 < v3)
check.is_true(v3 < v4)
check.is_true(v4 < v5)
check.is_true(v4 < v6)
check.is_true(v6 < v5)
check.is_true(v5 < v7)
check.is_false(v5 < v1)
check.is_false(v7 < v6)
end)
end)
check.is_true(v1 < v2)
check.is_true(v2 < v3)
check.is_true(v3 < v4)
check.is_true(v4 < v5)
check.is_true(v4 < v6)
check.is_true(v6 < v5)
check.is_true(v5 < v7)
check.is_false(v5 < v1)
check.is_false(v7 < v6)
end)
end)
end

View file

@ -1,122 +1,143 @@
local frktest = require("@pkg/frktest")
local test = frktest.test
local check = frktest.assert.check
local Semver = require("../lib")
return function()
test.suite("Invalid semver parsing tests", function()
test.case("Rejects missing components", function()
local res = Semver.parse("1.2")
check.is_true(res:isErr())
check.table.contains(res:unwrapErr(), {
kind = {
id = "MandatoryComponentMissing",
components = { "1", "2" },
},
})
end)
return function(test: typeof(frktest.test))
test.suite("Invalid semver parsing tests", function()
test.case("Rejects missing components", function()
local res = Semver.parse("1.2")
check.is_true(res:isErr())
check.table.contains(res:unwrapErr(), {
kind = {
id = "MandatoryComponentMissing",
components = { "1", "2" },
},
})
end)
test.case("Rejects invalid component types", function()
-- Test invalid major
local res = Semver.parse("a.2.3")
check.is_true(res:isErr())
check.table.contains(res:unwrapErr(), {
kind = {
id = "InvalidComponentType",
component = "major",
got = "char",
},
})
test.case("Rejects invalid component types", function()
-- Test invalid major
local res = Semver.parse("a.2.3")
check.is_true(res:isErr())
check.table.contains(res:unwrapErr(), {
kind = {
id = "InvalidComponentType",
component = "major",
got = "char",
},
})
-- Test invalid minor with symbols
res = Semver.parse("1.$.3")
check.is_true(res:isErr())
check.table.contains(res:unwrapErr(), {
kind = {
id = "InvalidComponentType",
component = "minor",
got = "symbol",
},
})
-- Test invalid minor with symbols
res = Semver.parse("1.$.3")
check.is_true(res:isErr())
check.table.contains(res:unwrapErr(), {
kind = {
id = "InvalidComponentType",
component = "minor",
got = "symbol",
},
})
-- Test invalid patch
res = Semver.parse("1.2.x")
check.is_true(res:isErr())
check.table.contains(res:unwrapErr(), {
kind = {
id = "InvalidComponentType",
component = "patch",
got = "char",
},
})
end)
-- Test invalid patch
res = Semver.parse("1.2.x")
check.is_true(res:isErr())
check.table.contains(res:unwrapErr(), {
kind = {
id = "InvalidComponentType",
component = "patch",
got = "char",
},
})
end)
test.case("Rejects leading zeros", function()
-- Test leading zeros in major
local res = Semver.parse("01.2.3")
check.is_true(res:isErr())
check.table.contains(res:unwrapErr(), {
kind = {
id = "LeadingZerosPresent",
component = "major",
},
})
test.case("Rejects leading zeros", function()
-- Test leading zeros in major
local res = Semver.parse("01.2.3")
check.is_true(res:isErr())
check.table.contains(res:unwrapErr(), {
kind = {
id = "LeadingZerosPresent",
component = "major",
},
})
-- Test leading zeros in minor
res = Semver.parse("1.02.3")
check.is_true(res:isErr())
check.table.contains(res:unwrapErr(), {
kind = {
id = "LeadingZerosPresent",
component = "minor",
},
})
-- Test leading zeros in minor
res = Semver.parse("1.02.3")
check.is_true(res:isErr())
check.table.contains(res:unwrapErr(), {
kind = {
id = "LeadingZerosPresent",
component = "minor",
},
})
-- Test leading zeros in patch
res = Semver.parse("1.2.03")
check.is_true(res:isErr())
check.table.contains(res:unwrapErr(), {
kind = {
id = "LeadingZerosPresent",
component = "patch",
},
})
end)
-- Test leading zeros in patch
res = Semver.parse("1.2.03")
check.is_true(res:isErr())
check.table.contains(res:unwrapErr(), {
kind = {
id = "LeadingZerosPresent",
component = "patch",
},
})
end)
test.case("Rejects invalid prerelease types", function()
local res = Semver.parse("1.2.3-gamma.1")
check.is_true(res:isErr())
check.table.contains(res:unwrapErr(), {
kind = {
id = "InvalidPrereleaseType",
type = "gamma",
},
})
end)
test.case("Rejects invalid prerelease types", function()
local res = Semver.parse("1.2.3-gamma.1")
check.is_true(res:isErr())
check.table.contains(res:unwrapErr(), {
kind = {
id = "InvalidPrereleaseType",
type = "gamma",
},
})
end)
test.case("Rejects invalid prerelease ordinals", function()
-- Test with character ordinal
local res = Semver.parse("1.2.3-beta.a")
check.is_true(res:isErr())
check.table.contains(res:unwrapErr(), {
kind = {
id = "InvalidPrereleaseOrdinalType",
expected = "number",
got = "char",
},
})
test.case("Rejects invalid prerelease ordinals", function()
-- Test with character ordinal
local res = Semver.parse("1.2.3-beta.abc")
check.is_true(res:isErr())
check.table.contains(res:unwrapErr(), {
kind = {
id = "InvalidPrereleaseOrdinalType",
expected = "number",
got = "char",
},
})
-- Test with symbol ordinal
res = Semver.parse("1.2.3-beta.$")
check.is_true(res:isErr())
check.table.contains(res:unwrapErr(), {
kind = {
id = "InvalidPrereleaseOrdinalType",
expected = "number",
got = "symbol",
},
})
end)
end)
-- Test with symbol ordinal
res = Semver.parse("1.2.3-beta.$")
check.is_true(res:isErr())
check.table.contains(res:unwrapErr(), {
kind = {
id = "InvalidPrereleaseOrdinalType",
expected = "number",
got = "symbol",
},
})
-- Test with extra symbols in ordinal
res = Semver.parse("1.2.3-beta.3.4.5")
check.is_true(res:isErr())
check.table.contains(res:unwrapErr(), {
kind = {
id = "InvalidPrereleaseOrdinalType",
expected = "number",
got = "symbol",
},
})
-- Test with extra symbols in ordinal and build metadata
res = Semver.parse("1.2.3-beta.3.4.5+build.1732213169")
check.is_true(res:isErr())
check.table.contains(res:unwrapErr(), {
kind = {
id = "InvalidPrereleaseOrdinalType",
expected = "number",
got = "symbol",
},
})
end)
end)
end

View file

@ -1,5 +1,4 @@
local frktest = require("@pkg/frktest")
local test = frktest.test
local check = frktest.assert.check
local Option = require("../luau_packages/option")
@ -7,66 +6,66 @@ type Option<T> = Option.Option<T>
local Semver = require("../lib")
return function()
test.suite("Basic tests", function()
test.case("Semver creates valid version objects", function()
local res = Semver.parse("1.2.3-beta.1")
check.is_true(res:isOk())
return function(test: typeof(frktest.test))
test.suite("Basic tests", function()
test.case("Semver creates valid version objects", function()
local res = Semver.parse("1.2.3-beta.1")
check.is_true(res:isOk())
local version = res:unwrap()
check.equal(version.major, 1)
check.equal(version.minor, 2)
check.equal(version.patch, 3)
end)
local version = res:unwrap()
check.equal(version.major, 1)
check.equal(version.minor, 2)
check.equal(version.patch, 3)
end)
test.case("Semver handles prerelease versions", function()
local res = Semver.parse("1.2.3-beta.1")
check.is_true(res:isOk())
test.case("Semver handles prerelease versions", function()
local res = Semver.parse("1.2.3-beta.1")
check.is_true(res:isOk())
local version = res:unwrap()
check.equal(version.major, 1)
check.equal(version.minor, 2)
check.equal(version.patch, 3)
local version = res:unwrap()
check.equal(version.major, 1)
check.equal(version.minor, 2)
check.equal(version.patch, 3)
check.table.equal(
version.prerelease,
Option.Some({
type = "beta",
ordinal = Option.Some(1),
})
)
end)
check.table.equal(
version.prerelease,
Option.Some({
type = "beta",
ordinal = Option.Some(1),
})
)
end)
test.case("Semver handles build metadata", function()
local res = Semver.parse("1.2.3+build.123")
check.is_true(res:isOk())
test.case("Semver handles build metadata", function()
local res = Semver.parse("1.2.3+build.123")
check.is_true(res:isOk())
local version = res:unwrap()
local version = res:unwrap()
check.equal(version.major, 1)
check.equal(version.minor, 2)
check.equal(version.patch, 3)
check.table.equal(version.buildMetadata, Option.Some("build.123"))
end)
check.equal(version.major, 1)
check.equal(version.minor, 2)
check.equal(version.patch, 3)
check.table.equal(version.buildMetadata, Option.Some("build.123"))
end)
test.case("Semver handles prerelease versions with build metadata", function()
local res = Semver.parse("1.2.3-beta.1+build.123")
check.is_true(res:isOk())
test.case("Semver handles prerelease versions with build metadata", function()
local res = Semver.parse("1.2.3-beta.1+build.123")
check.is_true(res:isOk())
local version = res:unwrap()
check.equal(version.major, 1)
check.equal(version.minor, 2)
check.equal(version.patch, 3)
local version = res:unwrap()
check.equal(version.major, 1)
check.equal(version.minor, 2)
check.equal(version.patch, 3)
check.table.equal(
version.prerelease,
Option.Some({
type = "beta",
ordinal = Option.Some(1),
})
)
check.table.equal(
version.prerelease,
Option.Some({
type = "beta",
ordinal = Option.Some(1),
})
)
check.table.equal(version.buildMetadata, Option.Some("build.123"))
end)
end)
check.table.equal(version.buildMetadata, Option.Some("build.123"))
end)
end)
end

82
tests/tostring.luau Normal file
View file

@ -0,0 +1,82 @@
local frktest = require("@pkg/frktest")
local check = frktest.assert.check
local Option = require("../luau_packages/option")
type Option<T> = Option.Option<T>
local Semver = require("../lib")
return function(test: typeof(frktest.test))
test.suite("Stringification tests", function()
test.case("A version constructed with new() when stringified should match expected string", function()
local versionMap: { [string]: Semver.Version } = {
["1.2.3"] = { major = 1, minor = 2, patch = 3, prerelease = Option.None, buildMetadata = Option.None },
["1.0.0-alpha"] = {
major = 1,
minor = 0,
patch = 0,
prerelease = Option.Some({ type = "alpha" :: Semver.PreleaseType, ordinal = Option.None }),
buildMetadata = Option.None,
},
["2.3.4-beta.1"] = {
major = 2,
minor = 3,
patch = 4,
prerelease = Option.Some({
type = "beta" :: Semver.PreleaseType,
ordinal = Option.Some(1),
}),
buildMetadata = Option.None,
},
["3.0.0-rc.1+build.123"] = {
major = 3,
minor = 0,
patch = 0,
prerelease = Option.Some({
type = "rc" :: Semver.PreleaseType,
ordinal = Option.Some(1),
}),
buildMetadata = Option.Some("build.123"),
},
["4.5.6+sha.xyz"] = {
major = 4,
minor = 5,
patch = 6,
prerelease = Option.None,
buildMetadata = Option.Some("sha.xyz"),
},
}
-- FIXME: unknown usage here is because these types are too complex for
-- Luau to typecheck properly, we cast it manually since the above
-- map is typed properly anyway
for expectedString, version: unknown in versionMap do
local constructed = Semver.new(version :: Semver.Version)
local stringified = tostring(constructed)
check.equal(stringified, expectedString)
end
end)
test.case("A parsed version when stringified should not change in roundtrip", function()
local versions = {
"1.2.3",
"1.0.0-alpha",
"2.3.4-beta.1",
"3.0.0-rc.1+build.123",
"4.5.6+sha.xyz",
"5.0.0-alpha.1+build.999",
"6.7.8-beta.2+exp.sha.5114f85",
"7.0.0-alpha.1",
"9.9.9+20230615",
}
for _, version in versions do
local parsed = Semver.parse(version):unwrap()
local stringified = tostring(parsed)
check.equal(stringified, version)
end
end)
end)
end