Files
soft_quay/core/domain/semver.go
T

213 lines
5.1 KiB
Go

package domain
import (
"errors"
"fmt"
"strings"
)
var ErrInvalidSemVer = errors.New("invalid semantic version")
// SemVersion is a parsed Semantic Version 2.0.0 value.
type SemVersion struct {
original string
major string
minor string
patch string
prerelease []string
build []string
}
// ParseSemVer parses a complete Semantic Version 2.0.0 string.
func ParseSemVer(value string) (SemVersion, error) {
if value == "" {
return SemVersion{}, fmt.Errorf("%w: empty value", ErrInvalidSemVer)
}
coreAndPrerelease := value
var build []string
if separator := strings.IndexByte(value, '+'); separator >= 0 {
if strings.IndexByte(value[separator+1:], '+') >= 0 {
return SemVersion{}, fmt.Errorf("%w: multiple build separators", ErrInvalidSemVer)
}
coreAndPrerelease = value[:separator]
var err error
build, err = parseIdentifiers(value[separator+1:], false)
if err != nil {
return SemVersion{}, err
}
}
core := coreAndPrerelease
var prerelease []string
if separator := strings.IndexByte(coreAndPrerelease, '-'); separator >= 0 {
core = coreAndPrerelease[:separator]
var err error
prerelease, err = parseIdentifiers(coreAndPrerelease[separator+1:], true)
if err != nil {
return SemVersion{}, err
}
}
parts := strings.Split(core, ".")
if len(parts) != 3 {
return SemVersion{}, fmt.Errorf("%w: core must contain major.minor.patch", ErrInvalidSemVer)
}
for _, part := range parts {
if !validNumericIdentifier(part, false) {
return SemVersion{}, fmt.Errorf("%w: invalid core identifier %q", ErrInvalidSemVer, part)
}
}
return SemVersion{
original: value,
major: parts[0],
minor: parts[1],
patch: parts[2],
prerelease: prerelease,
build: build,
}, nil
}
// String returns the original normalized-by-validation input.
func (version SemVersion) String() string {
return version.original
}
// Compare applies SemVer precedence. Build metadata does not affect ordering.
func (version SemVersion) Compare(other SemVersion) int {
if comparison := compareNumericText(version.major, other.major); comparison != 0 {
return comparison
}
if comparison := compareNumericText(version.minor, other.minor); comparison != 0 {
return comparison
}
if comparison := compareNumericText(version.patch, other.patch); comparison != 0 {
return comparison
}
if len(version.prerelease) == 0 && len(other.prerelease) == 0 {
return 0
}
if len(version.prerelease) == 0 {
return 1
}
if len(other.prerelease) == 0 {
return -1
}
count := len(version.prerelease)
if len(other.prerelease) < count {
count = len(other.prerelease)
}
for index := 0; index < count; index++ {
left := version.prerelease[index]
right := other.prerelease[index]
leftNumeric := allDigits(left)
rightNumeric := allDigits(right)
switch {
case leftNumeric && rightNumeric:
if comparison := compareNumericText(left, right); comparison != 0 {
return comparison
}
case leftNumeric:
return -1
case rightNumeric:
return 1
case left < right:
return -1
case left > right:
return 1
}
}
switch {
case len(version.prerelease) < len(other.prerelease):
return -1
case len(version.prerelease) > len(other.prerelease):
return 1
default:
return 0
}
}
// CompareSemVer parses and compares two version strings.
func CompareSemVer(left, right string) (int, error) {
leftVersion, err := ParseSemVer(left)
if err != nil {
return 0, err
}
rightVersion, err := ParseSemVer(right)
if err != nil {
return 0, err
}
return leftVersion.Compare(rightVersion), nil
}
func parseIdentifiers(value string, rejectNumericLeadingZero bool) ([]string, error) {
identifiers := strings.Split(value, ".")
if value == "" || len(identifiers) == 0 {
return nil, fmt.Errorf("%w: empty identifier list", ErrInvalidSemVer)
}
for _, identifier := range identifiers {
if identifier == "" {
return nil, fmt.Errorf("%w: empty identifier", ErrInvalidSemVer)
}
for _, character := range identifier {
if (character < '0' || character > '9') &&
(character < 'A' || character > 'Z') &&
(character < 'a' || character > 'z') &&
character != '-' {
return nil, fmt.Errorf(
"%w: invalid identifier %q",
ErrInvalidSemVer,
identifier,
)
}
}
if rejectNumericLeadingZero &&
allDigits(identifier) &&
!validNumericIdentifier(identifier, false) {
return nil, fmt.Errorf(
"%w: numeric prerelease identifier %q has a leading zero",
ErrInvalidSemVer,
identifier,
)
}
}
return identifiers, nil
}
func validNumericIdentifier(value string, allowLeadingZero bool) bool {
if value == "" || !allDigits(value) {
return false
}
return allowLeadingZero || len(value) == 1 || value[0] != '0'
}
func allDigits(value string) bool {
if value == "" {
return false
}
for _, character := range value {
if character < '0' || character > '9' {
return false
}
}
return true
}
func compareNumericText(left, right string) int {
switch {
case len(left) < len(right):
return -1
case len(left) > len(right):
return 1
case left < right:
return -1
case left > right:
return 1
default:
return 0
}
}