semverdemo.gno
6.00 Kb · 195 lines
1// Package semverdemo is a small on-chain demo of the semantic-versioning
2// parser/comparator provided by the
3// [p/moul/x/daily/semver](/p/moul/x/daily/semver/v0) library.
4//
5// It holds the realm state the pure library deliberately does not: a
6// "submitted versions" board (an avl tree of version string -> submitter
7// address) that anyone can post to with [Submit], plus a gnoweb [Render] view
8// that lists them in SemVer precedence order and compares arbitrary pairs. All
9// parsing and comparison math lives in the library; this realm is just the
10// on-chain wiring and the view.
11package semverdemo
12
13import (
14 "sort"
15 "strconv"
16 "strings"
17
18 "chain"
19 "chain/runtime/unsafe"
20
21 "gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/md/v0"
22 "gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/x/daily/semver/v0"
23 "gno.land/p/nt/avl/v0"
24)
25
26// submitted holds versions users have posted on-chain: version string -> submitter.
27var submitted = avl.NewTree()
28
29// Submit stores a valid version on-chain, tagged with the caller's address, so
30// Render("/sorted") can list everyone's submissions in precedence order. It
31// validates the input with [semver.Parse] and panics on anything malformed.
32func Submit(cur realm, version string) {
33 if _, err := semver.Parse(version); err != nil {
34 panic("invalid semantic version: " + version)
35 }
36 who := unsafe.PreviousRealm().Address()
37 submitted.Set(version, who.String())
38 chain.Emit("VersionSubmitted", "version", version, "by", who.String())
39}
40
41// ---- Render ----------------------------------------------------------------
42
43func Render(path string) string {
44 path = strings.TrimPrefix(path, "/")
45 switch {
46 case path == "":
47 return renderHome()
48 case path == "sorted":
49 return renderSorted()
50 default:
51 parts := strings.SplitN(path, "/", 2)
52 if len(parts) != 2 {
53 return renderHome()
54 }
55 return renderCompare(parts[0], parts[1])
56 }
57}
58
59func renderHome() string {
60 var b strings.Builder
61 b.WriteString("# semver — Semantic Versioning on-chain\n\n")
62 b.WriteString("Demo of the [`p/moul/x/daily/semver`](/p/moul/x/daily/semver/v0) ")
63 b.WriteString("library — a port of `golang.org/x/mod/semver`. Parse ")
64 b.WriteString("`vMAJOR.MINOR.PATCH[-prerelease][+build]` and compare with ")
65 b.WriteString("SemVer 2.0.0 precedence.\n\n")
66
67 b.WriteString("## Examples\n\n")
68 b.WriteString("| a | rel | b | note |\n|---|:---:|---|---|\n")
69 examples := [][2]string{
70 {"1.2.3", "1.2.10"},
71 {"1.0.0-alpha", "1.0.0"},
72 {"1.0.0-alpha.1", "1.0.0-alpha.beta"},
73 {"1.0.0-rc.1", "1.0.0"},
74 {"2.0.0", "2.0.0+build.5"},
75 }
76 notes := []string{
77 "numeric patch, not lexical",
78 "pre-release < release",
79 "numeric id < alphanumeric id",
80 "release candidate < final",
81 "build metadata ignored",
82 }
83 for i, ex := range examples {
84 b.WriteString("| `" + ex[0] + "` | " + relSym(semver.Compare(ex[0], ex[1])) +
85 " | `" + ex[1] + "` | " + notes[i] + " |\n")
86 }
87
88 b.WriteString("\n## Try it\n\n")
89 b.WriteString("- Compare two versions: [`/1.2.3/1.2.10`](/r/moul/x/daily/semverdemo/v0:1.2.3/1.2.10)\n")
90 b.WriteString("- Submitted list: [`/sorted`](/r/moul/x/daily/semverdemo/v0:sorted)\n")
91 b.WriteString("- Submit on-chain: `Submit(\"v1.4.2\")`\n")
92 return b.String()
93}
94
95func renderCompare(a, b string) string {
96 var sb strings.Builder
97 sb.WriteString("# Compare\n\n")
98 sb.WriteString(describe("A", a))
99 sb.WriteString(describe("B", b))
100
101 _, ea := semver.Parse(a)
102 _, eb := semver.Parse(b)
103 if ea != nil || eb != nil {
104 sb.WriteString("\n> One or both inputs are not valid semver.\n")
105 return sb.String()
106 }
107 c := semver.Compare(a, b)
108 sb.WriteString("\n## Result\n\n")
109 //gnovet:ignore render-path-in-code-span a and b passed semver.Parse above, so neither can carry a backtick
110 sb.WriteString("`" + a + "` **" + word(c) + "** `" + b + "` ")
111 sb.WriteString("→ `Compare = " + strconv.Itoa(c) + "`\n")
112 return sb.String()
113}
114
115func describe(label, s string) string {
116 v, err := semver.Parse(s)
117 if err != nil {
118 return "## " + label + ": " + md.InlineCode(s) + "\n\n> invalid\n\n"
119 }
120 pre := "—"
121 if len(v.Pre) > 0 {
122 pre = "`" + strings.Join(v.Pre, ".") + "`"
123 }
124 build := "—"
125 if v.Build != "" {
126 //gnovet:ignore render-path-in-code-span Build passed semver.Parse, whose build metadata is [0-9A-Za-z-.] only
127 build = "`" + v.Build + "`"
128 }
129 return "## " + label + ": `" + v.Canonical() + "`\n\n" +
130 "| major | minor | patch | pre | build |\n|---|---|---|---|---|\n" +
131 "| " + strconv.Itoa(v.Major) + " | " + strconv.Itoa(v.Minor) + " | " + strconv.Itoa(v.Patch) +
132 " | " + pre + " | " + build + " |\n\n"
133}
134
135func renderSorted() string {
136 var es []entry
137 // Capture the submitter straight from Iterate's callback value, avoiding a
138 // separate avl.Get whose arity differs across gno versions.
139 submitted.Iterate("", "", func(key string, value any) bool {
140 if v, err := semver.Parse(key); err == nil {
141 who, _ := value.(string)
142 es = append(es, entry{v: v, who: who})
143 }
144 return false
145 })
146 if len(es) == 0 {
147 return "# Submitted versions\n\n_None yet._ Call `Submit(\"v1.0.0\")` to add one.\n"
148 }
149 sort.Stable(byPrecedence(es))
150
151 var b strings.Builder
152 b.WriteString("# Submitted versions (lowest → highest)\n\n")
153 b.WriteString("| # | version | submitter |\n|---|---|---|\n")
154 for i, e := range es {
155 b.WriteString("| " + strconv.Itoa(i+1) + " | `" + e.v.Canonical() + "` | `" + e.who + "` |\n")
156 }
157 return b.String()
158}
159
160// entry pairs a parsed version with the address that submitted it.
161type entry struct {
162 v semver.Version
163 who string
164}
165
166// byPrecedence sorts entries ascending. `sort` on-chain has no Slice helper,
167// so we implement sort.Interface; ordering defers to semver.Compare on the
168// original strings.
169type byPrecedence []entry
170
171func (p byPrecedence) Len() int { return len(p) }
172func (p byPrecedence) Less(i, j int) bool {
173 return semver.Compare(p[i].v.Orig, p[j].v.Orig) < 0
174}
175func (p byPrecedence) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
176
177func relSym(c int) string {
178 switch {
179 case c < 0:
180 return "<"
181 case c > 0:
182 return ">"
183 }
184 return "="
185}
186
187func word(c int) string {
188 switch {
189 case c < 0:
190 return "<"
191 case c > 0:
192 return ">"
193 }
194 return "=="
195}