From 41bf5b86eab49688814e5050c3110d063043e03d Mon Sep 17 00:00:00 2001 From: Jan Tytgat Date: Tue, 15 Apr 2025 09:36:26 +0200 Subject: [PATCH] Add package semver Signed-off-by: Jan Tytgat --- pkg/semver/metadata.go | 23 ++++ pkg/semver/metadata_test.go | 37 +++++++ pkg/semver/prerelease.go | 3 + pkg/semver/version.go | 117 ++++++++++++++++++++ pkg/semver/version_test.go | 210 ++++++++++++++++++++++++++++++++++++ 5 files changed, 390 insertions(+) create mode 100644 pkg/semver/metadata.go create mode 100644 pkg/semver/metadata_test.go create mode 100644 pkg/semver/prerelease.go create mode 100644 pkg/semver/version.go create mode 100644 pkg/semver/version_test.go diff --git a/pkg/semver/metadata.go b/pkg/semver/metadata.go new file mode 100644 index 0000000..363bf0d --- /dev/null +++ b/pkg/semver/metadata.go @@ -0,0 +1,23 @@ +package semver + +import ( + "fmt" + "regexp" +) + +const ( + validMetadata = `^(?P[0-9a-zA-Z]{8}).(?P[0-9]{8})$` +) + +var regexMetadata = regexp.MustCompile(validMetadata) + +type Metadata string + +func SplitMetadata(m Metadata) (string, string, error) { + if !regexMetadata.MatchString(string(m)) { + return "", "", fmt.Errorf("invalid metadata: %s", m) + } + + match := regexMetadata.FindStringSubmatch(string(m)) + return match[1], match[2], nil +} diff --git a/pkg/semver/metadata_test.go b/pkg/semver/metadata_test.go new file mode 100644 index 0000000..8e564ac --- /dev/null +++ b/pkg/semver/metadata_test.go @@ -0,0 +1,37 @@ +package semver + +import "testing" + +var validMetadataTests = []struct { + name string + metadata Metadata + commit string + date string + err bool +}{ + { + name: "simple", + metadata: "metadata.20101112", + commit: "metadata", + date: "20101112", + err: false, + }, +} + +func TestSplitMetadata(t *testing.T) { + for _, tt := range validMetadataTests { + t.Run(tt.name, func(t *testing.T) { + got, got1, err := SplitMetadata(tt.metadata) + if (err != nil) != tt.err { + t.Errorf("SplitMetadata() error = %v, wantErr %v", err, tt.err) + return + } + if got != tt.commit { + t.Errorf("SplitMetadata() got = %v, want %v", got, tt.commit) + } + if got1 != tt.date { + t.Errorf("SplitMetadata() got1 = %v, want %v", got1, tt.date) + } + }) + } +} diff --git a/pkg/semver/prerelease.go b/pkg/semver/prerelease.go new file mode 100644 index 0000000..864c275 --- /dev/null +++ b/pkg/semver/prerelease.go @@ -0,0 +1,3 @@ +package semver + +type PreRelease string diff --git a/pkg/semver/version.go b/pkg/semver/version.go new file mode 100644 index 0000000..06b8ede --- /dev/null +++ b/pkg/semver/version.go @@ -0,0 +1,117 @@ +package semver + +import ( + "bytes" + "fmt" + "regexp" + "strconv" +) + +const ( + // https://semver.org/ && https://regex101.com/r/Ly7O1x/3/ + validSemVer = `^(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$` +) + +var regexSemver = regexp.MustCompile(validSemVer) + +type Version struct { + Major int64 + Minor int64 + Patch int64 + PreRelease PreRelease + Metadata Metadata +} + +func (v Version) Commit() string { + commit, _, err := SplitMetadata(v.Metadata) + if err != nil { + return string(v.Metadata) + } + return commit +} + +func (v Version) Date() string { + _, date, err := SplitMetadata(v.Metadata) + if err != nil { + return string(v.Metadata) + } + return date +} + +func (v Version) Release() string { + switch v.PreRelease { + case "": + return fmt.Sprint("stable") + default: + return string(v.PreRelease) + } +} + +func (v Version) String() string { + var buf bytes.Buffer + + fmt.Fprintf(&buf, "%d.%d.%d", v.Major, v.Minor, v.Patch) + + if v.PreRelease != "" { + fmt.Fprintf(&buf, "-%s", v.PreRelease) + } + + if v.Metadata != "" { + fmt.Fprintf(&buf, "+%s", v.Metadata) + } + + return buf.String() +} + +func (v Version) Number() string { + return fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch) +} + +func Parse(v string) (Version, error) { + if !regexSemver.MatchString(v) { + return Version{}, fmt.Errorf("invalid version: %s", v) + } + + match := regexSemver.FindStringSubmatch(v) + matchMap := make(map[string]string) + for i, name := range regexSemver.SubexpNames() { + if i != 0 && name != "" { + matchMap[name] = match[i] + } + } + + var err error + var major int64 + var minor int64 + var patch int64 + var preRelease PreRelease + var metadata Metadata + + if major, err = strconv.ParseInt(matchMap["major"], 10, 64); err != nil { + return Version{}, err + } + + if minor, err = strconv.ParseInt(matchMap["minor"], 10, 64); err != nil { + return Version{}, err + } + + if patch, err = strconv.ParseInt(matchMap["patch"], 10, 64); err != nil { + return Version{}, err + } + + if matchMap["prerelease"] != "" { + preRelease = PreRelease(matchMap["prerelease"]) + } + + if matchMap["buildmetadata"] != "" { + metadata = Metadata(matchMap["buildmetadata"]) + } + + return Version{ + Major: major, + Minor: minor, + Patch: patch, + PreRelease: preRelease, + Metadata: metadata, + }, nil +} diff --git a/pkg/semver/version_test.go b/pkg/semver/version_test.go new file mode 100644 index 0000000..a211934 --- /dev/null +++ b/pkg/semver/version_test.go @@ -0,0 +1,210 @@ +package semver + +import ( + "reflect" + "testing" +) + +var validVersionTests = []struct { + name string + input string + version Version + inputErr bool + commit string + date string + release string + full string + number string +}{ + { + name: "stable", + input: "0.1.0", + version: Version{ + Major: 0, + Minor: 1, + }, + inputErr: false, + commit: "", + date: "", + release: "stable", + full: "0.1.0", + number: "0.1.0", + }, + { + name: "stable.1", + input: "0.1.1", + version: Version{ + Major: 0, + Minor: 1, + Patch: 1, + }, + inputErr: false, + commit: "", + date: "", + release: "stable", + full: "0.1.1", + number: "0.1.1", + }, + { + name: "stable.1+metadata", + input: "0.1.1+metadata", + version: Version{ + Major: 0, + Minor: 1, + Patch: 1, + Metadata: "metadata", + }, + inputErr: false, + commit: "metadata", + date: "metadata", + release: "stable", + full: "0.1.1+metadata", + number: "0.1.1", + }, + { + name: "stable.1+metadata.date", + input: "0.1.1+metadata.20101112", + version: Version{ + Major: 0, + Minor: 1, + Patch: 1, + Metadata: "metadata.20101112", + }, + inputErr: false, + commit: "metadata", + date: "20101112", + release: "stable", + full: "0.1.1+metadata.20101112", + number: "0.1.1", + }, + { + name: "alpha", + input: "0.1.1-alpha", + version: Version{ + Major: 0, + Minor: 1, + Patch: 1, + PreRelease: "alpha", + }, + inputErr: false, + commit: "", + release: "alpha", + full: "0.1.1-alpha", + number: "0.1.1", + }, + { + name: "alpha.1", + input: "0.1.1-alpha.1", + version: Version{ + Major: 0, + Minor: 1, + Patch: 1, + PreRelease: "alpha.1", + }, + inputErr: false, + commit: "", + release: "alpha.1", + full: "0.1.1-alpha.1", + number: "0.1.1", + }, +} + +func TestParse(t *testing.T) { + for _, tt := range validVersionTests { + t.Run(tt.name, func(t *testing.T) { + got, err := Parse(tt.input) + if (err != nil) != tt.inputErr { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.inputErr) + return + } + if !reflect.DeepEqual(got, tt.version) { + t.Errorf("Parse() got = %v, want %v", got, tt.version) + } + }) + } +} + +func TestVersion_Commit(t *testing.T) { + for _, tt := range validVersionTests { + t.Run(tt.name, func(t *testing.T) { + v := Version{ + Major: tt.version.Major, + Minor: tt.version.Minor, + Patch: tt.version.Patch, + PreRelease: tt.version.PreRelease, + Metadata: tt.version.Metadata, + } + if got := v.Commit(); got != tt.commit { + t.Errorf("Commit() = %v, want %v", got, tt.commit) + } + }) + } +} + +func TestVersion_Date(t *testing.T) { + for _, tt := range validVersionTests { + t.Run(tt.name, func(t *testing.T) { + v := Version{ + Major: tt.version.Major, + Minor: tt.version.Minor, + Patch: tt.version.Patch, + PreRelease: tt.version.PreRelease, + Metadata: tt.version.Metadata, + } + if got := v.Date(); got != tt.date { + t.Errorf("Date() = %v, want %v", got, tt.date) + } + }) + } +} + +func TestVersion_Release(t *testing.T) { + for _, tt := range validVersionTests { + t.Run(tt.name, func(t *testing.T) { + v := Version{ + Major: tt.version.Major, + Minor: tt.version.Minor, + Patch: tt.version.Patch, + PreRelease: tt.version.PreRelease, + Metadata: tt.version.Metadata, + } + if got := v.Release(); got != tt.release { + t.Errorf("Release() = %v, want %v", got, tt.release) + } + }) + } +} + +func TestVersion_String(t *testing.T) { + for _, tt := range validVersionTests { + t.Run(tt.name, func(t *testing.T) { + v := Version{ + Major: tt.version.Major, + Minor: tt.version.Minor, + Patch: tt.version.Patch, + PreRelease: tt.version.PreRelease, + Metadata: tt.version.Metadata, + } + if got := v.String(); got != tt.full { + t.Errorf("String() = %v, want %v", got, tt.full) + } + }) + } +} + +func TestVersion_VersionNumber(t *testing.T) { + for _, tt := range validVersionTests { + t.Run(tt.name, func(t *testing.T) { + v := Version{ + Major: tt.version.Major, + Minor: tt.version.Minor, + Patch: tt.version.Patch, + PreRelease: tt.version.PreRelease, + Metadata: tt.version.Metadata, + } + if got := v.Number(); got != tt.number { + t.Errorf("Number() = %v, want %v", got, tt.number) + } + }) + } +}