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