package handlers import ( "fmt" "strconv" "strings" ) // Semver represents a parsed semantic version (major.minor.patch[-prerelease]). type Semver struct { Major int Minor int Patch int Prerelease string } // ParseSemver parses a version string like "1.2.3" or "v1.2.3-beta.1". // Leading "v" is stripped. Prerelease suffix after "-" is preserved. func ParseSemver(s string) (Semver, error) { s = strings.TrimSpace(s) s = strings.TrimPrefix(s, "v") if s == "" { return Semver{}, fmt.Errorf("empty version string") } // Split off prerelease suffix var pre string if idx := strings.IndexByte(s, '-'); idx >= 0 { pre = s[idx+1:] s = s[:idx] } parts := strings.Split(s, ".") if len(parts) != 3 { return Semver{}, fmt.Errorf("version %q must have exactly 3 parts (major.minor.patch)", s) } major, err := strconv.Atoi(parts[0]) if err != nil { return Semver{}, fmt.Errorf("invalid major version %q: %w", parts[0], err) } minor, err := strconv.Atoi(parts[1]) if err != nil { return Semver{}, fmt.Errorf("invalid minor version %q: %w", parts[1], err) } patch, err := strconv.Atoi(parts[2]) if err != nil { return Semver{}, fmt.Errorf("invalid patch version %q: %w", parts[2], err) } return Semver{Major: major, Minor: minor, Patch: patch, Prerelease: pre}, nil } // Compare returns -1 if a < b, 0 if a == b, +1 if a > b. // Prerelease versions sort lower than the same release version (per semver spec). func (a Semver) Compare(b Semver) int { if a.Major != b.Major { return cmpInt(a.Major, b.Major) } if a.Minor != b.Minor { return cmpInt(a.Minor, b.Minor) } if a.Patch != b.Patch { return cmpInt(a.Patch, b.Patch) } // Same numeric version — prerelease < release if a.Prerelease == b.Prerelease { return 0 } if a.Prerelease == "" { return 1 // a is release, b has prerelease → a > b } if b.Prerelease == "" { return -1 // a has prerelease, b is release → a < b } // Both have prerelease — compare lexicographically if a.Prerelease < b.Prerelease { return -1 } return 1 } // String returns the version in "major.minor.patch[-prerelease]" format. func (v Semver) String() string { s := fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch) if v.Prerelease != "" { s += "-" + v.Prerelease } return s } func cmpInt(a, b int) int { if a < b { return -1 } return 1 }