Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

semver.gno

4.64 Kb · 203 lines
  1// Package semver is an on-chain port of the core of golang.org/x/mod/semver
  2// (and the spirit of github.com/Masterminds/semver): parse "vMAJOR.MINOR.PATCH
  3// [-prerelease][+build]" strings and compare them with correct Semantic
  4// Versioning 2.0.0 precedence. Pure and deterministic — no time, randomness,
  5// goroutines, or I/O.
  6//
  7// This package holds only the pure parsing/comparison API. For a live,
  8// stateful gnoweb demo that imports it and keeps an on-chain "submitted
  9// versions" board, see
 10// [r/moul/x/daily/semverdemo](/r/moul/x/daily/semverdemo/v0).
 11package semver
 12
 13import (
 14	"errors"
 15	"strings"
 16)
 17
 18// Version is a parsed semantic version. Build metadata is retained for display
 19// but, per the spec, is ignored when comparing precedence.
 20type Version struct {
 21	Major, Minor, Patch int
 22	Pre                 []string // pre-release identifiers (dot-separated)
 23	Build               string   // build metadata (after '+')
 24	Orig                string   // original input
 25}
 26
 27var errBadVersion = errors.New("invalid semantic version")
 28
 29// Parse reads "vMAJOR.MINOR.PATCH[-prerelease][+build]". The leading 'v' is
 30// optional. Numeric fields must be non-empty digit runs without leading zeros.
 31func Parse(s string) (Version, error) {
 32	v := Version{Orig: s}
 33	body := strings.TrimPrefix(s, "v")
 34
 35	if i := strings.IndexByte(body, '+'); i >= 0 {
 36		v.Build = body[i+1:]
 37		body = body[:i]
 38		if v.Build == "" {
 39			return Version{}, errBadVersion
 40		}
 41	}
 42	if i := strings.IndexByte(body, '-'); i >= 0 {
 43		pre := body[i+1:]
 44		body = body[:i]
 45		v.Pre = strings.Split(pre, ".")
 46		for _, id := range v.Pre {
 47			if !validPreIdent(id) {
 48				return Version{}, errBadVersion
 49			}
 50		}
 51	}
 52
 53	parts := strings.Split(body, ".")
 54	if len(parts) != 3 {
 55		return Version{}, errBadVersion
 56	}
 57	nums := [3]int{}
 58	for i, p := range parts {
 59		n, ok := parseNum(p)
 60		if !ok {
 61			return Version{}, errBadVersion
 62		}
 63		nums[i] = n
 64	}
 65	v.Major, v.Minor, v.Patch = nums[0], nums[1], nums[2]
 66	return v, nil
 67}
 68
 69// Compare returns -1, 0, or +1 as a < b, a == b, or a > b under semver
 70// precedence. Unparseable inputs sort after everything valid (and equal to
 71// each other), so Compare never panics.
 72func Compare(a, b string) int {
 73	va, ea := Parse(a)
 74	vb, eb := Parse(b)
 75	switch {
 76	case ea != nil && eb != nil:
 77		return 0
 78	case ea != nil:
 79		return 1
 80	case eb != nil:
 81		return -1
 82	}
 83	return va.compare(vb)
 84}
 85
 86func (v Version) compare(o Version) int {
 87	if c := cmpInt(v.Major, o.Major); c != 0 {
 88		return c
 89	}
 90	if c := cmpInt(v.Minor, o.Minor); c != 0 {
 91		return c
 92	}
 93	if c := cmpInt(v.Patch, o.Patch); c != 0 {
 94		return c
 95	}
 96	return comparePre(v.Pre, o.Pre)
 97}
 98
 99// comparePre implements SemVer §11: a version WITH a pre-release has lower
100// precedence than the same version WITHOUT one.
101func comparePre(a, b []string) int {
102	if len(a) == 0 && len(b) == 0 {
103		return 0
104	}
105	if len(a) == 0 { // a is a release, b is a pre-release
106		return 1
107	}
108	if len(b) == 0 {
109		return -1
110	}
111	for i := 0; i < len(a) && i < len(b); i++ {
112		if c := compareIdent(a[i], b[i]); c != 0 {
113			return c
114		}
115	}
116	return cmpInt(len(a), len(b))
117}
118
119// compareIdent: numeric identifiers compare numerically and always rank below
120// alphanumeric ones; alphanumerics compare in ASCII order.
121func compareIdent(a, b string) int {
122	an, aNum := parseNum(a)
123	bn, bNum := parseNum(b)
124	switch {
125	case aNum && bNum:
126		return cmpInt(an, bn)
127	case aNum:
128		return -1
129	case bNum:
130		return 1
131	}
132	return strings.Compare(a, b)
133}
134
135func cmpInt(a, b int) int {
136	switch {
137	case a < b:
138		return -1
139	case a > b:
140		return 1
141	}
142	return 0
143}
144
145// parseNum accepts a non-empty run of ASCII digits with no leading zero (except
146// "0" itself), returning the value and whether it qualified.
147func parseNum(s string) (int, bool) {
148	if s == "" {
149		return 0, false
150	}
151	if len(s) > 1 && s[0] == '0' {
152		return 0, false
153	}
154	n := 0
155	for i := 0; i < len(s); i++ {
156		c := s[i]
157		if c < '0' || c > '9' {
158			return 0, false
159		}
160		n = n*10 + int(c-'0')
161	}
162	return n, true
163}
164
165// validPreIdent allows [0-9A-Za-z-] and rejects empty identifiers.
166func validPreIdent(s string) bool {
167	if s == "" {
168		return false
169	}
170	for i := 0; i < len(s); i++ {
171		c := s[i]
172		ok := (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') ||
173			(c >= 'A' && c <= 'Z') || c == '-'
174		if !ok {
175			return false
176		}
177	}
178	return true
179}
180
181// Canonical renders the parsed version back to a canonical string.
182func (v Version) Canonical() string {
183	s := ufmt(v.Major) + "." + ufmt(v.Minor) + "." + ufmt(v.Patch)
184	if len(v.Pre) > 0 {
185		s += "-" + strings.Join(v.Pre, ".")
186	}
187	if v.Build != "" {
188		s += "+" + v.Build
189	}
190	return s
191}
192
193func ufmt(n int) string {
194	if n == 0 {
195		return "0"
196	}
197	var b []byte
198	for n > 0 {
199		b = append([]byte{byte('0' + n%10)}, b...)
200		n /= 10
201	}
202	return string(b)
203}