Recognize local app states (T-202)
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user