Compare commits

...

10 Commits

Author SHA1 Message Date
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 353 additions and 11 deletions
+47
View File
@@ -0,0 +1,47 @@
name: Test
on:
pull_request:
branches:
- develop
push:
branches:
- develop
jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ^1.21.0
- name: Display Go version
run: go version
- name: Install dependencies
run: go get
- name: Run tests
run: go test -v
notify:
name: Send Discord workflow notification
runs-on: ubuntu-latest
needs: test
steps:
- name: Send notification
uses: annybs/action-notify-discord@v1
if: ${{ always() }}
with:
repository: ${{ github.repository }}
result: ${{ needs.test.result }}
run-id: ${{ github.run_id }}
run-number: ${{ github.run_number }}
webhook-url: ${{ secrets.DISCORD_WEBHOOK }}
workflow: ${{ github.workflow }}
+1
View File
@@ -0,0 +1 @@
go.sum
+1 -1
View File
@@ -1,6 +1,6 @@
# MIT License # 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: 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 ## 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" import "fmt"
// Version error.
var (
ErrInvalidVersion = Error{Message: "invalid version %q"}
)
// Error represents a version error. // Error represents a version error.
type Error struct { type Error struct {
Message string Message string
Version string Version string
} }
// Version error.
var (
ErrInvalidVersion = Error{Message: "invalid version %q"}
)
// Error retrieves the message of a REST API error. // Error retrieves the message of a REST API error.
func (e Error) Error() string { func (e Error) Error() string {
return fmt.Sprintf(e.Message, e.Version) return fmt.Sprintf(e.Message, e.Version)
+1 -1
View File
@@ -1,3 +1,3 @@
module github.com/annybs/go/version module gogs.home.aneur.in/go/version
go 1.21 go 1.21
+21
View File
@@ -3,6 +3,27 @@ package version
// List is a slice of versions that implements sort.Interface. // List is a slice of versions that implements sort.Interface.
type List []*Version 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 { func (list List) Len() int {
return len(list) 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)
}
}
}
+90
View File
@@ -0,0 +1,90 @@
package version
import (
"sort"
)
// 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 func(*Version)) 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 {
after(v)
}
}
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 func(*Version)) error {
versions, err := m.Versions()
if err != nil {
return err
}
for _, v := range versions {
patch := m[v.Text]
if err := patch.Up(); err != nil {
return err
}
if after != nil {
after(v)
}
}
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. // Extensions such as pre-release version or build metadata are ignored when comparing versions.
func (a *Version) Compare(b *Version) int { 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 return 0
} }
@@ -40,6 +44,47 @@ func (a *Version) Compare(b *Version) int {
return -1 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). // 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. // 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.0"), Expected: 1},
{A: MustParse("1.20.0"), B: MustParse("1.2.20"), 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.20.0"), B: MustParse("1.20.1"), Expected: -1},
{A: MustParse("1.0.0"), Expected: 0}, {A: MustParse("1.0.0"), Expected: 1},
{B: MustParse("1.0.0"), Expected: 0}, {B: MustParse("1.0.0"), Expected: -1},
{Expected: 0}, {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)
}
}
}