| | | 1 | | package domain |
| | | 2 | | |
| | | 3 | | import ( |
| | | 4 | | "fmt" |
| | | 5 | | "strconv" |
| | | 6 | | "strings" |
| | | 7 | | ) |
| | | 8 | | |
| | | 9 | | // Version represents a semantic version (major.minor.patch) with optional prerelease and build metadata. |
| | | 10 | | type Version struct { |
| | | 11 | | Major int |
| | | 12 | | Minor int |
| | | 13 | | Patch int |
| | | 14 | | Prerelease string |
| | | 15 | | Build string |
| | | 16 | | } |
| | | 17 | | |
| | | 18 | | // ZeroVersion returns a 0.0.0 version. |
| | | 19 | | func ZeroVersion() Version { |
| | | 20 | | return Version{} |
| | | 21 | | } |
| | | 22 | | |
| | | 23 | | // NewVersion creates a version from major, minor, patch components. |
| | | 24 | | func NewVersion(major, minor, patch int) Version { |
| | | 25 | | return Version{Major: major, Minor: minor, Patch: patch} |
| | | 26 | | } |
| | | 27 | | |
| | | 28 | | // ParseVersion parses a semantic version string. Accepts optional "v" prefix. |
| | | 29 | | func ParseVersion(s string) (Version, error) { |
| | | 30 | | s = strings.TrimPrefix(s, "v") |
| | | 31 | | |
| | | 32 | | // Split off build metadata first. |
| | | 33 | | build := "" |
| | | 34 | | if idx := strings.IndexByte(s, '+'); idx >= 0 { |
| | | 35 | | build = s[idx+1:] |
| | | 36 | | s = s[:idx] |
| | | 37 | | } |
| | | 38 | | |
| | | 39 | | // Split off prerelease. |
| | | 40 | | prerelease := "" |
| | | 41 | | if idx := strings.IndexByte(s, '-'); idx >= 0 { |
| | | 42 | | prerelease = s[idx+1:] |
| | | 43 | | s = s[:idx] |
| | | 44 | | } |
| | | 45 | | |
| | | 46 | | parts := strings.Split(s, ".") |
| | | 47 | | if len(parts) != 3 { |
| | | 48 | | return Version{}, fmt.Errorf("invalid version %q: expected major.minor.patch", s) |
| | | 49 | | } |
| | | 50 | | |
| | | 51 | | major, err := strconv.Atoi(parts[0]) |
| | | 52 | | if err != nil { |
| | | 53 | | return Version{}, fmt.Errorf("invalid major version %q: %w", parts[0], err) |
| | | 54 | | } |
| | | 55 | | |
| | | 56 | | minor, err := strconv.Atoi(parts[1]) |
| | | 57 | | if err != nil { |
| | | 58 | | return Version{}, fmt.Errorf("invalid minor version %q: %w", parts[1], err) |
| | | 59 | | } |
| | | 60 | | |
| | | 61 | | patch, err := strconv.Atoi(parts[2]) |
| | | 62 | | if err != nil { |
| | | 63 | | return Version{}, fmt.Errorf("invalid patch version %q: %w", parts[2], err) |
| | | 64 | | } |
| | | 65 | | |
| | | 66 | | return Version{ |
| | | 67 | | Major: major, |
| | | 68 | | Minor: minor, |
| | | 69 | | Patch: patch, |
| | | 70 | | Prerelease: prerelease, |
| | | 71 | | Build: build, |
| | | 72 | | }, nil |
| | | 73 | | } |
| | | 74 | | |
| | | 75 | | // String returns the version as "major.minor.patch[-prerelease][+build]". |
| | 15 | 76 | | func (v Version) String() string { |
| | 15 | 77 | | s := fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch) |
| | 3 | 78 | | if v.Prerelease != "" { |
| | 3 | 79 | | s += "-" + v.Prerelease |
| | 3 | 80 | | } |
| | 2 | 81 | | if v.Build != "" { |
| | 2 | 82 | | s += "+" + v.Build |
| | 2 | 83 | | } |
| | 15 | 84 | | return s |
| | | 85 | | } |
| | | 86 | | |
| | | 87 | | // TagString returns the version with a "v" prefix. |
| | 4 | 88 | | func (v Version) TagString() string { |
| | 4 | 89 | | return "v" + v.String() |
| | 4 | 90 | | } |
| | | 91 | | |
| | | 92 | | // IsZero returns true if this is the zero version (0.0.0 with no prerelease/build). |
| | 2 | 93 | | func (v Version) IsZero() bool { |
| | 2 | 94 | | return v.Major == 0 && v.Minor == 0 && v.Patch == 0 && v.Prerelease == "" && v.Build == "" |
| | 2 | 95 | | } |
| | | 96 | | |
| | | 97 | | // Bump returns a new version incremented by the given release type. |
| | 5 | 98 | | func (v Version) Bump(rt ReleaseType) Version { |
| | 5 | 99 | | switch rt { |
| | 1 | 100 | | case ReleaseMajor: |
| | 1 | 101 | | return NewVersion(v.Major+1, 0, 0) |
| | 2 | 102 | | case ReleaseMinor: |
| | 2 | 103 | | return NewVersion(v.Major, v.Minor+1, 0) |
| | 1 | 104 | | case ReleasePatch: |
| | 1 | 105 | | return NewVersion(v.Major, v.Minor, v.Patch+1) |
| | 1 | 106 | | default: |
| | 1 | 107 | | return v |
| | | 108 | | } |
| | | 109 | | } |
| | | 110 | | |
| | | 111 | | // WithPrerelease returns a copy with the given prerelease identifier. |
| | 1 | 112 | | func (v Version) WithPrerelease(pre string) Version { |
| | 1 | 113 | | v.Prerelease = pre |
| | 1 | 114 | | v.Build = "" |
| | 1 | 115 | | return v |
| | 1 | 116 | | } |
| | | 117 | | |
| | | 118 | | // GreaterThan returns true if v is greater than other. |
| | 20 | 119 | | func (v Version) GreaterThan(other Version) bool { |
| | 8 | 120 | | if v.Major != other.Major { |
| | 8 | 121 | | return v.Major > other.Major |
| | 8 | 122 | | } |
| | 5 | 123 | | if v.Minor != other.Minor { |
| | 5 | 124 | | return v.Minor > other.Minor |
| | 5 | 125 | | } |
| | 7 | 126 | | return v.Patch > other.Patch |
| | | 127 | | } |
| | | 128 | | |
| | | 129 | | // Equal returns true if versions are identical (excluding build metadata per semver spec). |
| | 30 | 130 | | func (v Version) Equal(other Version) bool { |
| | 30 | 131 | | return v.Major == other.Major && |
| | 30 | 132 | | v.Minor == other.Minor && |
| | 30 | 133 | | v.Patch == other.Patch && |
| | 30 | 134 | | v.Prerelease == other.Prerelease |
| | 30 | 135 | | } |