json.gno
5.16 Kb · 202 lines
1package validator_scores
2
3import (
4 "sort"
5 "strconv"
6 "strings"
7 "unicode/utf8"
8)
9
10const hexDigits = "0123456789abcdef"
11
12// quoteSafe quotes a string the realm validated to need no JSON escaping
13// (addresses, dates, hashes, arena names). Concatenation is cheap in gas;
14// per-byte loops are not.
15func quoteSafe(s string) string { return `"` + s + `"` }
16
17// quoteMoniker quotes a validated moniker (valid UTF-8, no control
18// characters): only '"' and '\' need escaping.
19func quoteMoniker(s string) string {
20 if !strings.ContainsAny(s, `"\`) {
21 return `"` + s + `"`
22 }
23 var b strings.Builder
24 writeJSONString(&b, s)
25 return b.String()
26}
27
28// writeJSONString writes any string as a JSON string. Invalid UTF-8 bytes
29// become U+FFFD. It costs gas per rune: use it only for short or unvalidated
30// input.
31func writeJSONString(b *strings.Builder, s string) {
32 b.WriteByte('"')
33 for i := 0; i < len(s); {
34 r, size := utf8.DecodeRuneInString(s[i:])
35 switch {
36 case r == utf8.RuneError && size == 1:
37 b.WriteString(`\ufffd`)
38 case r == '"':
39 b.WriteString(`\"`)
40 case r == '\\':
41 b.WriteString(`\\`)
42 case r < 0x20:
43 b.WriteString(`\u00`)
44 b.WriteByte(hexDigits[r>>4])
45 b.WriteByte(hexDigits[r&0xf])
46 default:
47 b.WriteString(s[i : i+size])
48 }
49 i += size
50 }
51 b.WriteByte('"')
52}
53
54func writeStrings(b *strings.Builder, list []string) {
55 b.WriteByte('[')
56 for i, s := range list {
57 if i > 0 {
58 b.WriteByte(',')
59 }
60 b.WriteString(quoteSafe(s))
61 }
62 b.WriteByte(']')
63}
64
65// candidate is a stored line split into its fields. Numbers stay in their
66// canonical decimal text, which is valid JSON.
67type candidate struct {
68 operator string
69 moniker string
70 total string
71 rank int
72 rankText string
73 github string
74 arenaScores string // "s1,...,sN"
75}
76
77func decode(line string) candidate {
78 f := strings.Split(line, "|")
79 rank, _ := strconv.Atoi(f[3])
80 return candidate{operator: f[0], moniker: f[1], total: f[2], rank: rank, rankText: f[3], github: f[4], arenaScores: f[5]}
81}
82
83type byRank []candidate
84
85func (s byRank) Len() int { return len(s) }
86func (s byRank) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
87func (s byRank) Less(i, j int) bool {
88 if s[i].rank != s[j].rank {
89 return s[i].rank < s[j].rank
90 }
91 if s[i].moniker != s[j].moniker {
92 return s[i].moniker < s[j].moniker
93 }
94 return s[i].operator < s[j].operator
95}
96
97// sortedCandidates lists an edition's candidates by rank, then moniker
98// (byte order), then operator.
99func sortedCandidates(e *edition) []candidate {
100 list := make([]candidate, len(e.lines))
101 for i, line := range e.lines {
102 list[i] = decode(line)
103 }
104 sort.Sort(byRank(list))
105 return list
106}
107
108func latestSealed() *edition {
109 for i := len(editions) - 1; i >= 0; i-- {
110 if editions[i].sealed {
111 return editions[i]
112 }
113 }
114 return nil
115}
116
117// scoreFields is `"moniker":M,"total":T,"rank":R,"github":G`, shared by
118// both JSON shapes.
119func scoreFields(c candidate) string {
120 return `"moniker":` + quoteMoniker(c.moniker) + `,"total":` + c.total + `,"rank":` + c.rankText + `,"github":` + c.github
121}
122
123func editionJSON(e *edition) string {
124 var b strings.Builder
125 b.WriteString(`{"number":` + strconv.Itoa(e.number) + `,"computedAt":` + quoteSafe(e.computedAt) +
126 `,"scorecardSHA256":` + quoteSafe(e.scorecardSHA256) + `,"arenas":`)
127 writeStrings(&b, e.arenas)
128 b.WriteString(`,"candidates":[`)
129 for i, c := range sortedCandidates(e) {
130 if i > 0 {
131 b.WriteByte(',')
132 }
133 b.WriteString(`{"operator":` + quoteSafe(c.operator) + `,` + scoreFields(c) + `,"arenaScores":[` + c.arenaScores + `]}`)
134 }
135 b.WriteString(`]}`)
136 return b.String()
137}
138
139// LatestJSON returns the latest sealed edition, or "null".
140func LatestJSON() string {
141 e := latestSealed()
142 if e == nil {
143 return "null"
144 }
145 return editionJSON(e)
146}
147
148// EditionJSON returns sealed edition n, or "null".
149func EditionJSON(n int) string {
150 if n < 1 || n > len(editions) || !editions[n-1].sealed {
151 return "null"
152 }
153 return editionJSON(editions[n-1])
154}
155
156// CandidateJSON returns the operator's entries across sealed editions, in
157// edition order; "entries" is [] for an unknown address.
158func CandidateJSON(operator string) string {
159 var b strings.Builder
160 b.WriteString(`{"operator":`)
161 writeJSONString(&b, operator)
162 b.WriteString(`,"entries":[`)
163 first := true
164 for _, e := range editions {
165 if !e.sealed {
166 continue
167 }
168 line, ok := e.find(operator)
169 if !ok {
170 continue
171 }
172 if !first {
173 b.WriteByte(',')
174 }
175 first = false
176 c := decode(line)
177 b.WriteString(`{"edition":` + strconv.Itoa(e.number) + `,"computedAt":` + quoteSafe(e.computedAt) + `,` + scoreFields(c) + `,"arenas":`)
178 writeStrings(&b, e.arenas)
179 b.WriteString(`,"arenaScores":[` + c.arenaScores + `]}`)
180 }
181 b.WriteString(`]}`)
182 return b.String()
183}
184
185// OpenEditionJSON returns the open edition (number, scorecard hash and the
186// operators already added, in the order added), or "null".
187func OpenEditionJSON() string {
188 e := openEdition()
189 if e == nil {
190 return "null"
191 }
192 var b strings.Builder
193 b.WriteString(`{"number":` + strconv.Itoa(e.number) + `,"scorecardSHA256":` + quoteSafe(e.scorecardSHA256) + `,"operators":[`)
194 for i, line := range e.lines {
195 if i > 0 {
196 b.WriteByte(',')
197 }
198 b.WriteString(quoteSafe(line[:operatorLen]))
199 }
200 b.WriteString(`]}`)
201 return b.String()
202}