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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

semver.gno

4.65 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/nym-moultest123/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}