From 33d31f84bea6b289c550a4ae2b455523d4eeebd6 Mon Sep 17 00:00:00 2001 From: Aneurin Barker Snook Date: Fri, 12 Jul 2024 12:11:18 +0100 Subject: [PATCH] add version constraints --- constraint.go | 9 ++++++ list.go | 13 ++++++++ list_test.go | 48 +++++++++++++++++++++++++++++ version.go | 34 +++++++++++++++++++++ version_test.go | 81 +++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 185 insertions(+) create mode 100644 constraint.go diff --git a/constraint.go b/constraint.go new file mode 100644 index 0000000..26c1838 --- /dev/null +++ b/constraint.go @@ -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... +} diff --git a/list.go b/list.go index 20deb16..9bd7bc0 100644 --- a/list.go +++ b/list.go @@ -3,6 +3,19 @@ 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(c *Constraint) List { + filtered := List{} + + for _, v := range list { + if v.Match(c) { + filtered = append(filtered, v) + } + } + + return filtered +} + func (list List) Len() int { return len(list) } diff --git a/list_test.go b/list_test.go index f061d1e..6e6bcd3 100644 --- a/list_test.go +++ b/list_test.go @@ -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) + } + } +} diff --git a/version.go b/version.go index 7c1ebc0..e4b5448 100644 --- a/version.go +++ b/version.go @@ -40,6 +40,40 @@ func (a *Version) Compare(b *Version) int { return -1 } +// 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. // diff --git a/version_test.go b/version_test.go index 85df525..d9e03e4 100644 --- a/version_test.go +++ b/version_test.go @@ -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) + } + } +}