Compare commits

...
Author SHA1 Message Date
claudeandClaude Sonnet 5 8a0f5c45e7 add CI workflow running go vet and go test
CI / test (pull_request) Successful in 38s
Adds .gitea/workflows/ci.yml: on push to main and on pull requests, run
`go vet ./...` then `go test ./...` in a golang:1.21-alpine container.

Also removes .github/workflows/test.yml, which only triggered on a
`develop` branch that no longer exists and depended on an external
Discord notification action and an unset DISCORD_WEBHOOK secret.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 12:50:54 +01:00
aneurin 9f229dde97 improve patch callback in migrations 2026-07-06 22:04:36 +01:00
aneurin 384a4a94d1 rename package hopefully for last time 2026-07-04 01:38:03 +01:00
aneurin 6e3ff81381 improve constraint matching, migration structure 2026-06-25 13:52:56 +01:00
aneurin 95c8c6a839 add migration type 2026-06-24 16:51:49 +01:00
aneurin 53786e214f update module name 2026-06-24 16:51:40 +01:00
Aneurin Barker Snook 63bfda2a7e add test workflow 2024-07-19 14:33:22 +01:00
Aneurin Barker Snook d02f5a6d0f ensure go.sum not committed 2024-07-19 14:33:22 +01:00
Aneurin Barker Snook 709bc58463 fix link to license 2024-07-19 14:33:22 +01:00
Aneurin Barker Snook 24588faf35 rename module 2024-07-19 14:33:22 +01:00
Aneurin Barker Snook 72e0de304e fix nil version comparisons, add equal func 2024-07-15 08:54:56 +01:00
Aneurin Barker Snook 8f3b8e5c46 reorganise version errors 2024-07-12 12:20:08 +01:00
Aneurin Barker Snook 33d31f84be add version constraints 2024-07-12 12:11:18 +01:00
12 changed files with 345 additions and 11 deletions
+26
View File
@@ -0,0 +1,26 @@
# Vet and test the package. This is a pure Go library -- no build
# step, no services -- so CI just runs `go vet` then `go test` on
# every push to main and every pull request.
name: CI
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
container:
image: golang:1.21-alpine
steps:
- name: Install checkout dependencies
run: apk add --no-cache git nodejs
- uses: actions/checkout@v4
- name: go vet
run: go vet ./...
- name: go test
run: go test ./...
+1
View File
@@ -0,0 +1 @@
go.sum
+1 -1
View File
@@ -1,6 +1,6 @@
# MIT License
Copyright © 2024 Aneurin Barker Snook a@aneur.in
Copyright © 2026 Aneurin Barker Snook a@aneur.in
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+1 -1
View File
@@ -4,4 +4,4 @@ A simple library for parsing and comparing versions according to [Semantic Versi
## License
See [LICENSE.md](../LICENSE.md)
See [LICENSE.md](./LICENSE.md)
+9
View File
@@ -0,0 +1,9 @@
package version
// Constraint enables matching a version based on lower and upper bounds.
type Constraint struct {
Gt *Version // Greater than...
Gte *Version // Greater than or equal to...
Lt *Version // Less than...
Lte *Version // Less than or equal to...
}
+5 -5
View File
@@ -2,17 +2,17 @@ package version
import "fmt"
// Version error.
var (
ErrInvalidVersion = Error{Message: "invalid version %q"}
)
// Error represents a version error.
type Error struct {
Message string
Version string
}
// Version error.
var (
ErrInvalidVersion = Error{Message: "invalid version %q"}
)
// Error retrieves the message of a REST API error.
func (e Error) Error() string {
return fmt.Sprintf(e.Message, e.Version)
+1 -1
View File
@@ -1,3 +1,3 @@
module github.com/annybs/go/version
module code.aneur.in/go/version
go 1.21
+21
View File
@@ -3,6 +3,27 @@ package version
// List is a slice of versions that implements sort.Interface.
type List []*Version
// Match tests versions against a constraint and returns a new List of matching versions only.
func (list List) Match(constraints ...*Constraint) List {
filtered := List{}
for _, v := range list {
ok := true
for _, c := range constraints {
if !v.Match(c) {
ok = false
break
}
}
if ok {
filtered = append(filtered, v)
}
}
return filtered
}
func (list List) Len() int {
return len(list)
}
+48
View File
@@ -47,3 +47,51 @@ func TestListSort(t *testing.T) {
}
}
}
func TestList_Match(t *testing.T) {
type TestCase struct {
Input List
Constraint *Constraint
Expected List
}
testCases := []TestCase{
{
Input: List{MustParse("1.0.0"), MustParse("1.1.0"), MustParse("2.0.2"), MustParse("3.4.5")},
Expected: List{MustParse("1.0.0"), MustParse("1.1.0"), MustParse("2.0.2"), MustParse("3.4.5")},
},
{
Input: List{MustParse("1.0.0"), MustParse("1.1.0"), MustParse("2.0.2"), MustParse("3.4.5")},
Constraint: &Constraint{Gt: MustParse("1.0.0")},
Expected: List{MustParse("1.1.0"), MustParse("2.0.2"), MustParse("3.4.5")},
},
{
Input: List{MustParse("1.0.0"), MustParse("1.1.0"), MustParse("2.0.2"), MustParse("3.4.5")},
Constraint: &Constraint{Lt: MustParse("3.0.0")},
Expected: List{MustParse("1.0.0"), MustParse("1.1.0"), MustParse("2.0.2")},
},
{
Input: List{MustParse("1.0.0"), MustParse("1.1.0"), MustParse("2.0.2"), MustParse("3.4.5")},
Constraint: &Constraint{Gte: MustParse("2.0.2")},
Expected: List{MustParse("2.0.2"), MustParse("3.4.5")},
},
}
for i, testCase := range testCases {
actual := testCase.Input.Match(testCase.Constraint)
ok := true
for j, v := range actual {
expected := testCase.Expected[j]
cmp := v.Compare(expected)
if cmp != 0 {
ok = false
t.Errorf("test %d failed at position %d (expected %s, got %s)", i, j, expected, actual)
}
}
if ok {
t.Logf("test %d passed", i)
}
}
}
+103
View File
@@ -0,0 +1,103 @@
package version
import (
"sort"
)
// AfterPatchCallback is called after each time a migration is successfully applied (up) or reverted (down).
type AfterPatchCallback func(applied, next *Version)
// Migration maps provide a simple way to store and run versioned 'patch' functions.
type Migration map[string]Patch
// Down reverts all patches in reverse version order.
func (m Migration) Down(after AfterPatchCallback) error {
versions, err := m.Versions()
if err != nil {
return err
}
for i := len(versions); i > 0; i-- {
v := versions[i-1]
patch := m[v.Text]
if err := patch.Down(); err != nil {
return err
}
if after != nil {
var next *Version
if i > 1 {
next = versions[i-1]
}
after(v, next)
}
}
return nil
}
// Match tests versions against a constraint and returns a new migration map of matching versions only.
func (m Migration) Match(constraints ...*Constraint) (Migration, error) {
l, err := m.Versions()
if err != nil {
return nil, err
}
l = l.Match(constraints...)
m2 := Migration{}
for _, v := range l {
m2[v.Text] = m[v.Text]
}
return m2, nil
}
// Up executes all patches in version order.
func (m Migration) Up(after AfterPatchCallback) error {
versions, err := m.Versions()
if err != nil {
return err
}
for i, v := range versions {
patch := m[v.Text]
if err := patch.Up(); err != nil {
return err
}
if after != nil {
var next *Version
if i < len(versions)-1 {
next = versions[i+1]
}
after(v, next)
}
}
return nil
}
// Versions returns a List of all versions in the migration map.
func (m Migration) Versions() (List, error) {
l := List{}
for str := range m {
v, err := Parse(str)
if err != nil {
return nil, err
}
l = append(l, v)
}
sort.Stable(l)
return l, nil
}
type Patch struct {
Down func() error
Up func() error
}
+46 -1
View File
@@ -19,7 +19,11 @@ type Version struct {
//
// Extensions such as pre-release version or build metadata are ignored when comparing versions.
func (a *Version) Compare(b *Version) int {
if a == nil || b == nil {
if a == nil && b != nil {
return -1
} else if a != nil && b == nil {
return 1
} else if a == nil && b == nil {
return 0
}
@@ -40,6 +44,47 @@ func (a *Version) Compare(b *Version) int {
return -1
}
// Equal checks for equality between two versions.
//
// Extensions such as pre-release version or build metadata are ignored when comparing versions.
func (a *Version) Equal(b *Version) bool {
return a.Compare(b) == 0
}
// Match tests the version against a constraint.
// Gt and Lt take precedence over Gte and Lte.
func (v *Version) Match(c *Constraint) bool {
if v == nil {
return false
}
if c == nil {
return true
}
if c.Gt != nil {
if v.Compare(c.Gt) <= 0 {
return false
}
} else if c.Gte != nil {
if v.Compare(c.Gte) < 0 {
return false
}
}
if c.Lt != nil {
if v.Compare(c.Lt) >= 0 {
return false
}
} else if c.Lte != nil {
if v.Compare(c.Lte) > 0 {
return false
}
}
return true
}
// Less performs a simple comparison of this version (a) with another version (b).
// This function returns true if a is less than b, or false otherwise.
//
+83 -2
View File
@@ -55,8 +55,8 @@ func TestVersion_Compare(t *testing.T) {
{A: MustParse("1.20.0"), B: MustParse("1.2.0"), Expected: 1},
{A: MustParse("1.20.0"), B: MustParse("1.2.20"), Expected: 1},
{A: MustParse("1.20.0"), B: MustParse("1.20.1"), Expected: -1},
{A: MustParse("1.0.0"), Expected: 0},
{B: MustParse("1.0.0"), Expected: 0},
{A: MustParse("1.0.0"), Expected: 1},
{B: MustParse("1.0.0"), Expected: -1},
{Expected: 0},
}
@@ -102,3 +102,84 @@ func TestVersion_Less(t *testing.T) {
}
}
}
func TestVersion_Match(t *testing.T) {
type TestCase struct {
V *Version
C *Constraint
Expected bool
}
testCases := []TestCase{
{V: MustParse("1.0.0"), Expected: true},
{V: MustParse("1.0.0"), C: &Constraint{Gt: MustParse("0.1.0")}, Expected: true},
{V: MustParse("1.0.0"), C: &Constraint{Gt: MustParse("1.0.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Gt: MustParse("1.1.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Gte: MustParse("0.1.0")}, Expected: true},
{V: MustParse("1.0.0"), C: &Constraint{Gte: MustParse("1.0.0")}, Expected: true},
{V: MustParse("1.0.0"), C: &Constraint{Gte: MustParse("1.1.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Lt: MustParse("0.1.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Lt: MustParse("1.0.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Lt: MustParse("1.1.0")}, Expected: true},
{V: MustParse("1.0.0"), C: &Constraint{Lte: MustParse("0.1.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Lte: MustParse("1.0.0")}, Expected: true},
{V: MustParse("1.0.0"), C: &Constraint{Lte: MustParse("1.1.0")}, Expected: true},
{V: MustParse("1.0.0"), C: &Constraint{Gt: MustParse("0.1.0"), Lt: MustParse("0.1.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Gt: MustParse("0.1.0"), Lt: MustParse("1.0.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Gt: MustParse("0.1.0"), Lt: MustParse("1.1.0")}, Expected: true},
{V: MustParse("1.0.0"), C: &Constraint{Gt: MustParse("1.0.0"), Lt: MustParse("0.1.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Gt: MustParse("1.0.0"), Lt: MustParse("1.0.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Gt: MustParse("1.0.0"), Lt: MustParse("1.1.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Gt: MustParse("1.1.0"), Lt: MustParse("0.1.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Gt: MustParse("1.1.0"), Lt: MustParse("1.0.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Gt: MustParse("1.1.0"), Lt: MustParse("1.1.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Gt: MustParse("0.1.0"), Lte: MustParse("0.1.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Gt: MustParse("0.1.0"), Lte: MustParse("1.0.0")}, Expected: true},
{V: MustParse("1.0.0"), C: &Constraint{Gt: MustParse("0.1.0"), Lte: MustParse("1.1.0")}, Expected: true},
{V: MustParse("1.0.0"), C: &Constraint{Gt: MustParse("1.0.0"), Lte: MustParse("0.1.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Gt: MustParse("1.0.0"), Lte: MustParse("1.0.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Gt: MustParse("1.0.0"), Lte: MustParse("1.1.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Gt: MustParse("1.1.0"), Lte: MustParse("0.1.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Gt: MustParse("1.1.0"), Lte: MustParse("1.0.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Gt: MustParse("1.1.0"), Lte: MustParse("1.1.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Gte: MustParse("0.1.0"), Lt: MustParse("0.1.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Gte: MustParse("0.1.0"), Lt: MustParse("1.0.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Gte: MustParse("0.1.0"), Lt: MustParse("1.1.0")}, Expected: true},
{V: MustParse("1.0.0"), C: &Constraint{Gte: MustParse("1.0.0"), Lt: MustParse("0.1.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Gte: MustParse("1.0.0"), Lt: MustParse("1.0.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Gte: MustParse("1.0.0"), Lt: MustParse("1.1.0")}, Expected: true},
{V: MustParse("1.0.0"), C: &Constraint{Gte: MustParse("1.1.0"), Lt: MustParse("0.1.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Gte: MustParse("1.1.0"), Lt: MustParse("1.0.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Gte: MustParse("1.1.0"), Lt: MustParse("1.1.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Gte: MustParse("0.1.0"), Lte: MustParse("0.1.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Gte: MustParse("0.1.0"), Lte: MustParse("1.0.0")}, Expected: true},
{V: MustParse("1.0.0"), C: &Constraint{Gte: MustParse("0.1.0"), Lte: MustParse("1.1.0")}, Expected: true},
{V: MustParse("1.0.0"), C: &Constraint{Gte: MustParse("1.0.0"), Lte: MustParse("0.1.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Gte: MustParse("1.0.0"), Lte: MustParse("1.0.0")}, Expected: true},
{V: MustParse("1.0.0"), C: &Constraint{Gte: MustParse("1.0.0"), Lte: MustParse("1.1.0")}, Expected: true},
{V: MustParse("1.0.0"), C: &Constraint{Gte: MustParse("1.1.0"), Lte: MustParse("0.1.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Gte: MustParse("1.1.0"), Lte: MustParse("1.0.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Gte: MustParse("1.1.0"), Lte: MustParse("1.1.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Gt: MustParse("1.0.0"), Gte: MustParse("1.0.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Gt: MustParse("0.1.0"), Gte: MustParse("1.1.0")}, Expected: true},
{V: MustParse("1.0.0"), C: &Constraint{Gt: MustParse("1.1.0"), Gte: MustParse("0.1.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Lt: MustParse("1.0.0"), Lte: MustParse("1.0.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Lt: MustParse("0.1.0"), Lte: MustParse("1.1.0")}, Expected: false},
{V: MustParse("1.0.0"), C: &Constraint{Lt: MustParse("1.1.0"), Lte: MustParse("0.1.0")}, Expected: true},
}
for i, testCase := range testCases {
actual := testCase.V.Match(testCase.C)
if actual != testCase.Expected {
t.Errorf("test %d failed (expected %v, actual %v)", i, testCase.Expected, actual)
} else {
t.Logf("test %d passed with %v", i, actual)
}
}
}