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

9.16 Kb · 374 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.
  6package semver
  7
  8import (
  9	"chain"
 10	"chain/runtime/unsafe"
 11	"errors"
 12	"sort"
 13	"strings"
 14
 15	"gno.land/p/nt/avl/v0"
 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// submitted holds versions users have posted on-chain: version string -> submitter.
 30var submitted = avl.NewTree()
 31
 32// Parse reads "vMAJOR.MINOR.PATCH[-prerelease][+build]". The leading 'v' is
 33// optional. Numeric fields must be non-empty digit runs without leading zeros.
 34func Parse(s string) (Version, error) {
 35	v := Version{Orig: s}
 36	body := strings.TrimPrefix(s, "v")
 37
 38	if i := strings.IndexByte(body, '+'); i >= 0 {
 39		v.Build = body[i+1:]
 40		body = body[:i]
 41		if v.Build == "" {
 42			return Version{}, errBadVersion
 43		}
 44	}
 45	if i := strings.IndexByte(body, '-'); i >= 0 {
 46		pre := body[i+1:]
 47		body = body[:i]
 48		v.Pre = strings.Split(pre, ".")
 49		for _, id := range v.Pre {
 50			if !validPreIdent(id) {
 51				return Version{}, errBadVersion
 52			}
 53		}
 54	}
 55
 56	parts := strings.Split(body, ".")
 57	if len(parts) != 3 {
 58		return Version{}, errBadVersion
 59	}
 60	nums := [3]int{}
 61	for i, p := range parts {
 62		n, ok := parseNum(p)
 63		if !ok {
 64			return Version{}, errBadVersion
 65		}
 66		nums[i] = n
 67	}
 68	v.Major, v.Minor, v.Patch = nums[0], nums[1], nums[2]
 69	return v, nil
 70}
 71
 72// Compare returns -1, 0, or +1 as a < b, a == b, or a > b under semver
 73// precedence. Unparseable inputs sort after everything valid (and equal to
 74// each other), so Compare never panics.
 75func Compare(a, b string) int {
 76	va, ea := Parse(a)
 77	vb, eb := Parse(b)
 78	switch {
 79	case ea != nil && eb != nil:
 80		return 0
 81	case ea != nil:
 82		return 1
 83	case eb != nil:
 84		return -1
 85	}
 86	return va.compare(vb)
 87}
 88
 89func (v Version) compare(o Version) int {
 90	if c := cmpInt(v.Major, o.Major); c != 0 {
 91		return c
 92	}
 93	if c := cmpInt(v.Minor, o.Minor); c != 0 {
 94		return c
 95	}
 96	if c := cmpInt(v.Patch, o.Patch); c != 0 {
 97		return c
 98	}
 99	return comparePre(v.Pre, o.Pre)
100}
101
102// comparePre implements SemVer §11: a version WITH a pre-release has lower
103// precedence than the same version WITHOUT one.
104func comparePre(a, b []string) int {
105	if len(a) == 0 && len(b) == 0 {
106		return 0
107	}
108	if len(a) == 0 { // a is a release, b is a pre-release
109		return 1
110	}
111	if len(b) == 0 {
112		return -1
113	}
114	for i := 0; i < len(a) && i < len(b); i++ {
115		if c := compareIdent(a[i], b[i]); c != 0 {
116			return c
117		}
118	}
119	return cmpInt(len(a), len(b))
120}
121
122// compareIdent: numeric identifiers compare numerically and always rank below
123// alphanumeric ones; alphanumerics compare in ASCII order.
124func compareIdent(a, b string) int {
125	an, aNum := parseNum(a)
126	bn, bNum := parseNum(b)
127	switch {
128	case aNum && bNum:
129		return cmpInt(an, bn)
130	case aNum:
131		return -1
132	case bNum:
133		return 1
134	}
135	return strings.Compare(a, b)
136}
137
138func cmpInt(a, b int) int {
139	switch {
140	case a < b:
141		return -1
142	case a > b:
143		return 1
144	}
145	return 0
146}
147
148// parseNum accepts a non-empty run of ASCII digits with no leading zero (except
149// "0" itself), returning the value and whether it qualified.
150func parseNum(s string) (int, bool) {
151	if s == "" {
152		return 0, false
153	}
154	if len(s) > 1 && s[0] == '0' {
155		return 0, false
156	}
157	n := 0
158	for i := 0; i < len(s); i++ {
159		c := s[i]
160		if c < '0' || c > '9' {
161			return 0, false
162		}
163		n = n*10 + int(c-'0')
164	}
165	return n, true
166}
167
168// validPreIdent allows [0-9A-Za-z-] and rejects empty identifiers.
169func validPreIdent(s string) bool {
170	if s == "" {
171		return false
172	}
173	for i := 0; i < len(s); i++ {
174		c := s[i]
175		ok := (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') ||
176			(c >= 'A' && c <= 'Z') || c == '-'
177		if !ok {
178			return false
179		}
180	}
181	return true
182}
183
184// Canonical renders the parsed version back to a canonical string.
185func (v Version) Canonical() string {
186	s := ufmt(v.Major) + "." + ufmt(v.Minor) + "." + ufmt(v.Patch)
187	if len(v.Pre) > 0 {
188		s += "-" + strings.Join(v.Pre, ".")
189	}
190	if v.Build != "" {
191		s += "+" + v.Build
192	}
193	return s
194}
195
196func ufmt(n int) string {
197	if n == 0 {
198		return "0"
199	}
200	var b []byte
201	for n > 0 {
202		b = append([]byte{byte('0' + n%10)}, b...)
203		n /= 10
204	}
205	return string(b)
206}
207
208// Submit stores a valid version on-chain, tagged with the caller's address, so
209// Render("/sorted") can list everyone's submissions in precedence order.
210func Submit(cur realm, version string) {
211	if _, err := Parse(version); err != nil {
212		panic("invalid semantic version: " + version)
213	}
214	who := unsafe.PreviousRealm().Address()
215	submitted.Set(version, who.String())
216	chain.Emit("VersionSubmitted", "version", version, "by", who.String())
217}
218
219// ---- Render ----------------------------------------------------------------
220
221func Render(path string) string {
222	path = strings.TrimPrefix(path, "/")
223	switch {
224	case path == "":
225		return renderHome()
226	case path == "sorted":
227		return renderSorted()
228	default:
229		parts := strings.SplitN(path, "/", 2)
230		if len(parts) != 2 {
231			return renderHome()
232		}
233		return renderCompare(parts[0], parts[1])
234	}
235}
236
237func renderHome() string {
238	var b strings.Builder
239	b.WriteString("# semver — Semantic Versioning on-chain\n\n")
240	b.WriteString("A port of `golang.org/x/mod/semver`. Parse ")
241	b.WriteString("`vMAJOR.MINOR.PATCH[-prerelease][+build]` and compare with ")
242	b.WriteString("SemVer 2.0.0 precedence.\n\n")
243
244	b.WriteString("## Examples\n\n")
245	b.WriteString("| a | rel | b | note |\n|---|:---:|---|---|\n")
246	examples := [][2]string{
247		{"1.2.3", "1.2.10"},
248		{"1.0.0-alpha", "1.0.0"},
249		{"1.0.0-alpha.1", "1.0.0-alpha.beta"},
250		{"1.0.0-rc.1", "1.0.0"},
251		{"2.0.0", "2.0.0+build.5"},
252	}
253	notes := []string{
254		"numeric patch, not lexical",
255		"pre-release < release",
256		"numeric id < alphanumeric id",
257		"release candidate < final",
258		"build metadata ignored",
259	}
260	for i, ex := range examples {
261		b.WriteString("| `" + ex[0] + "` | " + relSym(Compare(ex[0], ex[1])) +
262			" | `" + ex[1] + "` | " + notes[i] + " |\n")
263	}
264
265	b.WriteString("\n## Try it\n\n")
266	b.WriteString("- Compare two versions: [`/1.2.3/1.2.10`](/r/moul/x/daily/semver:1.2.3/1.2.10)\n")
267	b.WriteString("- Submitted list: [`/sorted`](/r/moul/x/daily/semver:sorted)\n")
268	b.WriteString("- Submit on-chain: `Submit(\"v1.4.2\")`\n")
269	return b.String()
270}
271
272func renderCompare(a, b string) string {
273	var sb strings.Builder
274	sb.WriteString("# Compare\n\n")
275	sb.WriteString(describe("A", a))
276	sb.WriteString(describe("B", b))
277
278	va, ea := Parse(a)
279	vb, eb := Parse(b)
280	if ea != nil || eb != nil {
281		sb.WriteString("\n> One or both inputs are not valid semver.\n")
282		return sb.String()
283	}
284	c := va.compare(vb)
285	sb.WriteString("\n## Result\n\n")
286	sb.WriteString("`" + a + "` **" + word(c) + "** `" + b + "`  ")
287	sb.WriteString("→ `Compare = " + ufmtSigned(c) + "`\n")
288	return sb.String()
289}
290
291func describe(label, s string) string {
292	v, err := Parse(s)
293	if err != nil {
294		return "## " + label + ": `" + s + "`\n\n> invalid\n\n"
295	}
296	pre := "—"
297	if len(v.Pre) > 0 {
298		pre = "`" + strings.Join(v.Pre, ".") + "`"
299	}
300	build := "—"
301	if v.Build != "" {
302		build = "`" + v.Build + "`"
303	}
304	return "## " + label + ": `" + v.Canonical() + "`\n\n" +
305		"| major | minor | patch | pre | build |\n|---|---|---|---|---|\n" +
306		"| " + ufmt(v.Major) + " | " + ufmt(v.Minor) + " | " + ufmt(v.Patch) +
307		" | " + pre + " | " + build + " |\n\n"
308}
309
310func renderSorted() string {
311	var es []entry
312	// Capture the submitter straight from Iterate's callback value, avoiding a
313	// separate avl.Get whose arity differs across gno versions.
314	submitted.Iterate("", "", func(key string, value any) bool {
315		if v, err := Parse(key); err == nil {
316			who, _ := value.(string)
317			es = append(es, entry{v: v, who: who})
318		}
319		return false
320	})
321	if len(es) == 0 {
322		return "# Submitted versions\n\n_None yet._ Call `Submit(\"v1.0.0\")` to add one.\n"
323	}
324	sort.Stable(byPrecedence(es))
325
326	var b strings.Builder
327	b.WriteString("# Submitted versions (lowest → highest)\n\n")
328	b.WriteString("| # | version | submitter |\n|---|---|---|\n")
329	for i, e := range es {
330		b.WriteString("| " + ufmt(i+1) + " | `" + e.v.Canonical() + "` | `" + e.who + "` |\n")
331	}
332	return b.String()
333}
334
335// entry pairs a parsed version with the address that submitted it.
336type entry struct {
337	v   Version
338	who string
339}
340
341// byPrecedence sorts entries ascending. `sort` on-chain has no Slice helper,
342// so we implement sort.Interface.
343type byPrecedence []entry
344
345func (p byPrecedence) Len() int           { return len(p) }
346func (p byPrecedence) Less(i, j int) bool { return p[i].v.compare(p[j].v) < 0 }
347func (p byPrecedence) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }
348
349func relSym(c int) string {
350	switch {
351	case c < 0:
352		return "&lt;"
353	case c > 0:
354		return "&gt;"
355	}
356	return "="
357}
358
359func word(c int) string {
360	switch {
361	case c < 0:
362		return "<"
363	case c > 0:
364		return ">"
365	}
366	return "=="
367}
368
369func ufmtSigned(c int) string {
370	if c < 0 {
371		return "-1"
372	}
373	return ufmt(c)
374}