pax_global_header00006660000000000000000000000064147707456460014536gustar00rootroot0000000000000052 comment=5faa0faa5242cba61ce3dc0037935f698c4a2b9c relaxed-semver-0.15.0/000077500000000000000000000000001477074564600145445ustar00rootroot00000000000000relaxed-semver-0.15.0/.github/000077500000000000000000000000001477074564600161045ustar00rootroot00000000000000relaxed-semver-0.15.0/.github/workflows/000077500000000000000000000000001477074564600201415ustar00rootroot00000000000000relaxed-semver-0.15.0/.github/workflows/test.yaml000066400000000000000000000017161477074564600220110ustar00rootroot00000000000000name: test on: push: branches: - master pull_request: jobs: unit-test: strategy: matrix: os: [windows-latest, macOS-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@master - uses: actions/setup-go@v1 with: go-version: "1.24" - name: Build native run: GOARCH=amd64 go build -v ./... shell: bash - name: Run unit tests run: go test -v -race ./... shell: bash unit-test-with-coverage: runs-on: ubuntu-latest steps: - uses: actions/checkout@master - uses: actions/setup-go@v1 with: go-version: "1.24" - name: Run unit tests with coverage run: go test -v -race -covermode=atomic -coverprofile=coverage.out ./... shell: bash - uses: codecov/codecov-action@v5 with: verbose: true env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} relaxed-semver-0.15.0/LICENSE000066400000000000000000000027501477074564600155550ustar00rootroot00000000000000 Copyright (c) 2018-2025, Cristian Maglie. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. relaxed-semver-0.15.0/README.md000066400000000000000000000174211477074564600160300ustar00rootroot00000000000000# go.bug.st/relaxed-semver [![build status](https://github.com/bugst/relaxed-semver/workflows/test/badge.svg)](https://travis-ci.org/bugst/relaxed-semver) [![codecov](https://codecov.io/gh/bugst/relaxed-semver/branch/master/graph/badge.svg)](https://codecov.io/gh/bugst/relaxed-semver) A library for handling a superset of semantic versioning in golang. ## Documentation and examples See the godoc here: https://godoc.org/go.bug.st/relaxed-semver ## Semantic versioning specification followed in this library This library tries to implement the semantic versioning specification [2.0.0](https://semver.org/spec/v2.0.0.html) with an exception: the numeric format `major.minor.patch` like `1.3.2` may be truncated if a number is zero, so: - `1.2.0` or `1.2.0-beta` may be written as `1.2` or `1.2-beta` respectively - `1.0.0` or `1.0.0-beta` may be written `1` or `1-beta` respectively - `0.0.0` may be written as the **empty string**, but `0.0.0-beta` may **not** be written as `-beta` ## Usage You can parse a semver version string with the `Parse` function that returns a `Version` object that can be used to be compared with other `Version` objects using the `CompareTo`, `LessThan` , `LessThanOrEqual`, `Equal`, `GreaterThan` and `GreaterThanOrEqual` methods. The `Parse` function returns an `error` if the string does not comply to the above specification. Alternatively the `MustParse` function can be used, it returns only the `Version` object or panics if a parsing error occurs. ## Why Relaxed? This library allows the use of an even more relaxed semver specification using the `RelaxedVersion` object. It works with the following rules: - If the parsed string is a valid semver (following the rules above), then the `RelaxedVersion` will behave exactly as a normal `Version` object - if the parsed string is **not** a valid semver, then the string is kept as-is inside the `RelaxedVersion` object as a custom version string - when comparing two `RelaxedVersion` the rule is simple: if both are valid semver, the semver rules applies; if both are custom version string they are compared as alphanumeric strings; if one is valid semver and the other is a custom version string the valid semver is always greater - two `RelaxedVersion` are compatible (by the `CompatibleWith` operation) only if - they are equal - they are both valid semver and they are compatible as per semver specification The `RelaxedVersion` object is basically made to allow systems that do not use semver to soft transition to semantic versioning, because it allows an intermediate period where the invalid version is still tolerated. To parse a `RelaxedVersion` you can use the `ParseRelaxed` function. ## Version constraints Dependency version matching can be specified via version constraints, which might be a version range or an exact version. The following operators are supported: | | | | -------- | ------------------------ | | `=` | equal to | | `>` | greater than | | `>=` | greater than or equal to | | `<` | less than | | `<=` | less than or equal to | | `^` | compatible-with | | `!` | NOT | | `&&` | AND | | `\|\|` | OR | | `(`, `)` | constraint group | ### Examples Given the following releases of a dependency: - `0.1.0` - `0.1.1` - `0.2.0` - `1.0.0` - `2.0.0` - `2.0.5` - `2.0.6` - `2.1.0` - `3.0.0` constraints conditions would match as follows: | The following condition... | will match with versions... | | -------------------------------- | ---------------------------------------------------------------------- | | `=1.0.0` | `1.0.0` | | `>1.0.0` | `2.0.0`, `2.0.5`, `2.0.6`, `2.1.0`, `3.0.0` | | `>=1.0.0` | `1.0.0`, `2.0.0`, `2.0.5`, `2.0.6`, `2.1.0`, `3.0.0` | | `<2.0.0` | `0.1.0`, `0.1.1`, `0.2.0`, `1.0.0` | | `<=2.0.0` | `0.1.0`, `0.1.1`, `0.2.0`, `1.0.0`, `2.0.0` | | `!=1.0.0` | `0.1.0`, `0.1.1`, `0.2.0`, `2.0.0`, `2.0.5`, `2.0.6`, `2.1.0`, `3.0.0` | | `>1.0.0 && <2.1.0` | `2.0.0`, `2.0.5`, `2.0.6` | | `<1.0.0 \|\| >2.0.0` | `0.1.0`, `0.1.1`, `0.2.0`, `2.0.5`, `2.0.6`, `2.1.0`, `3.0.0` | | `(>0.1.0 && <2.0.0) \|\| >2.0.5` | `0.1.1`, `0.2.0`, `1.0.0`, `2.0.6`, `2.1.0`, `3.0.0` | | `^2.0.5` | `2.0.5`, `2.0.6`, `2.1.0` | | `^0.1.0` | `0.1.0`, `0.1.1` | ## Json parsable The `Version` and `RelaxedVersion` have the JSON un/marshaler implemented so they can be JSON decoded/encoded. ## Binary/GOB encoding support The `Version` and `RelaxedVersion` provides optimized `MarshalBinary`/`UnmarshalBinary` methods for binary encoding. ## Yaml parsable with `gopkg.in/yaml.v3` The `Version` and `RelaxedVersion` have the YAML un/marshaler implemented so they can be YAML decoded/encoded with the excellent `gopkg.in/yaml.v3` library. ## SQL support The `Version` and `RelaxedVersion` types provides the `sql.Scanner` and `driver.Valuer` interfaces. Those objects could be directly used in SQL queries, their value will be mapped into a string field. ## Lexicographic sortable strings that keeps semantic versioning order The `Version` and `RelaxedVersion` objects provides the `SortableString()` method that returns a string with a peculiar property: the alphanumeric sorting of two `Version.SortableString()` matches the semantic versioning ordering of the underling `Version` objects. In other words, given two `Version` object `a` and `b`: * if `a.LessThan(b)` is true then `a.SortableString() < b.SortableString()` is true and vice-versa. * if `a.Equals(b)` is true then `a.SortableString() == b.SortableString()` is true and vice-versa. * more generally, the following assertion is always true: `a.CompareTo(b) == cmp.Compare(a.SortableString(), b.SortableString())` This is accomplished by adding some adjustment characters to the original semver `Version` string with the purpose to change the behaviour of the natural alphabetic ordering, in particular: * to allow comparison of numeric values (keeping digits aligned by unit, tenths, hundhereds, etc...). * to invert the ordering of versions with and without prelease (a version with prelease should be lower priority compared to the same version without prerelease, but being longer alphanumerically it naturally follows it). To give you an idea on how it works, the following table shows some examples of semver versions and their `SortableString` counter part: | semver | `SortableString()` | | ------------------ | ------------------- | | `1.2.4` | `;1.2.4;` | | `1.3.0-rc` | `;1.3.0-;rc` | | `1.3.0-rc.0` | `;1.3.0-;rc,:0` | | `1.3.0-rc.5` | `;1.3.0-;rc,:5` | | `1.3.0-rc.5+build` | `;1.3.0-;rc,:5` | | `1.3.0-rc.20` | `;1.3.0-;rc,::20` | | `1.3.0-rc-` | `;1.3.0-;rc-` | | `1.3.0` | `;1.3.0;` | | `1.20.0` | `;1.:20.0;` | | `1.90.0` | `;1.:90.0;` | | `1.300.0-6` | `;1.::300.0-:6` | | `1.300.0-30` | `;1.::300.0-::30` | | `1.300.0-1pre` | `;1.::300.0-;1pre` | | `1.300.0-pre` | `;1.::300.0-;pre` | | `1.300.0` | `;1.::300.0;` | The `SortableString()` can be used in SQL databases to simplify the ordering of a set of versions in a table. relaxed-semver-0.15.0/benchmark_test.go000066400000000000000000000061621477074564600200710ustar00rootroot00000000000000// // Copyright 2018-2025 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // package semver import ( "testing" ) var list = []string{ "0.0.1-rc.0", // 0 "0.0.1-rc.0+build", // 1 "0.0.1-rc.1", // 2 "0.0.1", // 3 "0.0.1+build", // 4 "0.0.2-rc.1", // 5 - BREAKING CHANGE "0.0.2-rc.1+build", // 6 "0.0.2", // 7 "0.0.2+build", // 8 "0.0.3-rc.1", // 9 - BREAKING CHANGE "0.0.3-rc.2", // 10 "0.0.3", // 11 "0.1.0", // 12 - BREAKING CHANGE "0.3.3-rc.0", // 13 - BREAKING CHANGE "0.3.3-rc.1", // 14 "0.3.3", // 15 "0.3.3+build", // 16 "0.3.4-rc.1", // 17 "0.3.4", // 18 "0.4.0", // 19 - BREAKING CHANGE "1.0.0-rc", // 20 - BREAKING CHANGE "1.0.0", // 21 "1.0.0+build", // 22 "1.2.1-rc", // 23 "1.2.1", // 24 "1.2.1+build", // 25 "1.2.3-rc.2", // 26 "1.2.3-rc.2+build", // 27 "1.2.3", // 28 "1.2.3+build", // 29 "1.2.4", // 30 "1.3.0-rc.0+build", // 31 "1.3.0", // 32 "1.3.0+build", // 33 "1.3.1-rc.0", // 34 "1.3.1-rc.1", // 35 "1.3.1", // 36 "1.3.5", // 37 "2.0.0-rc", // 38 - BREAKING CHANGE "2.0.0-rc+build", // 39 "2.0.0", // 40 "2.0.0+build", // 41 "2.1.0-rc", // 42 "2.1.0-rc+build", // 43 "2.1.0", // 44 "2.1.0+build", // 45 "2.1.3-rc", // 46 "2.1.3", // 47 "2.3.0", // 48 "2.3.1", // 49 "3.0.0", // 50 - BREAKING CHANGE } func BenchmarkVersionParser(b *testing.B) { res := &Version{} for i := 0; i < b.N; i++ { for _, v := range list { res.raw = v res.bytes = []byte(v) _ = parse(res) } } // $ go test -benchmem -run=^$ -bench ^BenchmarkVersionParser$ go.bug.st/relaxed-semver // goos: linux // goarch: amd64 // pkg: go.bug.st/relaxed-semver // cpu: AMD Ryzen 5 3600 6-Core Processor // Results for v0.11.0: // BenchmarkVersionParser-12 188611 7715 ns/op 8557 B/op 51 allocs/op // Results for v0.12.0: \o/ // BenchmarkVersionParser-12 479626 3719 ns/op 616 B/op 51 allocs/op } func BenchmarkVersionComparator(b *testing.B) { b.StopTimer() vList := []*Version{} for _, in := range list { vList = append(vList, MustParse(in)) } l := len(vList) b.StartTimer() for i := 0; i < b.N; i++ { // cross compare all versions for x := 0; x < l; x++ { for y := 0; y < l; y++ { vList[x].CompareTo(vList[y]) } } } // $ go test -benchmem -run=^$ -bench ^BenchmarkVersionComparator$ go.bug.st/relaxed-semver -v // goos: linux // goarch: amd64 // pkg: go.bug.st/relaxed-semver // cpu: AMD Ryzen 5 3600 6-Core Processor // Results for v0.11.0: // BenchmarkVersionComparator-12 74793 17347 ns/op 0 B/op 0 allocs/op // Results for v0.12.0: :-D // BenchmarkVersionComparator-12 101772 11720 ns/op 0 B/op 0 allocs/op } relaxed-semver-0.15.0/binary.go000066400000000000000000000046111477074564600163610ustar00rootroot00000000000000// // Copyright 2018-2025 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // package semver import ( "bytes" "encoding/binary" ) func marshalByteArray(b []byte) []byte { l := len(b) res := make([]byte, l+4) binary.BigEndian.PutUint32(res, uint32(l)) copy(res[4:], b) return res } // MarshalBinary implements binary custom encoding func (v *Version) MarshalBinary() ([]byte, error) { // TODO could be preallocated without bytes.Buffer res := new(bytes.Buffer) intBuff := [4]byte{} _, _ = res.Write(marshalByteArray([]byte(v.raw))) binary.BigEndian.PutUint32(intBuff[:], uint32(v.major)) _, _ = res.Write(intBuff[:]) binary.BigEndian.PutUint32(intBuff[:], uint32(v.minor)) _, _ = res.Write(intBuff[:]) binary.BigEndian.PutUint32(intBuff[:], uint32(v.patch)) _, _ = res.Write(intBuff[:]) binary.BigEndian.PutUint32(intBuff[:], uint32(v.prerelease)) _, _ = res.Write(intBuff[:]) binary.BigEndian.PutUint32(intBuff[:], uint32(v.build)) _, _ = res.Write(intBuff[:]) return res.Bytes(), nil } func decodeArray(data []byte) ([]byte, []byte) { l, data := int(binary.BigEndian.Uint32(data)), data[4:] return data[:l], data[l:] } func decodeInt(data []byte) (int, []byte) { return int(binary.BigEndian.Uint32(data)), data[4:] } // UnmarshalJSON implements binary custom decoding func (v *Version) UnmarshalBinary(data []byte) error { var buff []byte buff, data = decodeArray(data) v.raw = string(buff) v.bytes = []byte(v.raw) v.major, data = decodeInt(data) v.minor, data = decodeInt(data) v.patch, data = decodeInt(data) v.prerelease, data = decodeInt(data) v.build, _ = decodeInt(data) return nil } // MarshalBinary implements encoding.BinaryMarshaler func (v *RelaxedVersion) MarshalBinary() ([]byte, error) { res := new(bytes.Buffer) if len(v.customversion) > 0 { _, _ = res.Write([]byte{0}) _, _ = res.Write(marshalByteArray(v.customversion)) return res.Bytes(), nil } res.Write([]byte{1}) d, _ := v.version.MarshalBinary() // can't fail _, _ = res.Write(d) return res.Bytes(), nil } // UnmarshalBinary implements encoding.BinaryUnmarshaler func (v *RelaxedVersion) UnmarshalBinary(data []byte) error { if data[0] == 0 { v.customversion, _ = decodeArray(data[1:]) v.version = nil return nil } v.customversion = nil v.version = &Version{} return v.version.UnmarshalBinary(data[1:]) } relaxed-semver-0.15.0/binary_test.go000066400000000000000000000043431477074564600174220ustar00rootroot00000000000000// // Copyright 2018-2025 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // package semver import ( "bytes" "encoding/gob" "fmt" "testing" "github.com/stretchr/testify/require" ) func TestGOBEncoderVersion(t *testing.T) { testVersion := "1.2.3-aaa.4.5.6+bbb.7.8.9" v, err := Parse(testVersion) require.NoError(t, err) dumpV := fmt.Sprintf("%v,%v,%v,%v,%v,%v", v.raw, v.major, v.minor, v.patch, v.prerelease, v.build) require.Equal(t, "1.2.3-aaa.4.5.6+bbb.7.8.9,1,3,5,15,25", dumpV) require.Equal(t, testVersion, v.String()) dataV := new(bytes.Buffer) err = gob.NewEncoder(dataV).Encode(v) require.NoError(t, err) var u Version err = gob.NewDecoder(dataV).Decode(&u) require.NoError(t, err) dumpU := fmt.Sprintf("%v,%v,%v,%v,%v,%v", v.raw, u.major, u.minor, u.patch, u.prerelease, u.build) require.Equal(t, dumpV, dumpU) require.Equal(t, testVersion, u.String()) { dataV := new(bytes.Buffer) dataU := new(bytes.Buffer) require.NoError(t, gob.NewEncoder(dataV).Encode(MustParse("1.6.2"))) require.NoError(t, gob.NewEncoder(dataU).Encode(MustParse("1.6.3"))) var v, u *Version require.NoError(t, gob.NewDecoder(dataV).Decode(&v)) require.NoError(t, gob.NewDecoder(dataU).Decode(&u)) require.True(t, u.GreaterThan(v)) } } func TestGOBEncoderRelaxedVersion(t *testing.T) { check := func(testVersion string) { v := ParseRelaxed(testVersion) dataV := new(bytes.Buffer) err := gob.NewEncoder(dataV).Encode(v) require.NoError(t, err) var u RelaxedVersion err = gob.NewDecoder(dataV).Decode(&u) require.NoError(t, err) require.Equal(t, testVersion, u.String()) } check("1.2.3-aaa.4.5.6+bbb.7.8.9") check("asdasdasd-1.2.3-aaa.4.5.6+bbb.7.8.9") } func BenchmarkBinaryDecoding(b *testing.B) { testVersion := "1.2.3-aaa.4.5.6+bbb.7.8.9" v := MustParse(testVersion) data, _ := v.MarshalBinary() var u Version for i := 0; i < b.N; i++ { _ = u.UnmarshalBinary(data) } } func BenchmarkBinaryDecodingRelaxed(b *testing.B) { testVersion := "1.2.3-aaa.4.5.6+bbb.7.8.9" v := ParseRelaxed(testVersion) data, _ := v.MarshalBinary() var u RelaxedVersion for i := 0; i < b.N; i++ { _ = u.UnmarshalBinary(data) } } relaxed-semver-0.15.0/charsets.go000066400000000000000000000023471477074564600167150ustar00rootroot00000000000000// // Copyright 2018-2025 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // package semver /* goos: linux goarch: amd64 pkg: go.bug.st/semver BenchmarkNumericArray-8 2000000000 1.71 ns/op BenchmarkNumericFunction-8 2000000000 1.23 ns/op = '0' && c <= '9' } func isIdentifier(c byte) bool { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '-' } func isVersionSeparator(c byte) bool { return c == '.' || c == '-' || c == '+' } var numeric [256]bool var identifier [256]bool var versionSeparator [256]bool func init() { for i := 0; i < 256; i++ { c := byte(i) if isNumeric(c) { numeric[c] = true } if isIdentifier(c) { identifier[c] = true } if isVersionSeparator(c) { versionSeparator[c] = true } } } relaxed-semver-0.15.0/charsets_test.go000066400000000000000000000022671477074564600177550ustar00rootroot00000000000000// // Copyright 2018-2025 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // package semver import ( "testing" ) var result int func BenchmarkNumericArray(b *testing.B) { count := 0 for n := 0; n < b.N; n++ { c := byte(n) if numeric[c] { count++ } } result = count } func BenchmarkNumericFunction(b *testing.B) { count := 0 for n := 0; n < b.N; n++ { c := byte(n) if isNumeric(c) { count++ } } result = count } func BenchmarkIdentifierArray(b *testing.B) { count := 0 for n := 0; n < b.N; n++ { c := byte(n) if identifier[c] { count++ } } result = count } func BenchmarkIdentifierFunction(b *testing.B) { count := 0 for n := 0; n < b.N; n++ { c := byte(n) if isIdentifier(c) { count++ } } result = count } func BenchmarkVersionSeparatorArray(b *testing.B) { count := 0 for n := 0; n < b.N; n++ { c := byte(n) if versionSeparator[c] { count++ } } result = count } func BenchmarkVersionSeparatorFunction(b *testing.B) { count := 0 for n := 0; n < b.N; n++ { c := byte(n) if isVersionSeparator(c) { count++ } } result = count } relaxed-semver-0.15.0/constraints.go000066400000000000000000000156001477074564600174440ustar00rootroot00000000000000// // Copyright 2018-2025 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // package semver import ( "fmt" "strings" ) // Constraint is a condition that a Version can match or not type Constraint interface { // Match returns true if the Version satisfies the condition Match(*Version) bool String() string } // ParseConstraint converts a string into a Constraint. The resulting Constraint // may be converted back to string using the String() method. func ParseConstraint(in string) (Constraint, error) { in = strings.TrimSpace(in) curr := 0 l := len(in) if l == 0 { return &True{}, nil } next := func() byte { if curr < l { curr++ return in[curr-1] } return 0 } skipSpace := func() { for curr < l && in[curr] == ' ' { curr++ } } peek := func() byte { if curr < l { return in[curr] } return 0 } version := func() (*Version, error) { start := curr for { n := peek() if !isIdentifier(n) && !isVersionSeparator(n) { if start == curr { return nil, fmt.Errorf("invalid version") } return Parse(in[start:curr]) } curr++ } } var terminal func() (Constraint, error) var constraint func() (Constraint, error) terminal = func() (Constraint, error) { skipSpace() switch next() { case '!': expr, err := terminal() if err != nil { return nil, err } return &Not{expr}, nil case '(': expr, err := constraint() if err != nil { return nil, err } skipSpace() if c := next(); c != ')' { return nil, fmt.Errorf("unexpected char at: %s", in[curr-1:]) } return expr, nil case '=': v, err := version() if err != nil { return nil, err } return &Equals{v}, nil case '^': v, err := version() if err != nil { return nil, err } return &CompatibleWith{v}, nil case '>': if peek() == '=' { next() v, err := version() if err != nil { return nil, err } return &GreaterThanOrEqual{v}, nil } else { v, err := version() if err != nil { return nil, err } return &GreaterThan{v}, nil } case '<': if peek() == '=' { next() v, err := version() if err != nil { return nil, err } return &LessThanOrEqual{v}, nil } else { v, err := version() if err != nil { return nil, err } return &LessThan{v}, nil } default: return nil, fmt.Errorf("unexpected char at: %s", in[curr-1:]) } } andExpr := func() (Constraint, error) { t1, err := terminal() if err != nil { return nil, err } stack := []Constraint{t1} for { skipSpace() if peek() != '&' { if len(stack) == 1 { return stack[0], nil } return &And{stack}, nil } next() if peek() != '&' { return nil, fmt.Errorf("unexpected char at: %s", in[curr-1:]) } next() t2, err := terminal() if err != nil { return nil, err } stack = append(stack, t2) } } constraint = func() (Constraint, error) { t1, err := andExpr() if err != nil { return nil, err } stack := []Constraint{t1} for { skipSpace() switch peek() { case '|': next() if peek() != '|' { return nil, fmt.Errorf("unexpected char at: %s", in[curr-1:]) } next() t2, err := andExpr() if err != nil { return nil, err } stack = append(stack, t2) case 0, ')': if len(stack) == 1 { return stack[0], nil } return &Or{stack}, nil default: return nil, fmt.Errorf("unexpected char at: %s", in[curr-1:]) } } } return constraint() } // True is the empty constraint type True struct { } // Match always return true func (t *True) Match(v *Version) bool { return true } func (t *True) String() string { return "" } // Equals is the equality (=) constraint type Equals struct { Version *Version } // Match returns true if v satisfies the condition func (eq *Equals) Match(v *Version) bool { return v.Equal(eq.Version) } func (eq *Equals) String() string { return "=" + eq.Version.String() } // LessThan is the less than (<) constraint type LessThan struct { Version *Version } // Match returns true if v satisfies the condition func (lt *LessThan) Match(v *Version) bool { return v.LessThan(lt.Version) } func (lt *LessThan) String() string { return "<" + lt.Version.String() } // LessThanOrEqual is the "less than or equal" (<=) constraint type LessThanOrEqual struct { Version *Version } // Match returns true if v satisfies the condition func (lte *LessThanOrEqual) Match(v *Version) bool { return v.LessThanOrEqual(lte.Version) } func (lte *LessThanOrEqual) String() string { return "<=" + lte.Version.String() } // GreaterThan is the "greater than" (>) constraint type GreaterThan struct { Version *Version } // Match returns true if v satisfies the condition func (gt *GreaterThan) Match(v *Version) bool { return v.GreaterThan(gt.Version) } func (gt *GreaterThan) String() string { return ">" + gt.Version.String() } // GreaterThanOrEqual is the "greater than or equal" (>=) constraint type GreaterThanOrEqual struct { Version *Version } // Match returns true if v satisfies the condition func (gte *GreaterThanOrEqual) Match(v *Version) bool { return v.GreaterThanOrEqual(gte.Version) } func (gte *GreaterThanOrEqual) String() string { return ">=" + gte.Version.String() } // CompatibleWith is the "compatible with" (^) constraint type CompatibleWith struct { Version *Version } // Match returns true if v satisfies the condition func (cw *CompatibleWith) Match(v *Version) bool { return cw.Version.CompatibleWith(v) } func (cw *CompatibleWith) String() string { return "^" + cw.Version.String() } // Or will match if ANY of the Operands Constraint will match type Or struct { Operands []Constraint } // Match returns true if v satisfies the condition func (or *Or) Match(v *Version) bool { for _, op := range or.Operands { if op.Match(v) { return true } } return false } func (or *Or) String() string { res := "(" for i, op := range or.Operands { if i > 0 { res += " || " } res += op.String() } res += ")" return res } // And will match if ALL the Operands Constraint will match type And struct { Operands []Constraint } // Match returns true if v satisfies the condition func (and *And) Match(v *Version) bool { for _, op := range and.Operands { if !op.Match(v) { return false } } return true } func (and *And) String() string { res := "(" for i, op := range and.Operands { if i > 0 { res += " && " } res += op.String() } res += ")" return res } // Not match if Operand does not match and viceversa type Not struct { Operand Constraint } // Match returns ture if v does NOT satisfies the condition func (not *Not) Match(v *Version) bool { return !not.Operand.Match(v) } func (not *Not) String() string { op := not.Operand.String() if op == "" || op[0] != '(' { return "!(" + op + ")" } return "!" + op } relaxed-semver-0.15.0/constraints_test.go000066400000000000000000000114011477074564600204760ustar00rootroot00000000000000// // Copyright 2018-2025 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // package semver import ( "fmt" "testing" "github.com/stretchr/testify/require" ) func TestConstraints(t *testing.T) { lt := &LessThan{v("1.3.0")} require.True(t, lt.Match(v("1.0.0"))) require.False(t, lt.Match(v("1.3.0"))) require.False(t, lt.Match(v("2.0.0"))) require.Equal(t, "<1.3.0", lt.String()) lte := &LessThanOrEqual{v("1.3.0")} require.True(t, lte.Match(v("1.0.0"))) require.True(t, lte.Match(v("1.3.0"))) require.False(t, lte.Match(v("2.0.0"))) require.Equal(t, "<=1.3.0", lte.String()) eq := &Equals{v("1.3.0")} require.False(t, eq.Match(v("1.0.0"))) require.True(t, eq.Match(v("1.3.0"))) require.False(t, eq.Match(v("2.0.0"))) require.Equal(t, "=1.3.0", eq.String()) gte := &GreaterThanOrEqual{v("1.3.0")} require.False(t, gte.Match(v("1.0.0"))) require.True(t, gte.Match(v("1.3.0"))) require.True(t, gte.Match(v("2.0.0"))) require.Equal(t, ">=1.3.0", gte.String()) gt := &GreaterThan{v("1.3.0")} require.False(t, gt.Match(v("1.0.0"))) require.False(t, gt.Match(v("1.3.0"))) require.True(t, gt.Match(v("2.0.0"))) require.Equal(t, ">1.3.0", gt.String()) tr := &True{} require.True(t, tr.Match(v("1.0.0"))) require.True(t, tr.Match(v("1.3.0"))) require.True(t, tr.Match(v("2.0.0"))) require.Equal(t, "", tr.String()) gt100 := &GreaterThan{v("1.0.0")} lte200 := &LessThanOrEqual{v("2.0.0")} and := &And{[]Constraint{gt100, lte200}} require.False(t, and.Match(v("0.9.0"))) require.False(t, and.Match(v("1.0.0"))) require.True(t, and.Match(v("1.3.0"))) require.True(t, and.Match(v("2.0.0"))) require.False(t, and.Match(v("2.1.0"))) require.Equal(t, "(>1.0.0 && <=2.0.0)", and.String()) gt200 := &GreaterThan{v("2.0.0")} lte100 := &LessThanOrEqual{v("1.0.0")} or := &Or{[]Constraint{gt200, lte100}} require.True(t, or.Match(v("0.9.0"))) require.True(t, or.Match(v("1.0.0"))) require.False(t, or.Match(v("1.3.0"))) require.False(t, or.Match(v("2.0.0"))) require.True(t, or.Match(v("2.1.0"))) require.Equal(t, "(>2.0.0 || <=1.0.0)", or.String()) notOr := &Not{or} require.False(t, notOr.Match(v("0.9.0"))) require.False(t, notOr.Match(v("1.0.0"))) require.True(t, notOr.Match(v("1.3.0"))) require.True(t, notOr.Match(v("2.0.0"))) require.False(t, notOr.Match(v("2.1.0"))) require.Equal(t, "!(>2.0.0 || <=1.0.0)", notOr.String()) comp := &CompatibleWith{v("1.3.4-rc.3")} require.False(t, comp.Match(v("1.2.3"))) require.False(t, comp.Match(v("1.3.2"))) require.False(t, comp.Match(v("1.2.3"))) require.False(t, comp.Match(v("1.3.4-rc.1"))) require.True(t, comp.Match(v("1.3.4-rc.5"))) require.True(t, comp.Match(v("1.3.4"))) require.True(t, comp.Match(v("1.3.6"))) require.True(t, comp.Match(v("1.4.0"))) require.True(t, comp.Match(v("1.4.5"))) require.True(t, comp.Match(v("1.4.5-rc.2"))) require.False(t, comp.Match(v("2.0.0"))) } func TestConstraintsParser(t *testing.T) { type goodStringTest struct { In, Out string } good := []goodStringTest{ {"", ""}, // always true {"=1.3.0", "=1.3.0"}, {" =1.3.0 ", "=1.3.0"}, {"=1.3.0 ", "=1.3.0"}, {" =1.3.0", "=1.3.0"}, {">=1.3.0", ">=1.3.0"}, {">1.3.0", ">1.3.0"}, {"<=1.3.0", "<=1.3.0"}, {"<1.3.0", "<1.3.0"}, {"^1.3.0", "^1.3.0"}, {" ^1.3.0", "^1.3.0"}, {"^1.3.0 ", "^1.3.0"}, {" ^1.3.0 ", "^1.3.0"}, {"(=1.4.0)", "=1.4.0"}, {"!(=1.4.0)", "!(=1.4.0)"}, {"!(((=1.4.0)))", "!(=1.4.0)"}, {"=1.2.4 && =1.3.0", "(=1.2.4 && =1.3.0)"}, {"=1.2.4 && ^1.3.0", "(=1.2.4 && ^1.3.0)"}, {"=1.2.4 && =1.3.0 && =1.2.0", "(=1.2.4 && =1.3.0 && =1.2.0)"}, {"=1.2.4 && =1.3.0 || =1.2.0", "((=1.2.4 && =1.3.0) || =1.2.0)"}, {"=1.2.4 || =1.3.0 && =1.2.0", "(=1.2.4 || (=1.3.0 && =1.2.0))"}, {"(=1.2.4 || =1.3.0) && =1.2.0", "((=1.2.4 || =1.3.0) && =1.2.0)"}, {"(=1.2.4 || !>1.3.0) && =1.2.0", "((=1.2.4 || !(>1.3.0)) && =1.2.0)"}, {"!(=1.2.4 || >1.3.0) && =1.2.0", "(!(=1.2.4 || >1.3.0) && =1.2.0)"}, } for i, test := range good { in := test.In out := test.Out t.Run(fmt.Sprintf("GoodString%03d", i), func(t *testing.T) { p, err := ParseConstraint(in) require.NoError(t, err, "error parsing %s", in) require.Equal(t, out, p.String()) fmt.Printf("'%s' parsed as %s\n", in, p.String()) }) } bad := []string{ "1.0.0", "= 1.0.0", ">= 1.0.0", "> 1.0.0", "<= 1.0.0", "< 1.0.0", ">>1.0.0", ">1.0.0 =2.0.0", ">1.0.0 &", "^1.1.1.1", "!1.0.0", ">1.0.0 && 2.0.0", ">1.0.0 | =2.0.0", "(>1.0.0 | =2.0.0)", "(>1.0.0 || =2.0.0", ">1.0.0 || 2.0.0", } for i, s := range bad { in := s t.Run(fmt.Sprintf("BadString%03d", i), func(t *testing.T) { p, err := ParseConstraint(in) require.Nil(t, p) require.Error(t, err) fmt.Printf("'%s' parse error: %s\n", in, err) }) } } relaxed-semver-0.15.0/debug.go000066400000000000000000000004061477074564600161610ustar00rootroot00000000000000// // Copyright 2018-2025 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // package semver var debug = noopDebug func noopDebug(format string, a ...interface{}) {} relaxed-semver-0.15.0/debug_test.go000066400000000000000000000011311477074564600172140ustar00rootroot00000000000000// // Copyright 2018-2025 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // package semver import ( "fmt" rtdebug "runtime/debug" "strings" "testing" ) func init() { debug = func(format string, a ...interface{}) { level := strings.Count(string(rtdebug.Stack()), "\n") for i := 0; i < level; i++ { fmt.Print(" ") } if a != nil { fmt.Printf(format, a...) fmt.Println() } else { fmt.Println(format) } } } func TestNoopDebug(t *testing.T) { noopDebug("just for coverage!") } relaxed-semver-0.15.0/doc.go000066400000000000000000000002731477074564600156420ustar00rootroot00000000000000// // Copyright 2018-2025 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // package semver relaxed-semver-0.15.0/fuzzer_test.go000066400000000000000000000152641477074564600174670ustar00rootroot00000000000000// // Copyright 2018-2025 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // package semver import ( "cmp" "fmt" "testing" "github.com/stretchr/testify/require" ) func FuzzParser(f *testing.F) { f.Add("0-") f.Add("9+V.1.3.1.1.1.1.1.9.1.3.1.1.1.1.1.9.1") f.Add("757202302126572447535523755625377572023021265724475355237556000065.") f.Add("4+57023021265724475355237556253000065.\x00") f.Add("0--xAbbbbeAcAcCECaBaAAaAfeAdEBe-xfCCfBBfAEdcfFebeDxfCCfBBfAEdcfFebeD") f.Add("9-V.1.3.1.1.1.1.1.9.1") f.Add("3.1.12") f.Add("4.474368202171") f.Add("2-V.1.3.1.1.1.1.1.V.1.3.1.1.1.1.1.9.1") f.Add("4.02") f.Add("0-057e-33304e-91094BfAEd6cf6379282317958222700xfCCfB5BfAEd6cfFebe7D") f.Add("0\x01") f.Add("9+3.1.4.0.1.-.1.R.1") f.Add("1.1.1\x0e") f.Add("4-V.t.t.t.e.e.V.t.e.V.t.e.e.V.t.e.V") f.Add("1.0x") f.Add("1+1.1.1") f.Add("0-0-.0.0") f.Add("4.4.4740683-m") f.Add("0-0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0") f.Add("0-57e-33304e-910946379282317068958222700xfC792823CfB5BfAEd6cfFebe7D") f.Add("9298023223876953125\v") f.Add("4.4.474068") f.Add("1+1.1.1.3") f.Add("1.1.13682021700570577572023021265724475355237556253000065.") f.Add("0.0.") f.Add("0-9+0") f.Add("9-3.1.9.1.\x81") f.Add("3-9625") f.Add("4.0-0") f.Add("1+1") f.Add("1+1.1") f.Add("390625") f.Add("1.98023223876953125\v") f.Add("1+1.1.") f.Add("1.1.-") f.Add("1.1.0-0") f.Add("3-02") f.Add("1+1.1\x88") f.Add("0-0.0.0.0.0.0.0.0.00") f.Add("0\xab") f.Add("5.-") f.Add("0-0.0") f.Add("1.1.0") f.Add("9+3.1.1.1.-.1.R.1") f.Add("1.1.9+3") f.Add("1.1368202170057057757202302126572447535523755625377572023021265724475355237556000065.") f.Add("3-a-after ") f.Add("9-3.3.1.9.1.") f.Add("1+19446951953614188\x00") f.Add("1+") f.Add("1.1\x0e") f.Add("0-0x.0") f.Add("2-V.V.t.e.V") f.Add("0-0.0.0.0.0.00") f.Add("1+1.fterafter ") f.Add("1++") f.Add("0-9+") f.Add("1.1.11.") f.Add("12") f.Add("0-57e-33304e-910946379282317958222700xfC792823CfB5BfAEd6cfFebe7D910946379282317958222700xfC792823CfB5BfAEd6cfFebe7D") f.Add("3-a-fterafter ") f.Add("3-") f.Add("3-<") f.Add("3-4.9.4") f.Add("1+57e-3330-57e-3346379282317068958222700xfC792823CfB5BfAEd6cfFebe7D") f.Add("0-1690993073057962936658730400845563-0xacC.-0xFe34b9") f.Add("0-057e-33304e-910946379282317958222700xfCCfB5BfAEd6cfFebe7D") f.Add("4-V.t.t.V.t.t.t.e.e.V.t.e.V.t.e.e.V.t.e.t.e.e.V.t.e.V.t.e.e.V.t.e.V") f.Add("@") f.Add("9-3.1.9.1.n") f.Add("0-0.0-.0.0.0.0.0.0.0.0.0.0.0.0.0.0.00") f.Add("9+3.1.1.1.R.1") f.Add("9-3.1.1.1.9.1.") f.Add("9+3.1.3.1.1.1.1.1.V.1.3.1.1.1.1.1.3.1.1.1.1.1.V.1.3.1.1.1.1.1.9.1.9.1") f.Add("3.1.1-2") f.Add("2-V.t.e.V.t.e.V") f.Add("3-9402004353104906.474368202171") f.Add("3.1.8081828384858687888912") f.Add("1+1.1.1.1.") f.Add("9+3.1.4.0.1.-.1.R.1.") f.Add("9+3.1.1.1.R.1.") f.Add("0-0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0-.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0") f.Add("0") f.Add("0-057e-33304e-910946379282317958222700xfC792823CfB5BfAEd6cfFebe7D") f.Add("34694469519536141888238489627838134765\x00") f.Add("1.1.1") f.Add("0-0.0.0.0.0.0.00") f.Add("1+1.1.1.") f.Add("9+3.1.1.1.-.1.R.1.") f.Add("0.0.0.") f.Add("9-3.1.1.1.9.1") f.Add("9+3.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.-.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0") f.Add("3-after ") f.Add("2-V.t.t.e.e.V.t.e.V") f.Add("0-0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0") f.Add("1.1.1368202170057057757202302126572447535523755625377572023021265724475355237556000065.") f.Add("") f.Add("1.1.0\x0e") f.Add("4.4743682021710570577572023021265724475355237556253000065.02") f.Add("01") f.Add("9+V.1.3.1.1.1.1.1.9.1") f.Add("0.0.0.0") f.Add("1.0") f.Add("2-V.1.3.1.1.1.1.1.V.1.3.1.1.1.1.1.3.1.1.1.1.1.V.1.3.1.1.1.1.1.9.1.9.1") f.Add("3-m") f.Add("9\v") f.Add("2-V.V.e.V") f.Fuzz(func(t *testing.T, in string) { // ParseRelaxed should always succeed r := ParseRelaxed(in) if r.String() != in { t.Fatalf("reserialized relaxed string != deserialized string (in=%v)", in) } if r.CompareTo(r) != 0 { t.Fatalf("compare != 0 while comparing with self (in=%v)", in) } // Parse should succeed only if the input is a valid semver v, err := Parse(in) if err != nil { if v != nil { t.Fatalf("v != nil on error (in=%v)", in) } return } if v.String() != in { t.Fatalf("reserialized string != deserialized string (in=%v)", in) } if v.CompareTo(v) != 0 { t.Fatalf("compare != 0 while comparing with self (in=%v)", in) } v.Normalize() if v.CompareTo(v) != 0 { t.Fatalf("compare != 0 while comparing with self (in=%v)", in) } }) } func FuzzComparators(f *testing.F) { f.Add("1.2.4", "0.0.1-rc.0") f.Add("1.3.0-rc.0+build", "0.0.1-rc.0+build") f.Add("1.3.0", "0.0.1-rc.1") f.Add("1.3.0+build", "0.0.1") f.Add("0.0.1+build", "0.0.1-rc.0") f.Add("0.0.2-rc.1", "0.0.1-rc.0+build") f.Add("0.0.2-rc.1+build", "0.0.1-rc.1") f.Add("0.0.2", "0.0.1") f.Add("0.0.2+build", "0.0.1+build") f.Add("0.0.3-rc.1", "0.0.2-rc.1") f.Add("0.0.3-rc.2", "0.0.2-rc.1+build") f.Add("0.0.3", "0.0.2") f.Add("0.1.0", "0.0.2+build") f.Add("0.3.3-rc.0", "0.0.3-rc.1") f.Add("0.3.3-rc.1", "0.0.3-rc.2") f.Add("0.3.3", "0.0.3") f.Add("0.3.3+build", "0.1.0") f.Add("0.3.4-rc.1", "0.3.3-rc.0") f.Add("0.3.4", "0.3.3-rc.1") f.Add("0.4.0", "0.3.3") f.Add("1.0.0-rc", "0.3.3+build") f.Add("1.0.0", "0.3.4-rc.1") f.Add("1.0.0+build", "0.3.4") f.Add("1.2.1-rc", "0.4.0") f.Add("1.2.1", "1.0.0-rc") f.Add("1.2.1+build", "1.0.0") f.Add("1.2.3-rc.2", "1.0.0+build") f.Add("1.2.3-rc.2+build", "1.2.1-rc") f.Add("1.2.3", "1.2.1") f.Add("1.2.3+build", "1.2.1+build") f.Add("1.2.4", "1.2.3-rc.2") f.Add("1.3.0-rc.0+build", "1.2.3-rc.2+build") f.Add("1.3.0", "1.2.3") f.Add("1.3.0+build", "1.2.3+build") f.Add("1.3.1-rc.0", "1.2.4") f.Add("1.3.1-rc.1", "1.3.0-rc.0+build") f.Add("1.3.1", "1.3.0") f.Add("1.3.5", "1.3.0+build") f.Add("2.0.0-rc", "1.3.1-rc.0") f.Add("2.0.0-rc+build", "1.3.1-rc.1") f.Add("2.0.0", "1.3.1") f.Add("2.0.0+build", "1.3.5") f.Add("2.1.0-rc", "2.0.0-rc") f.Add("2.1.0-rc+build", "2.0.0-rc+build") f.Add("2.1.0", "2.0.0") f.Add("2.1.0+build", "2.0.0+build") f.Add("2.1.3-rc", "2.1.0-rc") f.Add("2.1.3", "2.1.0-rc+build") f.Add("2.3.0", "2.1.0") f.Add("2.3.1", "2.1.0+build") f.Add("3.0.0", "2.1.3-rc") f.Fuzz(func(t *testing.T, a, b string) { va, err := Parse(a) if err != nil { return } vb, err := Parse(b) if err != nil { return } fmt.Println(va.SortableString(), vb.SortableString()) require.Equal(t, va.CompareTo(vb), cmp.Compare(va.SortableString(), vb.SortableString()), "Comparing: %s and %s", a, b) }) } relaxed-semver-0.15.0/go.mod000066400000000000000000000003421477074564600156510ustar00rootroot00000000000000module go.bug.st/relaxed-semver go 1.24 require ( github.com/stretchr/testify v1.10.0 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect ) relaxed-semver-0.15.0/go.sum000066400000000000000000000015631477074564600157040ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= relaxed-semver-0.15.0/json.go000066400000000000000000000023701477074564600160460ustar00rootroot00000000000000// // Copyright 2018-2025 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // package semver import ( "encoding/json" ) // MarshalJSON implements json.Marshaler func (v *Version) MarshalJSON() ([]byte, error) { return json.Marshal(v.String()) } // UnmarshalJSON implements json.Unmarshaler func (v *Version) UnmarshalJSON(data []byte) error { var versionString string if err := json.Unmarshal(data, &versionString); err != nil { return err } parsed, err := Parse(versionString) if err != nil { return err } v.raw = parsed.raw v.bytes = []byte(v.raw) v.major = parsed.major v.minor = parsed.minor v.patch = parsed.patch v.prerelease = parsed.prerelease v.build = parsed.build return nil } // MarshalJSON implements json.Marshaler func (v *RelaxedVersion) MarshalJSON() ([]byte, error) { return json.Marshal(v.String()) } // UnmarshalJSON implements json.Unmarshaler func (v *RelaxedVersion) UnmarshalJSON(data []byte) error { var versionString string if err := json.Unmarshal(data, &versionString); err != nil { return err } parsed := ParseRelaxed(versionString) v.customversion = parsed.customversion v.version = parsed.version return nil } relaxed-semver-0.15.0/json_test.go000066400000000000000000000040021477074564600170770ustar00rootroot00000000000000// // Copyright 2018-2025 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // package semver import ( "encoding/json" "fmt" "testing" "github.com/stretchr/testify/require" ) func TestJSONParseVersion(t *testing.T) { testVersion := "1.2.3-aaa.4.5.6+bbb.7.8.9" v, err := Parse(testVersion) require.NoError(t, err) data, err := json.Marshal(v) fmt.Println(string(data)) require.NoError(t, err) var u Version err = json.Unmarshal(data, &u) require.NoError(t, err) dump := fmt.Sprintf("%v,%v,%v,%v,%v,%v", u.raw, u.major, u.minor, u.patch, u.prerelease, u.build) require.Equal(t, "1.2.3-aaa.4.5.6+bbb.7.8.9,1,3,5,15,25", dump) require.Equal(t, testVersion, u.String()) err = json.Unmarshal([]byte(`"invalid"`), &u) require.Error(t, err) err = json.Unmarshal([]byte(`123`), &u) require.Error(t, err) require.NoError(t, json.Unmarshal([]byte(`"1.6.2"`), &v)) require.NoError(t, json.Unmarshal([]byte(`"1.6.3"`), &u)) require.True(t, u.GreaterThan(v)) } func TestJSONParseRelaxedVersion(t *testing.T) { testVersion := "1.2.3-aaa.4.5.6+bbb.7.8.9" v := ParseRelaxed(testVersion) data, err := json.Marshal(v) fmt.Println(string(data)) require.NoError(t, err) var u RelaxedVersion err = json.Unmarshal(data, &u) require.NoError(t, err) require.Equal(t, testVersion, u.String()) err = json.Unmarshal([]byte(`"invalid"`), &u) require.NoError(t, err) require.Equal(t, "invalid", u.String()) err = json.Unmarshal([]byte(`123`), &u) require.Error(t, err) } func BenchmarkJSONDecoding(b *testing.B) { testVersion := "1.2.3-aaa.4.5.6+bbb.7.8.9" v, _ := Parse(testVersion) data, _ := json.Marshal(v) var u Version for i := 0; i < b.N; i++ { _ = json.Unmarshal(data, &u) } } func BenchmarkJSONDecodingRelaxed(b *testing.B) { testVersion := "1.2.3-aaa.4.5.6+bbb.7.8.9" v := ParseRelaxed(testVersion) data, _ := json.Marshal(v) var u RelaxedVersion for i := 0; i < b.N; i++ { _ = json.Unmarshal(data, &u) } } relaxed-semver-0.15.0/list.go000066400000000000000000000006311477074564600160460ustar00rootroot00000000000000// // Copyright 2018-2025 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // package semver // List is a list of Versions type List []*Version func (l List) Len() int { return len(l) } func (l List) Swap(i, j int) { l[i], l[j] = l[j], l[i] } func (l List) Less(i, j int) bool { return l[i].LessThan(l[j]) } relaxed-semver-0.15.0/list_test.go000066400000000000000000000020031477074564600171000ustar00rootroot00000000000000// // Copyright 2018-2025 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // package semver import ( "sort" "testing" "github.com/stretchr/testify/require" ) func TestListSorting(t *testing.T) { list := []*Version{ MustParse("2.1.1"), MustParse("1.0.0-beta"), MustParse("1.0.0-beta.11"), MustParse("1.0.0-alpha.beta"), MustParse("1.0.0-alpha.1"), MustParse("1.0.0-rc.1"), MustParse("1.0.1"), MustParse("1.0.0"), MustParse("1.0.0-alpha"), MustParse("1.0.0-beta.2"), MustParse("1.1.1"), } ordered := []*Version{ MustParse("1.0.0-alpha"), MustParse("1.0.0-alpha.1"), MustParse("1.0.0-alpha.beta"), MustParse("1.0.0-beta"), MustParse("1.0.0-beta.2"), MustParse("1.0.0-beta.11"), MustParse("1.0.0-rc.1"), MustParse("1.0.0"), MustParse("1.0.1"), MustParse("1.1.1"), MustParse("2.1.1"), } sort.Sort(List(list)) for i := range list { require.True(t, list[i].Equal(ordered[i])) } } relaxed-semver-0.15.0/parser.go000066400000000000000000000154301477074564600163720ustar00rootroot00000000000000// // Copyright 2018-2025 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // package semver import ( "fmt" ) // MustParse parse a version string and panic if the parsing fails func MustParse(inVersion string) *Version { res, err := Parse(inVersion) if err != nil { panic(err) } return res } // Parse parse a version string func Parse(inVersion string) (*Version, error) { result := &Version{ raw: inVersion, bytes: []byte(inVersion), } if err := parse(result); err != nil { return nil, err } return result, nil } func parse(result *Version) error { // Setup parsing harness in := result.bytes inLen := len(in) currIdx := -1 var curr byte next := func() bool { currIdx = currIdx + 1 if currIdx == inLen { return false } curr = in[currIdx] return true } // 2. A normal version number MUST take the form X.Y.Z where X, Y, and Z // are non-negative integers, and MUST NOT contain leading zeroes. X is // the major version, Y is the minor version, and Z is the patch version. // Each element MUST increase numerically. // For instance: 1.9.0 -> 1.10.0 -> 1.11.0. // Parse major if !next() { return nil // empty version } if !numeric[curr] { return fmt.Errorf("no major version found") } if curr == '0' { result.major = 1 if !next() { result.minor = 1 result.patch = 1 result.prerelease = 1 result.build = 1 return nil } if numeric[curr] { return fmt.Errorf("major version must not be prefixed with zero") } if !versionSeparator[curr] { return fmt.Errorf("invalid major version separator '%c'", curr) } // Fallthrough and parse next element } else { for { if !next() { result.major = currIdx result.minor = currIdx result.patch = currIdx result.prerelease = currIdx result.build = currIdx return nil } if numeric[curr] { continue } if versionSeparator[curr] { result.major = currIdx result.minor = currIdx result.patch = currIdx result.prerelease = currIdx result.build = currIdx break } return fmt.Errorf("invalid major version separator '%c'", curr) } } // Parse minor if curr == '.' { if !next() || !numeric[curr] { return fmt.Errorf("no minor version found") } if curr == '0' { result.minor = currIdx + 1 if !next() { result.patch = currIdx result.prerelease = currIdx result.build = currIdx return nil } if numeric[curr] { return fmt.Errorf("minor version must not be prefixed with zero") } if !versionSeparator[curr] { return fmt.Errorf("invalid minor version separator '%c'", curr) } // Fallthrough and parse next element } else { for { if !next() { result.minor = currIdx result.patch = currIdx result.prerelease = currIdx result.build = currIdx return nil } if numeric[curr] { continue } if versionSeparator[curr] { result.minor = currIdx result.patch = currIdx result.prerelease = currIdx result.build = currIdx break } return fmt.Errorf("invalid minor version separator '%c'", curr) } } } else { result.minor = currIdx } // Parse patch if curr == '.' { if !next() || !numeric[curr] { return fmt.Errorf("no patch version found") } if curr == '0' { result.patch = currIdx + 1 if !next() { result.prerelease = currIdx result.build = currIdx return nil } if numeric[curr] { return fmt.Errorf("patch version must not be prefixed with zero") } if !versionSeparator[curr] { return fmt.Errorf("invalid patch version separator '%c'", curr) } // Fallthrough and parse next element } else { for { if !next() { result.patch = currIdx result.prerelease = currIdx result.build = currIdx return nil } if numeric[curr] { continue } if curr == '-' || curr == '+' { result.patch = currIdx result.prerelease = currIdx result.build = currIdx break } return fmt.Errorf("invalid patch version separator '%c'", curr) } } } else { result.patch = currIdx } // 9. A pre-release version MAY be denoted by appending a hyphen and a series // of dot separated identifiers immediately following the patch version. // Identifiers MUST comprise only ASCII alphanumerics and hyphen [0-9A-Za-z-]. // Identifiers MUST NOT be empty. // Numeric identifiers MUST NOT include leading zeroes. // Pre-release versions have a lower precedence than the associated normal // version. A pre-release version indicates that the version is unstable and // might not satisfy the intended compatibility requirements as denoted by // its associated normal version. // Examples: 1.0.0-alpha, 1.0.0-alpha.1, 1.0.0-0.3.7, 1.0.0-x.7.z.92. if curr == '-' { // Pre-release parsing prereleaseIdx := currIdx + 1 zeroPrefix := false alphaIdentifier := false for { if hasNext := next(); !hasNext || curr == '.' || curr == '+' { if prereleaseIdx == currIdx { return fmt.Errorf("empty prerelease not allowed") } if zeroPrefix && !alphaIdentifier && currIdx-prereleaseIdx > 1 { return fmt.Errorf("numeric prerelease must not be prefixed with zero") } result.prerelease = currIdx if !hasNext { result.build = currIdx return nil } if curr == '+' { break } // Multiple prerelease prereleaseIdx = currIdx + 1 zeroPrefix = false alphaIdentifier = false continue } if prereleaseIdx == currIdx && curr == '0' { zeroPrefix = true continue } if numeric[curr] { continue } if identifier[curr] { alphaIdentifier = true continue } return fmt.Errorf("invalid prerelease separator: '%c'", curr) } } else { result.prerelease = currIdx } // 10. Build metadata MAY be denoted by appending a plus sign and a series of // dot separated identifiers immediately following the patch or pre-release // version. // Identifiers MUST comprise only ASCII alphanumerics and hyphen [0-9A-Za-z-]. // Identifiers MUST NOT be empty. // Build metadata SHOULD be ignored when determining version precedence. Thus // two versions that differ only in the build metadata, have the same precedence. // Examples: 1.0.0-alpha+001, 1.0.0+20130313144700, 1.0.0-beta+exp.sha.5114f85. // Builds parsing buildIdx := currIdx + 1 if curr == '+' { for { if hasNext := next(); !hasNext || curr == '.' { if buildIdx == currIdx { return fmt.Errorf("empty build tag not allowed") } result.build = currIdx if !hasNext { return nil } // Multiple builds buildIdx = currIdx + 1 continue } if identifier[curr] { continue } return fmt.Errorf("invalid separator for builds: '%c'", curr) } } return fmt.Errorf("invalid separator: '%c'", curr) } relaxed-semver-0.15.0/parser_test.go000066400000000000000000000263121477074564600174320ustar00rootroot00000000000000// // Copyright 2018-2025 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // package semver import ( "fmt" "testing" "github.com/stretchr/testify/require" ) func (v *Version) majorString() string { return v.raw[:v.major] } func (v *Version) minorString() string { if v.minor > v.major { return v.raw[v.major+1 : v.minor] } return "" } func (v *Version) patchString() string { if v.patch > v.minor { return v.raw[v.minor+1 : v.patch] } return "" } func (v *Version) prereleaseString() string { if v.prerelease > v.patch { return v.raw[v.patch+1 : v.prerelease] } return "" } func (v *Version) buildString() string { if v.build > v.prerelease { return v.raw[v.prerelease+1 : v.build] } return "" } func TestParser(t *testing.T) { MustParse("").CompareTo(MustParse("0+aaa")) valid := func(in, normalized, expectedDump string) { v, err := Parse(in) require.NoError(t, err, "parsing '%s'", in) require.Equal(t, in, v.String(), "printing of '%s'", in) require.Equal(t, normalized, string(v.NormalizedString()), "normalized printing of '%s'", in) dump := fmt.Sprintf("%v,%v,%v,%v,%v,%v", v.raw, v.majorString(), v.minorString(), v.patchString(), v.prereleaseString(), v.buildString()) require.Equal(t, expectedDump, dump, "fields of parsed '%s'", in) fmt.Printf("%s -> %s\n", in, v.String()) v.Normalize() require.Equal(t, normalized, v.String(), "normalization of '%s'", in) vn, err := Parse(normalized) require.NoError(t, err) dump = fmt.Sprintf("%v,%v,%v,%v,%v,%v", v.raw, v.majorString(), v.minorString(), v.patchString(), v.prereleaseString(), v.buildString()) dumpNormalized := fmt.Sprintf("%v,%v,%v,%v,%v,%v", vn.raw, vn.majorString(), vn.minorString(), vn.patchString(), vn.prereleaseString(), vn.buildString()) require.Equal(t, dumpNormalized, dump) } invalid := func(in string) { v, err := Parse(in) require.Error(t, err, "parsing '%s'", in) require.Nil(t, v, "parsed '%s'", in) fmt.Printf("%s -> %s\n", in, err) } t.Run("NoMajorOrSingleMajorVariants", func(t *testing.T) { valid("", "0.0.0", ",,,,,") invalid("0.0.0.0") invalid("a") invalid(".") invalid("-ab") invalid("+ab") valid("0", "0.0.0", "0,0,,,,") valid("0.0.0", "0.0.0", "0.0.0,0,0,0,,") valid("1", "1.0.0", "1,1,,,,") valid("1.0.0", "1.0.0", "1.0.0,1,0,0,,") valid("14", "14.0.0", "14,14,,,,") valid("123456789123456789123456789", "123456789123456789123456789.0.0", "123456789123456789123456789,123456789123456789123456789,,,,") invalid("12ab") invalid("01") invalid("0ab") invalid(".1.1") invalid("1-") valid("1-0", "1.0.0-0", "1-0,1,,,0,") valid("1-pre", "1.0.0-pre", "1-pre,1,,,pre,") valid("1-pre.a", "1.0.0-pre.a", "1-pre.a,1,,,pre.a,") valid("1-pre.a.0", "1.0.0-pre.a.0", "1-pre.a.0,1,,,pre.a.0,") valid("1-pre.0.a", "1.0.0-pre.0.a", "1-pre.0.a,1,,,pre.0.a,") valid("1-pre.a.10", "1.0.0-pre.a.10", "1-pre.a.10,1,,,pre.a.10,") invalid("1-pre.a.01") invalid("1-pre.a..1") invalid("1-pre.a.01.1") invalid("1-pre.a.01*.1") valid("1+build3", "1.0.0+build3", "1+build3,1,,,,build3") invalid("1+build3+build2") valid("1+build3.123.001", "1.0.0+build3.123.001", "1+build3.123.001,1,,,,build3.123.001") invalid("1+build3.123..001") invalid("1+build3.123*.001") valid("1-0+build3", "1.0.0-0+build3", "1-0+build3,1,,,0,build3") valid("1-pre+build3", "1.0.0-pre+build3", "1-pre+build3,1,,,pre,build3") valid("1-pre.a+build3", "1.0.0-pre.a+build3", "1-pre.a+build3,1,,,pre.a,build3") valid("1-pre.a.10+build3", "1.0.0-pre.a.10+build3", "1-pre.a.10+build3,1,,,pre.a.10,build3") invalid("1-pre.a.01+build3") invalid("1-pre.a..1+build3") invalid("1-pre.a.01.1+build3") invalid("1-pre.a.01*.1+build3") valid("1-0+build3.123.001", "1.0.0-0+build3.123.001", "1-0+build3.123.001,1,,,0,build3.123.001") valid("1-pre+build3.123.001", "1.0.0-pre+build3.123.001", "1-pre+build3.123.001,1,,,pre,build3.123.001") valid("1-pre.a+build3.123.001", "1.0.0-pre.a+build3.123.001", "1-pre.a+build3.123.001,1,,,pre.a,build3.123.001") valid("1-pre.a.0+build3.123.001", "1.0.0-pre.a.0+build3.123.001", "1-pre.a.0+build3.123.001,1,,,pre.a.0,build3.123.001") valid("1-pre.0.a+build3.123.001", "1.0.0-pre.0.a+build3.123.001", "1-pre.0.a+build3.123.001,1,,,pre.0.a,build3.123.001") valid("1-pre.a.10+build3.123.001", "1.0.0-pre.a.10+build3.123.001", "1-pre.a.10+build3.123.001,1,,,pre.a.10,build3.123.001") invalid("1-pre.a.+build3.123.001") invalid("1-pre.a.01+build3.123.001") invalid("1-pre.a.01*+build3.123.001") }) t.Run("NoMinorOrSingleMinorVariants", func(t *testing.T) { invalid("1.") invalid("1.a") invalid("1..2") valid("1.2", "1.2.0", "1.2,1,2,,,") valid("1.0", "1.0.0", "1.0,1,0,,,") invalid("1.02") invalid("1.0ab") invalid("1.12ab") valid("1.123456789123456789123456789", "1.123456789123456789123456789.0", "1.123456789123456789123456789,1,123456789123456789123456789,,,") invalid("1.2-") valid("1.2-0", "1.2.0-0", "1.2-0,1,2,,0,") valid("1.2-pre", "1.2.0-pre", "1.2-pre,1,2,,pre,") valid("1.2-pre.a", "1.2.0-pre.a", "1.2-pre.a,1,2,,pre.a,") valid("1.2-pre.a.0", "1.2.0-pre.a.0", "1.2-pre.a.0,1,2,,pre.a.0,") valid("1.2-pre.0.a", "1.2.0-pre.0.a", "1.2-pre.0.a,1,2,,pre.0.a,") valid("1.2-pre.a.10", "1.2.0-pre.a.10", "1.2-pre.a.10,1,2,,pre.a.10,") invalid("1.2-pre.a.01") invalid("1.2-pre.a..1") invalid("1.2-pre.a.01.1") invalid("1.2-pre.a.01*.1") valid("1.2+build3", "1.2.0+build3", "1.2+build3,1,2,,,build3") valid("1.2-0+build3", "1.2.0-0+build3", "1.2-0+build3,1,2,,0,build3") invalid("1.2+build3+build2") valid("1.2+build3.123.001", "1.2.0+build3.123.001", "1.2+build3.123.001,1,2,,,build3.123.001") invalid("1.2+build3.123..001") invalid("1.2+build3.123*.001") valid("1.2-pre+build3", "1.2.0-pre+build3", "1.2-pre+build3,1,2,,pre,build3") valid("1.2-pre.a.0+build3", "1.2.0-pre.a.0+build3", "1.2-pre.a.0+build3,1,2,,pre.a.0,build3") valid("1.2-pre.0.a+build3", "1.2.0-pre.0.a+build3", "1.2-pre.0.a+build3,1,2,,pre.0.a,build3") valid("1.2-pre.a.10+build3", "1.2.0-pre.a.10+build3", "1.2-pre.a.10+build3,1,2,,pre.a.10,build3") valid("1.2-pre.a+build3", "1.2.0-pre.a+build3", "1.2-pre.a+build3,1,2,,pre.a,build3") valid("1.2-pre.a.10+build3", "1.2.0-pre.a.10+build3", "1.2-pre.a.10+build3,1,2,,pre.a.10,build3") invalid("1.2-pre.a.01+build3") invalid("1.2-pre.a..1+build3") invalid("1.2-pre.a.01.1+build3") invalid("1.2-pre.a.01*.1+build3") valid("1.2-0+build3.123.001", "1.2.0-0+build3.123.001", "1.2-0+build3.123.001,1,2,,0,build3.123.001") valid("1.2-pre+build3.123.001", "1.2.0-pre+build3.123.001", "1.2-pre+build3.123.001,1,2,,pre,build3.123.001") valid("1.2-pre.a+build3.123.001", "1.2.0-pre.a+build3.123.001", "1.2-pre.a+build3.123.001,1,2,,pre.a,build3.123.001") valid("1.2-pre.a.0+build3.123.001", "1.2.0-pre.a.0+build3.123.001", "1.2-pre.a.0+build3.123.001,1,2,,pre.a.0,build3.123.001") valid("1.2-pre.0.a+build3.123.001", "1.2.0-pre.0.a+build3.123.001", "1.2-pre.0.a+build3.123.001,1,2,,pre.0.a,build3.123.001") valid("1.2-pre.a.10+build3.123.001", "1.2.0-pre.a.10+build3.123.001", "1.2-pre.a.10+build3.123.001,1,2,,pre.a.10,build3.123.001") valid("1.2-pre.a.10+build3.123.001", "1.2.0-pre.a.10+build3.123.001", "1.2-pre.a.10+build3.123.001,1,2,,pre.a.10,build3.123.001") invalid("1.2-pre.a.+build3.123.001") invalid("1.2-pre.a.01+build3.123.001") invalid("1.2-pre.a.01*+build3.123.001") }) t.Run("FullVariants", func(t *testing.T) { invalid("1.2.a") invalid("1.2.") valid("1.2.3", "1.2.3", "1.2.3,1,2,3,,") valid("1.2.0", "1.2.0", "1.2.0,1,2,0,,") invalid("1.2.03") invalid("1.2.0ab") invalid("1.2.34ab") valid("1.2.123456789123456789123456789", "1.2.123456789123456789123456789", "1.2.123456789123456789123456789,1,2,123456789123456789123456789,,") invalid("1.2.3-") valid("1.2.3-0", "1.2.3-0", "1.2.3-0,1,2,3,0,") valid("1.2.3-pre", "1.2.3-pre", "1.2.3-pre,1,2,3,pre,") valid("1.2.3-pre.a", "1.2.3-pre.a", "1.2.3-pre.a,1,2,3,pre.a,") valid("1.2.3-pre.a.0", "1.2.3-pre.a.0", "1.2.3-pre.a.0,1,2,3,pre.a.0,") valid("1.2.3-pre.0.a", "1.2.3-pre.0.a", "1.2.3-pre.0.a,1,2,3,pre.0.a,") valid("1.2.3-pre.a.10", "1.2.3-pre.a.10", "1.2.3-pre.a.10,1,2,3,pre.a.10,") valid("1.2.3-pre.a.10", "1.2.3-pre.a.10", "1.2.3-pre.a.10,1,2,3,pre.a.10,") invalid("1.2.3-pre.a.01") invalid("1.2.3-pre.a..1") invalid("1.2.3-pre.a.01.1") invalid("1.2.3-pre.a.01*.1") valid("1.2.3+build3", "1.2.3+build3", "1.2.3+build3,1,2,3,,build3") invalid("1.2.3+build3+build2") valid("1.2.3+build3.123.001", "1.2.3+build3.123.001", "1.2.3+build3.123.001,1,2,3,,build3.123.001") invalid("1.2.3+build3.123..001") invalid("1.2.3+build3.123*.001") valid("1.2.3-0+build3", "1.2.3-0+build3", "1.2.3-0+build3,1,2,3,0,build3") valid("1.2.3-pre+build3", "1.2.3-pre+build3", "1.2.3-pre+build3,1,2,3,pre,build3") valid("1.2.3-pre.a+build3", "1.2.3-pre.a+build3", "1.2.3-pre.a+build3,1,2,3,pre.a,build3") valid("1.2.3-pre.a.0+build3", "1.2.3-pre.a.0+build3", "1.2.3-pre.a.0+build3,1,2,3,pre.a.0,build3") valid("1.2.3-pre.0.a+build3", "1.2.3-pre.0.a+build3", "1.2.3-pre.0.a+build3,1,2,3,pre.0.a,build3") valid("1.2.3-pre.a.10+build3", "1.2.3-pre.a.10+build3", "1.2.3-pre.a.10+build3,1,2,3,pre.a.10,build3") valid("1.2.3-pre.a.10+build3", "1.2.3-pre.a.10+build3", "1.2.3-pre.a.10+build3,1,2,3,pre.a.10,build3") invalid("1.2.3-pre.a.01+build3") invalid("1.2.3-pre.a..1+build3") invalid("1.2.3-pre.a.01.1+build3") invalid("1.2.3-pre.a.01*.1+build3") valid("1.2.3-0+build3.123.001", "1.2.3-0+build3.123.001", "1.2.3-0+build3.123.001,1,2,3,0,build3.123.001") valid("1.2.3-pre+build3.123.001", "1.2.3-pre+build3.123.001", "1.2.3-pre+build3.123.001,1,2,3,pre,build3.123.001") valid("1.2.3-pre.a+build3.123.001", "1.2.3-pre.a+build3.123.001", "1.2.3-pre.a+build3.123.001,1,2,3,pre.a,build3.123.001") valid("1.2.3-pre.a.10+build3.123.001", "1.2.3-pre.a.10+build3.123.001", "1.2.3-pre.a.10+build3.123.001,1,2,3,pre.a.10,build3.123.001") invalid("1.2.3-pre.a.+build3.123.001") invalid("1.2.3-pre.a.01+build3.123.001") invalid("1.2.3-pre.a.01*+build3.123.001") invalid("1.2.3.4") invalid("1.2.3.") }) t.Run("AbsurdlyWeirdVersions", func(t *testing.T) { valid("1.2.3-pre.a-10.20.c-30", "1.2.3-pre.a-10.20.c-30", "1.2.3-pre.a-10.20.c-30,1,2,3,pre.a-10.20.c-30,") valid("1.2.3--1-.23.1", "1.2.3--1-.23.1", "1.2.3--1-.23.1,1,2,3,-1-.23.1,") }) } func TestNilVersionStringOutput(t *testing.T) { var nilVersion *Version require.Equal(t, "", nilVersion.String()) require.Equal(t, "", string(nilVersion.NormalizedString())) } func TestParseRelaxed(t *testing.T) { bad := ParseRelaxed("bad") require.Nil(t, bad.version) require.Equal(t, []byte("bad"), bad.customversion) require.Equal(t, "bad", bad.String()) good := ParseRelaxed("1.2.3-pre.a.10+build3.123.001") require.Nil(t, good.customversion) require.Equal(t, "1.2.3-pre.a.10+build3.123.001", good.version.String()) require.Equal(t, "1.2.3-pre.a.10+build3.123.001", good.String()) } func ExampleParseRelaxed() { WarnInvalidVersionWhenParsingRelaxed = true ParseRelaxed("bad") WarnInvalidVersionWhenParsingRelaxed = false // Output: // WARNING invalid semver version bad: no major version found } func TestMustParse(t *testing.T) { require.NotPanics(t, func() { MustParse("1.2.3") }) require.Panics(t, func() { MustParse("bad") }) } relaxed-semver-0.15.0/relaxed_version.go000066400000000000000000000116231477074564600202670ustar00rootroot00000000000000// // Copyright 2018-2025 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // package semver import "fmt" // RelaxedVersion allows any possible version string. If the version does not comply // with semantic versioning it is saved as-is and only Equal comparison will match. type RelaxedVersion struct { customversion []byte version *Version } // WarnInvalidVersionWhenParsingRelaxed must be set to true to show warnings while // parsing RelaxedVersion if an invalid semver string is found. This allows a soft // transition to strict semver var WarnInvalidVersionWhenParsingRelaxed = false // ParseRelaxed parse a RelaxedVersion func ParseRelaxed(in string) *RelaxedVersion { v, err := Parse(in) if err == nil { return &RelaxedVersion{version: v} } if WarnInvalidVersionWhenParsingRelaxed { fmt.Printf("WARNING invalid semver version %s: %s\n", in, err) } return &RelaxedVersion{customversion: []byte(in[:])} } func (v *RelaxedVersion) String() string { if v == nil { return "" } if v.version != nil { return v.version.String() } return string(v.customversion) } // NormalizedString return a string representation of the version that is // normalized (always have a major, minor and patch version when semver compliant). // This is useful to be used in maps and other places where the version is used as a key. func (v *RelaxedVersion) NormalizedString() NormalizedString { if v == nil { return "" } if v.version != nil { return v.version.NormalizedString() } return NormalizedString(v.customversion) } // CompareTo compares the RelaxedVersion with the one passed as parameter. // Returns -1, 0 or 1 if the version is respectively less than, equal // or greater than the compared Version func (v *RelaxedVersion) CompareTo(u *RelaxedVersion) int { if v.version == nil && u.version == nil { return compareAlpha(v.customversion, u.customversion) } if v.version == nil { return -1 } if u.version == nil { return 1 } return v.version.CompareTo(u.version) } // LessThan returns true if the RelaxedVersion is less than the RelaxedVersion passed as parameter func (v *RelaxedVersion) LessThan(u *RelaxedVersion) bool { return v.CompareTo(u) < 0 } // LessThanOrEqual returns true if the RelaxedVersion is less than or equal to the RelaxedVersion passed as parameter func (v *RelaxedVersion) LessThanOrEqual(u *RelaxedVersion) bool { return v.CompareTo(u) <= 0 } // Equal returns true if the RelaxedVersion is equal to the RelaxedVersion passed as parameter func (v *RelaxedVersion) Equal(u *RelaxedVersion) bool { return v.CompareTo(u) == 0 } // GreaterThan returns true if the RelaxedVersion is greater than the RelaxedVersion passed as parameter func (v *RelaxedVersion) GreaterThan(u *RelaxedVersion) bool { return v.CompareTo(u) > 0 } // GreaterThanOrEqual returns true if the RelaxedVersion is greater than or equal to the RelaxedVersion passed as parameter func (v *RelaxedVersion) GreaterThanOrEqual(u *RelaxedVersion) bool { return v.CompareTo(u) >= 0 } // CompatibleWith returns true if the RelaxedVersion is compatible with the RelaxedVersion passed as paramater func (v *RelaxedVersion) CompatibleWith(u *RelaxedVersion) bool { if v.version != nil && u.version != nil { return v.version.CompatibleWith(u.version) } return v.Equal(u) } // SortableString returns the version encoded as a string that when compared // with alphanumeric ordering it respects the original semver ordering: // // (v1.SortableString() < v2.SortableString()) == v1.LessThan(v2) // cmp.Compare[string](v1.SortableString(), v2.SortableString()) == v1.CompareTo(v2) // // This may turn out useful when the version is saved in a database or is // introduced in a system that doesn't support semver ordering. func (v *RelaxedVersion) SortableString() string { if v.version != nil { return v.version.SortableString() } return ":" + string(v.customversion) } // IsPrerelease returns true if the version is valid semver and has a pre-release part // otherwise it returns false. func (v *RelaxedVersion) IsPrerelease() bool { if v.version == nil { return false } return v.version.IsPrerelease() } // Prerelease returns the pre-release part of the version if the version is valid semver // otherwise it returns an empty string. func (v *RelaxedVersion) Prerelease() string { if v.version == nil { return "" } return v.version.Prerelease() } // HasBuildMetadata returns true if the version is valid semver and has a build metadata part // otherwise it returns false. func (v *RelaxedVersion) HasBuildMetadata() bool { if v.version == nil { return false } return v.version.HasBuildMetadata() } // BuildMetadata returns the build metadata part of the version if the version is valid semver // otherwise it returns an empty string. func (v *RelaxedVersion) BuildMetadata() string { if v.version == nil { return "" } return v.version.BuildMetadata() } relaxed-semver-0.15.0/relaxed_version_test.go000066400000000000000000000124271477074564600213310ustar00rootroot00000000000000// // Copyright 2018-2025 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // package semver import ( "cmp" "fmt" "testing" "github.com/stretchr/testify/require" ) func TestRelaxedVersionComparator(t *testing.T) { sign := map[int]string{1: ">", 0: "=", -1: "<"} ascending := func(list ...*RelaxedVersion) { for i := range list[0 : len(list)-1] { { a := list[i] b := list[i+1] comp := a.CompareTo(b) fmt.Printf("%s %s %s\n", a, sign[comp], b) require.Equal(t, comp, -1) require.True(t, a.LessThan(b)) require.True(t, a.LessThanOrEqual(b)) require.False(t, a.Equal(b)) require.False(t, a.GreaterThanOrEqual(b)) require.False(t, a.GreaterThan(b)) comp = b.CompareTo(a) fmt.Printf("%s %s %s\n", b, sign[comp], a) require.Equal(t, comp, 1) require.False(t, b.LessThan(a)) require.False(t, b.LessThanOrEqual(a)) require.False(t, b.Equal(a)) require.True(t, b.GreaterThanOrEqual(a)) require.True(t, b.GreaterThan(a)) } { a := list[i].SortableString() b := list[i+1].SortableString() comp := cmp.Compare(a, b) fmt.Printf("%s %s %s\n", a, sign[comp], b) require.Equal(t, comp, -1) require.True(t, a < b) require.True(t, a <= b) require.False(t, a == b) require.False(t, a >= b) require.False(t, a > b) comp = cmp.Compare(b, a) fmt.Printf("%s %s %s\n", b, sign[comp], a) require.Equal(t, comp, 1) require.False(t, b < a) require.False(t, b <= a) require.False(t, b == a) require.True(t, b >= a) require.True(t, b > a) } } } equal := func(list ...*RelaxedVersion) { for _, a := range list { for _, b := range list { { comp := a.CompareTo(b) fmt.Printf("%s %s %s\n", a, sign[comp], b) require.Equal(t, comp, 0) require.False(t, a.LessThan(b)) require.True(t, a.LessThanOrEqual(b)) require.True(t, a.Equal(b)) require.True(t, a.GreaterThanOrEqual(b)) require.False(t, a.GreaterThan(b)) comp = b.CompareTo(a) fmt.Printf("%s %s %s\n", b, sign[comp], a) require.Equal(t, comp, 0) require.False(t, b.LessThan(a)) require.True(t, b.LessThanOrEqual(a)) require.True(t, b.Equal(a)) require.True(t, b.GreaterThanOrEqual(a)) require.False(t, b.GreaterThan(a)) } { a := a.SortableString() b := b.SortableString() comp := cmp.Compare(a, b) fmt.Printf("%s %s %s\n", a, sign[comp], b) require.Equal(t, comp, 0) require.False(t, a < b) require.True(t, a <= b) require.True(t, a == b) require.True(t, a >= b) require.False(t, a > b) comp = cmp.Compare(b, a) fmt.Printf("%s %s %s\n", b, sign[comp], a) require.Equal(t, comp, 0) require.False(t, b < a) require.True(t, b <= a) require.True(t, b == a) require.True(t, b >= a) require.False(t, b > a) } } } } ascending( ParseRelaxed("6_2"), ParseRelaxed("alpha"), ParseRelaxed("beta"), ParseRelaxed("gamma"), ParseRelaxed("1.0.0-alpha"), ParseRelaxed("1.0.0-alpha.1"), ParseRelaxed("1.0.0-alpha.beta"), ParseRelaxed("1.0.0-beta"), ParseRelaxed("1.0.0-beta.2"), ParseRelaxed("1.0.0-beta.11"), ParseRelaxed("1.0.0-rc.1"), ParseRelaxed("1.0.0"), ParseRelaxed("1.0.1"), ParseRelaxed("1.1.1"), ParseRelaxed("2.1.1"), ) equal( ParseRelaxed(""), ParseRelaxed("0"), ParseRelaxed("0.0"), ParseRelaxed("0.0.0"), ParseRelaxed("0+aaa"), ParseRelaxed("0.0+aaa"), ParseRelaxed("0.0.0+aaa"), ParseRelaxed("0+aaa.bbb"), ParseRelaxed("0.0+aaa.bbb"), ParseRelaxed("0.0.0+aaa.bbb"), ) } func TestRelaxedCompatibleWith(t *testing.T) { inv := ParseRelaxed("invalid-semver") inv2 := ParseRelaxed("invalid-semver-2") v145 := ParseRelaxed("1.4.5") v152 := ParseRelaxed("1.5.2") v213 := ParseRelaxed("2.1.3") require.True(t, inv.CompatibleWith(inv)) require.False(t, inv.CompatibleWith(inv2)) require.False(t, inv.CompatibleWith(v145)) require.False(t, inv.CompatibleWith(v152)) require.False(t, inv.CompatibleWith(v213)) require.False(t, inv2.CompatibleWith(inv)) require.True(t, inv2.CompatibleWith(inv2)) require.False(t, inv2.CompatibleWith(v145)) require.False(t, inv2.CompatibleWith(v152)) require.False(t, inv2.CompatibleWith(v213)) require.False(t, v145.CompatibleWith(inv)) require.False(t, v145.CompatibleWith(inv2)) require.True(t, v145.CompatibleWith(v145)) require.True(t, v145.CompatibleWith(v152)) require.False(t, v145.CompatibleWith(v213)) require.False(t, v152.CompatibleWith(inv)) require.False(t, v152.CompatibleWith(inv2)) require.False(t, v152.CompatibleWith(v145)) require.True(t, v152.CompatibleWith(v152)) require.False(t, v152.CompatibleWith(v213)) require.False(t, v213.CompatibleWith(inv)) require.False(t, v213.CompatibleWith(inv2)) require.False(t, v213.CompatibleWith(v145)) require.False(t, v213.CompatibleWith(v152)) require.True(t, v213.CompatibleWith(v213)) } func TestNilRelaxedVersionString(t *testing.T) { var nilVersion *RelaxedVersion require.Equal(t, "", nilVersion.String()) require.Equal(t, "", string(nilVersion.NormalizedString())) require.Equal(t, "1.0.0", string(ParseRelaxed("1.0.0").NormalizedString())) require.Equal(t, "invalid-semver", string(ParseRelaxed("invalid-semver").NormalizedString())) } relaxed-semver-0.15.0/resolver.go000066400000000000000000000122571477074564600167430ustar00rootroot00000000000000// // Copyright 2018-2025 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // package semver import "sort" // Dependency represents a dependency, it must provide methods to return Name and Constraints type Dependency interface { GetName() string GetConstraint() Constraint } // Release represents a release, it must provide methods to return Name, Version and Dependencies type Release[D Dependency] interface { GetName() string GetVersion() *Version GetDependencies() []D } // Releases is a list of Release of the same package (all releases with // the same Name but different Version) type Releases[R Release[D], D Dependency] []R // FilterBy return a subset of the Releases matching the provided Constraint func (set Releases[R, D]) FilterBy(c Constraint) Releases[R, D] { var res Releases[R, D] for _, r := range set { if c.Match(r.GetVersion()) { res = append(res, r) } } return res } // SortDescent sort the Releases in this set in descending order (the lastest // release is the first) func (set Releases[R, D]) SortDescent() { sort.Slice(set, func(i, j int) bool { return set[i].GetVersion().GreaterThan(set[j].GetVersion()) }) } // Resolver is a container with references to all Releases to consider for // dependency resolution type Resolver[R Release[D], D Dependency] struct { releases map[string]Releases[R, D] // resolver state solution map[string]R depsToProcess []D problematicDeps map[dependencyHash]int } // NewResolver creates a new archive func NewResolver[R Release[D], D Dependency]() *Resolver[R, D] { return &Resolver[R, D]{ releases: map[string]Releases[R, D]{}, } } // AddRelease adds a release to this archive func (ar *Resolver[R, D]) AddRelease(rel R) { relName := rel.GetName() ar.releases[relName] = append(ar.releases[relName], rel) } // AddReleases adds all the releases to this archive func (ar *Resolver[R, D]) AddReleases(rels ...R) { for _, rel := range rels { relName := rel.GetName() ar.releases[relName] = append(ar.releases[relName], rel) } } // Resolve will try to depp-resolve dependencies from the Release passed as // arguent using a backtracking algorithm. This function is NOT thread-safe. func (ar *Resolver[R, D]) Resolve(release R) Releases[R, D] { // Initial empty state of the resolver ar.solution = map[string]R{} ar.depsToProcess = []D{} ar.problematicDeps = map[dependencyHash]int{} // Check if the release is in the archive if len(ar.releases[release.GetName()].FilterBy(&Equals{Version: release.GetVersion()})) == 0 { return nil } // Add the requested release to the solution and proceed // with the dependencies resolution ar.solution[release.GetName()] = release ar.depsToProcess = append(ar.depsToProcess, release.GetDependencies()...) return ar.resolve() } type dependencyHash string func hashDependency[D Dependency](dep D) dependencyHash { return dependencyHash(dep.GetName() + "/" + dep.GetConstraint().String()) } func (ar *Resolver[R, D]) resolve() Releases[R, D] { debug("deps to process: %s", ar.depsToProcess) if len(ar.depsToProcess) == 0 { debug("All dependencies have been resolved.") var res Releases[R, D] for _, v := range ar.solution { res = append(res, v) } return res } // Pick the first dependency in the deps to process dep := ar.depsToProcess[0] depName := dep.GetName() debug("Considering next dep: %s", depName) // If a release is already picked in the solution check if it match the dep if existingRelease, has := ar.solution[depName]; has { if dep.GetConstraint().Match(existingRelease.GetVersion()) { debug("%s already in solution and matching", existingRelease) oldDepsToProcess := ar.depsToProcess ar.depsToProcess = ar.depsToProcess[1:] if res := ar.resolve(); res != nil { return res } ar.depsToProcess = oldDepsToProcess return nil } debug("%s already in solution do not match... rollingback", existingRelease) return nil } // Otherwise start backtracking the dependency releases := ar.releases[depName].FilterBy(dep.GetConstraint()) // Consider the latest versions first releases.SortDescent() debug("releases matching criteria: %s", releases) backtracking_loop: for _, release := range releases { releaseDeps := release.GetDependencies() debug("try with %s %s", release, releaseDeps) for _, releaseDep := range releaseDeps { if _, ok := ar.releases[releaseDep.GetName()]; !ok { debug("%s did not work, becuase his dependency %s does not exists", release, releaseDep.GetName()) continue backtracking_loop } } ar.solution[depName] = release oldDepsToProcess := ar.depsToProcess ar.depsToProcess = append(ar.depsToProcess[1:], releaseDeps...) // bubble up problematics deps so they are processed first sort.Slice(ar.depsToProcess, func(i, j int) bool { ci := hashDependency(ar.depsToProcess[i]) cj := hashDependency(ar.depsToProcess[j]) return ar.problematicDeps[ci] > ar.problematicDeps[cj] }) if res := ar.resolve(); res != nil { return res } ar.depsToProcess = oldDepsToProcess debug("%s did not work...", release) delete(ar.solution, depName) } ar.problematicDeps[hashDependency(dep)]++ return nil } relaxed-semver-0.15.0/resolver_test.go000066400000000000000000000115001477074564600177700ustar00rootroot00000000000000// // Copyright 2018-2025 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // package semver import ( "fmt" "testing" "time" "github.com/stretchr/testify/require" ) type customDep struct { name string cond Constraint } // GetName return the name of the dependency (implements the Dependency interface) func (c *customDep) GetName() string { return c.name } // GetConstraint return the version contraints of the dependency (implements the Dependency interface) func (c *customDep) GetConstraint() Constraint { return c.cond } func (c *customDep) String() string { return c.name + c.cond.String() } type customRel struct { name string vers *Version deps []*customDep } // GetName return the name of the release (implements the Release interface) func (r *customRel) GetName() string { return r.name } // GetVersion return the version of the release (implements the Release interface) func (r *customRel) GetVersion() *Version { return r.vers } func (r *customRel) GetDependencies() []*customDep { return r.deps } func (r *customRel) String() string { return r.name + "@" + r.vers.String() } func d(dep string) *customDep { name := dep[0:1] cond, err := ParseConstraint(dep[1:]) if err != nil { panic("invalid operator in dep: " + dep + " (" + err.Error() + ")") } return &customDep{name: name, cond: cond} } func deps(deps ...string) []*customDep { var res []*customDep for _, dep := range deps { res = append(res, d(dep)) } return res } func rel(name, ver string, deps []*customDep) *customRel { return &customRel{name: name, vers: v(ver), deps: deps} } func TestResolver(t *testing.T) { a100 := rel("A", "1.0.0", deps("B>=1.2.0", "C>=2.0.0")) a110 := rel("A", "1.1.0", deps("B=1.2.0", "C>=2.0.0")) a111 := rel("A", "1.1.1", deps("B", "C=1.1.1")) a120 := rel("A", "1.2.0", deps("B=1.2.0", "C>2.0.0")) a121 := rel("A", "1.2.1", deps("B", "C", "G", "H", "I", "E=1.0.1")) b131 := rel("B", "1.3.1", deps("C<2.0.0")) b130 := rel("B", "1.3.0", deps()) b121 := rel("B", "1.2.1", deps()) b120 := rel("B", "1.2.0", deps()) b111 := rel("B", "1.1.1", deps()) b110 := rel("B", "1.1.0", deps()) b100 := rel("B", "1.0.0", deps()) c200 := rel("C", "2.0.0", deps()) c120 := rel("C", "1.2.0", deps()) c111 := rel("C", "1.1.1", deps("B=1.1.1")) c110 := rel("C", "1.1.0", deps()) c102 := rel("C", "1.0.2", deps()) c101 := rel("C", "1.0.1", deps()) c100 := rel("C", "1.0.0", deps()) c021 := rel("C", "0.2.1", deps()) c020 := rel("C", "0.2.0", deps()) c010 := rel("C", "0.1.0", deps("D")) d100 := rel("D", "1.0.0", deps()) d120 := rel("D", "1.2.0", deps("E")) e100 := rel("E", "1.0.0", deps()) e101 := rel("E", "1.0.1", deps("F")) // INVALID g130 := rel("G", "1.3.0", deps()) g140 := rel("G", "1.4.0", deps()) g150 := rel("G", "1.5.0", deps()) g160 := rel("G", "1.6.0", deps()) g170 := rel("G", "1.7.0", deps()) g180 := rel("G", "1.8.0", deps()) h130 := rel("H", "1.3.0", deps()) h140 := rel("H", "1.4.0", deps()) h150 := rel("H", "1.5.0", deps()) h160 := rel("H", "1.6.0", deps()) h170 := rel("H", "1.7.0", deps()) h180 := rel("H", "1.8.0", deps()) i130 := rel("I", "1.3.0", deps()) i140 := rel("I", "1.4.0", deps()) i150 := rel("I", "1.5.0", deps()) i160 := rel("I", "1.6.0", deps()) i170 := rel("I", "1.7.0", deps()) i180 := rel("I", "1.8.0", deps()) arch := NewResolver[*customRel, *customDep]() arch.AddReleases( a100, a110, a111, a120, a121, b131, b130, b121, b120, b111, b110, b100, c200, c120, c111, c110, c102, c101, c100, c021, c020, c010, d100, d120, g130, g140, g150, g160, g170, g180, h130, h140, h150, h160, h170, h180, i130, i140, i150, i160, i170, i180, ) arch.AddRelease(e100) // use this method for 100% code coverage arch.AddRelease(e101) a130 := rel("A", "1.3.0", deps()) r0 := arch.Resolve(a130) // Non-existent in archive require.Nil(t, r0) r1 := arch.Resolve(a100) require.Len(t, r1, 3) require.Contains(t, r1, a100) require.Contains(t, r1, b130) require.Contains(t, r1, c200) fmt.Println(r1) r2 := arch.Resolve(a110) require.Len(t, r2, 3) require.Contains(t, r2, a110) require.Contains(t, r2, b120) require.Contains(t, r2, c200) fmt.Println(r2) r3 := arch.Resolve(a111) require.Len(t, r3, 3) require.Contains(t, r3, a111) require.Contains(t, r3, b111) require.Contains(t, r3, c111) fmt.Println(r3) r4 := arch.Resolve(a120) require.Nil(t, r4) fmt.Println(r4) r5 := arch.Resolve(c010) require.Contains(t, r5, c010) require.Contains(t, r5, d120) require.Contains(t, r5, e100) fmt.Println(r5) done := make(chan bool) go func() { r6 := arch.Resolve(a121) require.Nil(t, r6) fmt.Println(r6) close(done) }() select { case <-done: case <-time.After(time.Second): require.FailNow(t, "test didn't complete in the allocated time") } r7 := arch.Resolve(e101) require.Nil(t, r7) } relaxed-semver-0.15.0/sql.go000066400000000000000000000021241477074564600156710ustar00rootroot00000000000000// // Copyright 2018-2025 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // package semver import ( "database/sql/driver" "fmt" ) // Scan implements the sql.Scanner interface func (v *Version) Scan(value interface{}) error { raw, ok := value.(string) if !ok { return fmt.Errorf("incompatible type %T for Version", value) } v.raw = raw v.bytes = []byte(v.raw) if err := parse(v); err != nil { return err } return nil } // Value implements the driver.Valuer interface func (v *Version) Value() (driver.Value, error) { return v.raw, nil } // Scan implements the sql.Scanner interface func (v *RelaxedVersion) Scan(value interface{}) error { raw, ok := value.(string) if !ok { return fmt.Errorf("incompatible type %T for Version", value) } res := ParseRelaxed(raw) *v = *res return nil } // Value implements the driver.Valuer interface func (v *RelaxedVersion) Value() (driver.Value, error) { if v.version != nil { return v.version.raw, nil } return string(v.customversion), nil } relaxed-semver-0.15.0/sql_test.go000066400000000000000000000035101477074564600167300ustar00rootroot00000000000000// // Copyright 2018-2025 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // package semver import ( "database/sql/driver" "testing" "github.com/stretchr/testify/require" ) func TestSQLDriverInterfaces(t *testing.T) { t.Run("Version", func(t *testing.T) { // Test Version Scan/Value v := &Version{} if _, ok := interface{}(v).(driver.Valuer); !ok { t.Error("Version does not implement driver.Valuer") } if _, ok := interface{}(v).(driver.Valuer); !ok { t.Error("Version does not implement driver.Valuer") } require.Error(t, v.Scan(1)) require.Error(t, v.Scan(nil)) require.Error(t, v.Scan("123asdf")) require.NoError(t, v.Scan("1.2.3-rc.1+build.2")) require.Equal(t, "1.2.3-rc.1+build.2", v.String()) d, err := v.Value() require.NoError(t, err) require.Equal(t, "1.2.3-rc.1+build.2", d) }) t.Run("RelaxedVersion", func(t *testing.T) { // Test RelaxedVersion Scan/Value rv := &RelaxedVersion{} if _, ok := interface{}(rv).(driver.Valuer); !ok { t.Error("RelaxedVersion does not implement driver.Valuer") } if _, ok := interface{}(rv).(driver.Valuer); !ok { t.Error("RelaxedVersion does not implement driver.Valuer") } require.Error(t, rv.Scan(1)) require.Error(t, rv.Scan(nil)) require.NoError(t, rv.Scan("4.5.6-rc.1+build.2")) require.Empty(t, rv.customversion) require.NotNil(t, rv.version) require.Equal(t, "4.5.6-rc.1+build.2", rv.String()) rd, err := rv.Value() require.NoError(t, err) require.Equal(t, "4.5.6-rc.1+build.2", rd) require.NoError(t, rv.Scan("a1-2.2-3.3")) require.NotEmpty(t, rv.customversion) require.Nil(t, rv.version) require.Equal(t, "a1-2.2-3.3", rv.String()) rd2, err := rv.Value() require.NoError(t, err) require.Equal(t, "a1-2.2-3.3", rd2) }) } relaxed-semver-0.15.0/testdata/000077500000000000000000000000001477074564600163555ustar00rootroot00000000000000relaxed-semver-0.15.0/testdata/fuzz/000077500000000000000000000000001477074564600173535ustar00rootroot00000000000000relaxed-semver-0.15.0/testdata/fuzz/FuzzComparators/000077500000000000000000000000001477074564600225245ustar00rootroot00000000000000relaxed-semver-0.15.0/testdata/fuzz/FuzzComparators/175b79ab7e360ad6000066400000000000000000000000711477074564600246660ustar00rootroot00000000000000go test fuzz v1 string("0.0.2-rc-") string("0.0.2-rc.0") relaxed-semver-0.15.0/testdata/fuzz/FuzzComparators/9b49ee68467ea2f3000066400000000000000000000000571477074564600247120ustar00rootroot00000000000000go test fuzz v1 string("1-10") string("1.0.0") relaxed-semver-0.15.0/testdata/fuzz/FuzzParser/000077500000000000000000000000001477074564600214665ustar00rootroot00000000000000relaxed-semver-0.15.0/testdata/fuzz/FuzzParser/771e938e4458e983000066400000000000000000000000341477074564600234400ustar00rootroot00000000000000go test fuzz v1 string("0") relaxed-semver-0.15.0/version.go000066400000000000000000000322361477074564600165660ustar00rootroot00000000000000// // Copyright 2018-2025 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // package semver // Version contains the results of parsed version string type Version struct { raw string bytes []byte major int minor int patch int prerelease int build int } func (v *Version) String() string { if v == nil { return "" } return v.raw } // NormalizedString is a datatype to be used in maps and other places where the // version is used as a key. type NormalizedString string // NormalizedString return a string representation of the version that is // normalized to always have a major, minor and patch version. This is useful // to be used in maps and other places where the version is used as a key. func (v *Version) NormalizedString() NormalizedString { if v == nil { return "" } if v.major == 0 { return NormalizedString("0.0.0") } else if v.minor == v.major { return NormalizedString(v.raw[0:v.major] + ".0.0" + v.raw[v.major:]) } else if v.patch == v.minor { return NormalizedString(v.raw[0:v.minor] + ".0" + v.raw[v.minor:]) } else { return NormalizedString(v.raw) } } // Normalize transforms a truncated semver version in a strictly compliant semver // version by adding minor and patch versions. For example: // "1" is trasformed to "1.0.0" or "2.5-dev" to "2.5.0-dev" func (v *Version) Normalize() { if v.major == 0 { v.raw = "0.0.0" + v.raw v.major = 1 v.minor = 3 v.patch = 5 v.prerelease += 5 v.build += 5 } else if v.minor == v.major { v.raw = v.raw[0:v.major] + ".0.0" + v.raw[v.major:] v.minor = v.major + 2 v.patch = v.major + 4 v.prerelease += 4 v.build += 4 } else if v.patch == v.minor { v.raw = v.raw[0:v.minor] + ".0" + v.raw[v.minor:] v.patch = v.minor + 2 v.prerelease += 2 v.build += 2 } v.bytes = []byte(v.raw) } func compareNumber(a, b []byte) int { la := len(a) lb := len(b) if la == lb { for i := range a { if a[i] == b[i] { continue } if a[i] > b[i] { return 1 } return -1 } return 0 } if la > lb { return 1 } return -1 } func compareAlpha(a, b []byte) int { if string(a) > string(b) { return 1 } if string(a) < string(b) { return -1 } return 0 } var zero = []byte("0") // CompareTo compares the Version with the one passed as parameter. // Returns -1, 0 or 1 if the version is respectively less than, equal // or greater than the compared Version func (v *Version) CompareTo(u *Version) int { // 11. Precedence refers to how versions are compared to each other when ordered. // Precedence MUST be calculated by separating the version into cmp, minor, // patch and pre-release identifiers in that order (Build metadata does not // figure into precedence). Precedence is determined by the first difference when // comparing each of these identifiers from left to right as follows: Major, minor, // and patch versions are always compared numerically. // Example: 1.0.0 < 2.0.0 < 2.1.0 < 2.1.1. vIdx := 0 uIdx := 0 vMajor := v.major uMajor := u.major { if vMajor == uMajor { for vIdx < vMajor { if v.bytes[vIdx] == u.bytes[uIdx] { vIdx++ uIdx++ continue } if v.bytes[vIdx] > u.bytes[uIdx] { return 1 } return -1 } } else if vMajor == 0 && u.bytes[uIdx] == '0' { // continue } else if uMajor == 0 && v.bytes[vIdx] == '0' { // continue } else if vMajor > uMajor { return 1 } else { return -1 } } vMinor := v.minor uMinor := u.minor vIdx = vMajor + 1 uIdx = uMajor + 1 { la := vMinor - vMajor - 1 lb := uMinor - uMajor - 1 if la == lb { for vIdx < vMinor { if v.bytes[vIdx] == u.bytes[uIdx] { vIdx++ uIdx++ continue } if v.bytes[vIdx] > u.bytes[uIdx] { return 1 } return -1 } } else if vMinor == vMajor && u.bytes[uIdx] == '0' { // continue } else if uMinor == uMajor && v.bytes[vIdx] == '0' { // continue } else if la > lb { return 1 } else { return -1 } } vPatch := v.patch uPatch := u.patch vIdx = vMinor + 1 uIdx = uMinor + 1 { la := vPatch - vMinor - 1 lb := uPatch - uMinor - 1 if la == lb { for vIdx < vPatch { if v.bytes[vIdx] == u.bytes[uIdx] { vIdx++ uIdx++ continue } if v.bytes[vIdx] > u.bytes[uIdx] { return 1 } return -1 } } else if vPatch == vMinor && u.bytes[uIdx] == '0' { // continue } else if uPatch == uMinor && v.bytes[vIdx] == '0' { // continue } else if la > lb { return 1 } else { return -1 } } // if both versions have no pre-release, they are equal if v.prerelease == vPatch && u.prerelease == uPatch { return 0 } // When major, minor, and patch are equal, a pre-release version has lower // precedence than a normal version. // Example: 1.0.0-alpha < 1.0.0. // if v has no pre-release, it's greater than u if v.prerelease == vPatch { return 1 } // if u has no pre-release, it's greater than v if u.prerelease == uPatch { return -1 } // Precedence for two pre-release versions with the same major, minor, and patch // version MUST be determined by comparing each dot separated identifier from left // to right until a difference is found as follows: // - identifiers consisting of only digits are compared numerically // - identifiers with letters or hyphens are compared lexically in ASCII sort order. // Numeric identifiers always have lower precedence than non-numeric identifiers. // A larger set of pre-release fields has a higher precedence than a smaller set, // if all of the preceding identifiers are equal. // Example: 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < // < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0. vIdx = vPatch + 1 uIdx = uPatch + 1 vLast := v.prerelease uLast := u.prerelease vIsAlpha := false uIsAlpha := false vIsLonger := false uIsLonger := false cmp := 0 for { var vCurr byte var uCurr byte if vIdx != vLast { vCurr = v.raw[vIdx] } if uIdx != uLast { uCurr = u.raw[uIdx] } if vIdx == vLast || vCurr == '.' { if uIdx != uLast && uCurr != '.' { if !uIsAlpha && !(uCurr >= '0' && uCurr <= '9') { uIsAlpha = true } uIsLonger = true uIdx++ continue } } else if uIdx == uLast || uCurr == '.' { if vIdx != vLast && vCurr != '.' { if !vIsAlpha && !(vCurr >= '0' && vCurr <= '9') { vIsAlpha = true } vIsLonger = true vIdx++ continue } } else { if cmp == 0 { if vCurr > uCurr { cmp = 1 } else if vCurr < uCurr { cmp = -1 } } if !vIsAlpha && !(vCurr >= '0' && vCurr <= '9') { vIsAlpha = true } if !uIsAlpha && !(uCurr >= '0' && uCurr <= '9') { uIsAlpha = true } vIdx++ uIdx++ continue } // Numeric identifiers always have lower precedence than non-numeric identifiers. if vIsAlpha && uIsAlpha { if cmp != 0 { // alphanumeric vs alphanumeric, sorting has priority return cmp } else if vIsLonger { // alphanumeric vs alphanumeric, v is longer, return > return 1 } else if uIsLonger { // alphanumeric vs alphanumeric, u is longer, return < return -1 } // Both alphanumeric, if comparison is equal, move on the next field } else if vIsAlpha && !uIsAlpha { // alphanumeric vs numeric, return > return 1 } else if !vIsAlpha && uIsAlpha { // numeric vs alphanumeric, return < return -1 } else { if vIsLonger { // numeric vs numeric, v is longer, return > return 1 } else if uIsLonger { // numeric vs numeric, u is longer, return < return -1 } else if cmp != 0 { // numeric vs numeric, return cmp if not equal return cmp } // Both numeric, if comparison is equal, move on the next field } // A larger set of pre-release fields has a higher precedence than a smaller set, // if all of the preceding identifiers are equal. if vIdx == vLast && uIdx == uLast { // No more field, proceed with build metadata break } if vIdx != vLast && uIdx == uLast { // v has more fields, return > return 1 } if vIdx == vLast && uIdx != uLast { // u has more fields, return < return -1 } // Move on the next field vIsAlpha = false uIsAlpha = false vIsLonger = false uIsLonger = false vIdx++ uIdx++ } return 0 } // LessThan returns true if the Version is less than the Version passed as parameter func (v *Version) LessThan(u *Version) bool { return v.CompareTo(u) < 0 } // LessThanOrEqual returns true if the Version is less than or equal to the Version passed as parameter func (v *Version) LessThanOrEqual(u *Version) bool { return v.CompareTo(u) <= 0 } // Equal returns true if the Version is equal to the Version passed as parameter func (v *Version) Equal(u *Version) bool { return v.CompareTo(u) == 0 } // GreaterThan returns true if the Version is greater than the Version passed as parameter func (v *Version) GreaterThan(u *Version) bool { return v.CompareTo(u) > 0 } // GreaterThanOrEqual returns true if the Version is greater than or equal to the Version passed as parameter func (v *Version) GreaterThanOrEqual(u *Version) bool { return v.CompareTo(u) >= 0 } // CompatibleWith returns true if the Version is compatible with the version passed as paramater func (v *Version) CompatibleWith(u *Version) bool { if !u.GreaterThanOrEqual(v) { return false } vMajor := zero[:] if v.major > 0 { vMajor = v.bytes[:v.major] } uMajor := zero[:] if u.major > 0 { uMajor = u.bytes[:u.major] } majorEquals := compareNumber(vMajor, uMajor) == 0 if v.major > 0 && v.bytes[0] != '0' { return majorEquals } if !majorEquals { return false } vMinor := zero[:] if v.minor > v.major { vMinor = v.bytes[v.major+1 : v.minor] } uMinor := zero[:] if u.minor > u.major { uMinor = u.bytes[u.major+1 : u.minor] } minorEquals := compareNumber(vMinor, uMinor) == 0 if vMinor[0] != '0' { return minorEquals } if !minorEquals { return false } vPatch := zero[:] if v.patch > v.minor { vPatch = v.bytes[v.minor+1 : v.patch] } uPatch := zero[:] if u.patch > u.minor { uPatch = u.bytes[u.minor+1 : u.patch] } return compareNumber(vPatch, uPatch) == 0 } // SortableString returns the version encoded as a string that when compared // with alphanumeric ordering it respects the original semver ordering: // // (v1.SortableString() < v2.SortableString()) == v1.LessThan(v2) // cmp.Compare[string](v1.SortableString(), v2.SortableString()) == v1.CompareTo(v2) // // This may turn out useful when the version is saved in a database or is // introduced in a system that doesn't support semver ordering. func (v *Version) SortableString() string { // Encode a number in a string that when compared as string it respects // the original numeric order. // To allow longer numbers to be compared correctly, a prefix of ":"s // with the length of the number is added minus 1. // For example: 123 -> "::123" // 45 -> ":45" // The number written as string compare as ("123" < "99") but the encoded // version keeps the original integer ordering ("::123" > ":99"). encodeNumber := func(in []byte) string { if len(in) == 0 { return "0" } p := "" for range in { p += ":" } return p[:len(p)-1] + string(in) } var vMajor, vMinor, vPatch []byte vMajor = v.bytes[:v.major] if v.minor > v.major { vMinor = v.bytes[v.major+1 : v.minor] } if v.patch > v.minor { vPatch = v.bytes[v.minor+1 : v.patch] } res := ";" + encodeNumber(vMajor) + "." + encodeNumber(vMinor) + "." + encodeNumber(vPatch) // If there is no pre-release, add a ";" to the end, otherwise add a "-" followed by the pre-release. // This ensure the correct ordering of the pre-release versions (that are always lower than the normal versions). if v.prerelease == v.patch { return res + ";" } res += "-" isAlpha := false add := func(in []byte) { // if the pre-release piece is alphanumeric, add a ";" before the piece // otherwise add an ":" before the piece. This ensure the correct ordering // of the pre-release piece (numeric are lower than alphanumeric). if isAlpha { res += ";" + string(in) } else { res += ":" + encodeNumber(in) } isAlpha = false } prerelease := v.bytes[v.patch+1 : v.prerelease] start := 0 for curr, c := range prerelease { if c == '.' { add(prerelease[start:curr]) // separate the pre-release pieces with a "," to ensure the correct ordering // of the pre-release pieces (the separator must be lower than any other allowed // character [a-zA-Z0-9-]). res += "," start = curr + 1 continue } if !isNumeric(c) { isAlpha = true } } add(prerelease[start:]) return res } // IsPrerelease returns true if the version has a pre-release part func (v *Version) IsPrerelease() bool { return v.prerelease != v.patch } // Prerelease returns the pre-release part of the version func (v *Version) Prerelease() string { if !v.IsPrerelease() { return "" } return v.raw[v.patch+1 : v.prerelease] } // HasBuildMetadata returns true if the version has a build metadata part func (v *Version) HasBuildMetadata() bool { return v.build != v.prerelease } // BuildMetadata returns the build metadata part of the version func (v *Version) BuildMetadata() string { if !v.HasBuildMetadata() { return "" } return v.raw[v.prerelease+1 : v.build] } relaxed-semver-0.15.0/version_test.go000066400000000000000000000244241477074564600176250ustar00rootroot00000000000000// // Copyright 2018-2025 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // package semver import ( "cmp" "fmt" "testing" "github.com/stretchr/testify/require" ) var sign = map[int]string{1: ">", 0: "=", -1: "<"} func v(vers string) *Version { return MustParse(vers) } func ascending(t *testing.T, allowEqual bool, list ...string) { for i := range list[0 : len(list)-1] { a := MustParse(list[i]) b := MustParse(list[i+1]) comp := a.CompareTo(b) if allowEqual { fmt.Printf("%s %s= %s\n", list[i], sign[comp], list[i+1]) require.LessOrEqual(t, comp, 0) require.True(t, a.LessThanOrEqual(b)) require.False(t, a.GreaterThan(b)) } else { fmt.Printf("%s %s %s\n", list[i], sign[comp], list[i+1]) require.Equal(t, comp, -1, "cmp(%s, %s) must return '<', but returned '%s'", list[i], list[i+1], sign[comp]) require.True(t, a.LessThan(b)) require.True(t, a.LessThanOrEqual(b)) require.False(t, a.Equal(b)) require.False(t, a.GreaterThanOrEqual(b)) require.False(t, a.GreaterThan(b)) } comp = b.CompareTo(a) fmt.Printf("%s %s %s\n", b, sign[comp], a) if allowEqual { require.GreaterOrEqual(t, comp, 0, "cmp(%s, %s) must return '>=', but returned '%s'", b, a, sign[comp]) require.False(t, b.LessThan(a)) require.True(t, b.GreaterThanOrEqual(a)) } else { require.Equal(t, comp, 1) require.False(t, b.LessThan(a)) require.False(t, b.LessThanOrEqual(a)) require.False(t, b.Equal(a)) require.True(t, b.GreaterThanOrEqual(a)) require.True(t, b.GreaterThan(a)) } } for i := range list[0 : len(list)-1] { a := MustParse(list[i]).SortableString() b := MustParse(list[i+1]).SortableString() comp := cmp.Compare(a, b) if allowEqual { fmt.Printf("%s %s= %s\n", list[i], sign[comp], list[i+1]) require.LessOrEqual(t, comp, 0) require.True(t, a <= b) require.False(t, a > b) } else { fmt.Printf("%s %s %s\n", list[i], sign[comp], list[i+1]) require.Equal(t, comp, -1, "cmp(%s, %s) (%s, %s) must return '<', but returned '%s'", list[i], list[i+1], a, b, sign[comp]) require.True(t, a < b) require.True(t, a <= b) require.False(t, a == b) require.False(t, a >= b) require.False(t, a > b) } comp = cmp.Compare(b, a) fmt.Printf("%s %s %s\n", b, sign[comp], a) if allowEqual { require.GreaterOrEqual(t, comp, 0, "cmp(%s, %s) must return '>=', but returned '%s'", b, a, sign[comp]) require.False(t, b < a) require.True(t, b >= a) } else { require.Equal(t, comp, 1) require.False(t, b < a) require.False(t, b <= a) require.False(t, b == a) require.True(t, b >= a) require.True(t, b > a) } } } func TestVersionComparator(t *testing.T) { equal := func(list ...*Version) { for i, a := range list[:len(list)-1] { for _, b := range list[i+1:] { comp := a.CompareTo(b) fmt.Printf("%s %s %s\n", a, sign[comp], b) require.Equal(t, comp, 0, "cmp(%s, %s) must return '=', but returned '%s'", a, b, sign[comp]) require.False(t, a.LessThan(b), "NOT wanted: %s < %s", a, b) require.True(t, a.LessThanOrEqual(b), "wanted: %s <= %s", a, b) require.True(t, a.Equal(b), "wanted: %s = %s", a, b) require.True(t, a.GreaterThanOrEqual(b), "wanted: %s >= %s", a, b) require.False(t, a.GreaterThan(b), "NOT wanted: %s > %s", a, b) comp = b.CompareTo(a) fmt.Printf("%s %s %s\n", b, sign[comp], a) require.Equal(t, comp, 0, "cmp(%s, %s) must return '=', but returned '%s'", b, a, sign[comp]) require.False(t, b.LessThan(a), "NOT wanted: %s < %s", b, a) require.True(t, b.LessThanOrEqual(a), "wanted: %s <= %s", b, a) require.True(t, b.Equal(a), "wanted: %s = %s", b, a) require.True(t, b.GreaterThanOrEqual(a), "wanted: %s >= %s", b, a) require.False(t, b.GreaterThan(a), "NOT wanted: %s > %s", b, a) } } } ascending(t, false, "", "0.0.1") ascending(t, false, "", "0.1") ascending(t, false, "", "1") ascending(t, false, "0", "0.0.1") ascending(t, false, "0", "0.1") ascending(t, false, "0", "1") ascending(t, false, "0.0", "0.0.1") ascending(t, false, "0.0", "0.1") ascending(t, false, "0.0", "1") ascending(t, false, "", "0.0.1", "0.1", "1.0.0-2", "1.0.0-11", "1.0.0-11a", "1.0.0-alpha", "1.0.0-alpha.1", "1.0.0-alpha.beta", "1.0.0-beta", "1.0.0-beta.2", "1.0.0-beta.11", "1.0.0-beta.11a", "1.0.0-rc.1", "1.0.0", "1.0.1", "1.1.1", "1.1.8", "1.1.22", "1.6.22", "1.8.1", "1.20.0", "2.1.1", "10.0.0", "17.3.0-atmel3.6.1-arduino7", "17.3.0-atmel3.6.1-arduino7not", "17.3.0-atmel3.6.1-beduino8", "17.3.0-atmel3.6.1-beduino8not", "17.3.0-atmel3a.6.1-arduino7", "17.3.0-atmel3a.16.2.arduino7", "17.3.0-atmel3a.16.12.arduino7", "17.3.0-atmel3a.16.1-arduino7", "17.3.0-atmel3a.16.12-arduino7", "17.3.0-atmel3a.16.2-arduino7", "34.0.0", "51.0.0", "99.0.0", "123.0.0", ) equal( MustParse(""), MustParse("0"), MustParse("0.0"), MustParse("0.0.0"), MustParse("0+aaa"), MustParse("0.0+aaa"), MustParse("0.0.0+aaa"), MustParse("0+aaa.bbb"), MustParse("0.0+aaa.bbb"), MustParse("0.0.0+aaa.bbb"), ) equal( MustParse("0-ab"), MustParse("0.0-ab"), MustParse("0.0.0-ab"), MustParse("0-ab+aaa"), MustParse("0.0-ab+aaa"), MustParse("0.0.0-ab+aaa"), MustParse("0-ab+aaa.bbb"), MustParse("0.0-ab+aaa.bbb"), MustParse("0.0.0-ab+aaa.bbb"), ) } func TestCompatibleWithVersionComparator(t *testing.T) { require.True(t, v("0.0.1-rc.0+build").CompatibleWith(v("0.0.1-rc.0"))) list := []string{ "0.0.1-rc.0", // 0 "0.0.1-rc.0+build", // 1 "0.0.1-rc.1", // 2 "0.0.1", // 3 "0.0.1+build", // 4 "0.0.2-rc.1", // 5 - BREAKING CHANGE "0.0.2-rc.1+build", // 6 "0.0.2", // 7 "0.0.2+build", // 8 "0.0.3-rc.1", // 9 - BREAKING CHANGE "0.0.3-rc.2", // 10 "0.0.3", // 11 "0.1.0", // 12 - BREAKING CHANGE "0.3.3-rc.0", // 13 - BREAKING CHANGE "0.3.3-rc.1", // 14 "0.3.3", // 15 "0.3.3+build", // 16 "0.3.4-rc.1", // 17 "0.3.4", // 18 "0.4.0", // 19 - BREAKING CHANGE "1.0.0-rc", // 20 - BREAKING CHANGE "1.0.0", // 21 "1.0.0+build", // 22 "1.2.1-rc", // 23 "1.2.1", // 24 "1.2.1+build", // 25 "1.2.3-rc.2", // 26 "1.2.3-rc.2+build", // 27 "1.2.3", // 28 "1.2.3+build", // 29 "1.2.4", // 30 "1.3.0-rc.0+build", // 31 "1.3.0", // 32 "1.3.0+build", // 33 "1.3.1-rc.0", // 34 "1.3.1-rc.1", // 35 "1.3.1", // 36 "1.3.5", // 37 "2.0.0-rc", // 38 - BREAKING CHANGE "2.0.0-rc+build", // 39 "2.0.0", // 40 "2.0.0+build", // 41 "2.1.0-rc", // 42 "2.1.0-rc+build", // 43 "2.1.0", // 44 "2.1.0+build", // 45 "2.1.3-rc", // 46 "2.1.3", // 47 "2.3.0", // 48 "2.3.1", // 49 "3.0.0", // 50 - BREAKING CHANGE } breaking := []int{5, 9, 12, 13, 19, 20, 38, 50} ascending(t, true, list...) compatible := func(which, from, to int) { x := MustParse(list[which]) for _, comp := range list[:from] { y := MustParse(comp) require.False(t, x.CompatibleWith(y), "%s is not compatible with %s", x, y) } for _, comp := range list[from:to] { y := MustParse(comp) require.True(t, x.CompatibleWith(y), "%s is compatible with %s", x, y) } for _, comp := range list[to:] { y := MustParse(comp) require.False(t, x.CompatibleWith(y), "%s is not compatible with %s", x, y) } } j := 0 for i := 0; i < len(list)-1; i++ { breakingIdx := 0 for _, b := range breaking { if b > i { breakingIdx = b break } } if !MustParse(list[j]).Equal(MustParse(list[i])) { j = i } compatible(i, j, breakingIdx) } } func TestNilVersionString(t *testing.T) { var nilVersion *Version require.Equal(t, "", nilVersion.String()) } func TestCompareNumbers(t *testing.T) { // == require.Zero(t, compareNumber([]byte("0"), []byte("0"))) require.Zero(t, compareNumber([]byte("5"), []byte("5"))) require.Zero(t, compareNumber([]byte("15"), []byte("15"))) // > testGreater := func(a, b string) { require.Positive(t, compareNumber([]byte(a), []byte(b)), `compareNumber("%s","%s") is not positive`, a, b) require.Negative(t, compareNumber([]byte(b), []byte(a)), `compareNumber("%s","%s") is not negative`, b, a) } testGreater("1", "") testGreater("1", "0") testGreater("1", "") testGreater("2", "1") testGreater("10", "") testGreater("10", "0") testGreater("10", "1") testGreater("10", "2") } func TestVersionGetters(t *testing.T) { type test struct { version string prerelease string build string } tests := []test{ {"", "", ""}, {"0", "", ""}, {"1", "", ""}, {"0.1", "", ""}, {"1.1", "", ""}, {"0.2.3", "", ""}, {"1.2.3-aaa", "aaa", ""}, {"0.2-aaa", "aaa", ""}, {"1-aaa", "aaa", ""}, {"0.2.3+bbb", "", "bbb"}, {"1.3+bbb", "", "bbb"}, {"0+bbb", "", "bbb"}, {"1.2.3-aaa+bbb", "aaa", "bbb"}, {"0.2-aaa+bbb", "aaa", "bbb"}, {"1-aaa+bbb", "aaa", "bbb"}, {"0.2.3-aaa.4.5.6+bbb.7.8.9", "aaa.4.5.6", "bbb.7.8.9"}, } for _, tt := range tests { v := MustParse(tt.version) require.Equal(t, tt.version, v.String()) require.Equal(t, tt.prerelease != "", v.IsPrerelease()) require.Equal(t, tt.prerelease, v.Prerelease()) require.Equal(t, tt.build != "", v.HasBuildMetadata()) require.Equal(t, tt.build, v.BuildMetadata()) r := ParseRelaxed(tt.version) require.Equal(t, tt.version, r.String()) require.Equal(t, tt.prerelease != "", r.IsPrerelease()) require.Equal(t, tt.prerelease, r.Prerelease()) require.Equal(t, tt.build != "", r.HasBuildMetadata()) require.Equal(t, tt.build, r.BuildMetadata()) } relaxedTests := []test{ {"asd", "", ""}, {"123.123.123.123-123", "", ""}, {"1.2.3-a@very@fancy@version", "", ""}, } for _, tt := range relaxedTests { v, err := Parse(tt.version) require.Error(t, err, "should not parse %s", tt.version) require.Nil(t, v) r := ParseRelaxed(tt.version) require.Equal(t, tt.version, r.String()) require.Equal(t, tt.prerelease != "", r.IsPrerelease()) require.Equal(t, tt.prerelease, r.Prerelease()) require.Equal(t, tt.build != "", r.HasBuildMetadata()) require.Equal(t, tt.build, r.BuildMetadata()) } } relaxed-semver-0.15.0/yaml.go000066400000000000000000000023511477074564600160360ustar00rootroot00000000000000// // Copyright 2018-2025 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // package semver import ( "gopkg.in/yaml.v3" ) // MarshalYAML implements yaml.Marshaler func (v *Version) MarshalYAML() (interface{}, error) { return v.String(), nil } // UnmarshalYAML implements yaml.Unmarshaler func (v *Version) UnmarshalYAML(node *yaml.Node) error { var versionString string if err := node.Decode(&versionString); err != nil { return err } parsed, err := Parse(versionString) if err != nil { return err } v.raw = parsed.raw v.bytes = []byte(v.raw) v.major = parsed.major v.minor = parsed.minor v.patch = parsed.patch v.prerelease = parsed.prerelease v.build = parsed.build return nil } // MarshalYAML implements yaml.Marshaler func (v *RelaxedVersion) MarshalYAML() (interface{}, error) { return v.String(), nil } // UnmarshalYAML implements yaml.Unmarshaler func (v *RelaxedVersion) UnmarshalYAML(node *yaml.Node) error { var versionString string if err := node.Decode(&versionString); err != nil { return err } parsed := ParseRelaxed(versionString) v.customversion = parsed.customversion v.version = parsed.version return nil } relaxed-semver-0.15.0/yaml_test.go000066400000000000000000000047711477074564600171050ustar00rootroot00000000000000// // Copyright 2018-2025 Cristian Maglie. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // package semver import ( "fmt" "testing" "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" ) func TestYAMLParseVersion(t *testing.T) { var versionIsYamlUnmarshaler yaml.Unmarshaler = MustParse("1.0.0") var versionIsYamlMarshaler yaml.Marshaler = MustParse("1.0.0") _ = versionIsYamlUnmarshaler _ = versionIsYamlMarshaler testVersion := "1.2.3-aaa.4.5.6+bbb.7.8.9" v, err := Parse(testVersion) require.NoError(t, err) data, err := yaml.Marshal(v) require.Equal(t, "1.2.3-aaa.4.5.6+bbb.7.8.9\n", string(data)) require.NoError(t, err) var u Version err = yaml.Unmarshal(data, &u) require.NoError(t, err) dump := fmt.Sprintf("%v,%v,%v,%v,%v,%v", u.raw, u.major, u.minor, u.patch, u.prerelease, u.build) require.Equal(t, "1.2.3-aaa.4.5.6+bbb.7.8.9,1,3,5,15,25", dump) require.Equal(t, testVersion, u.String()) err = yaml.Unmarshal([]byte(`"invalid"`), &u) require.Error(t, err) err = yaml.Unmarshal([]byte(`invalid:`), &u) require.Error(t, err) require.NoError(t, yaml.Unmarshal([]byte(`"1.6.2"`), &v)) require.NoError(t, yaml.Unmarshal([]byte(`"1.6.3"`), &u)) require.True(t, u.GreaterThan(v)) } func TestYAMLParseRelaxedVersion(t *testing.T) { var relaxedVersionIsYamlUnmarshaler yaml.Unmarshaler = ParseRelaxed("1.0.0") var relaxedVersionIsYamlMarshaler yaml.Marshaler = ParseRelaxed("1.0.0") _ = relaxedVersionIsYamlUnmarshaler _ = relaxedVersionIsYamlMarshaler testVersion := "1.2.3-aaa.4.5.6+bbb.7.8.9" v := ParseRelaxed(testVersion) data, err := yaml.Marshal(v) require.NoError(t, err) require.Equal(t, "1.2.3-aaa.4.5.6+bbb.7.8.9\n", string(data)) var u RelaxedVersion err = yaml.Unmarshal(data, &u) require.NoError(t, err) require.Equal(t, testVersion, u.String()) err = yaml.Unmarshal([]byte(`"invalid"`), &u) require.NoError(t, err) require.Equal(t, "invalid", u.String()) err = yaml.Unmarshal([]byte(`invalid:`), &u) require.Error(t, err) } func BenchmarkYAMLDecoding(b *testing.B) { testVersion := "1.2.3-aaa.4.5.6+bbb.7.8.9" v, _ := Parse(testVersion) data, _ := yaml.Marshal(v) var u Version for i := 0; i < b.N; i++ { _ = yaml.Unmarshal(data, &u) } } func BenchmarkYAMLDecodingRelaxed(b *testing.B) { testVersion := "1.2.3-aaa.4.5.6+bbb.7.8.9" v := ParseRelaxed(testVersion) data, _ := yaml.Marshal(v) var u RelaxedVersion for i := 0; i < b.N; i++ { _ = yaml.Unmarshal(data, &u) } }