migrate.bend
bend-kit-migrate@0.2.0.0 · 696 lines · 0 definitions
1# Versioned subscription records: validated JSON decode, proved migrations, and encode. Source: https://github.com/paymog/bend-kit/tree/main/migrate
2import Base
3import bend-kit-bytes@0.3.2.0/bytes.bend as Bytes
4import bend-kit-json@0.6.1.1/json.bend as Json
5
6type Period is Data:
7 Monthly{}
8 Yearly{}
9
10type Status is Data:
11 Active{}
12 Paused{}
13 Cancelled{}
14
15# v1: a monthly price and an on/off flag.
16type V1 is Type:
17 V1{id: U32, plan: Bytes.Bytes, monthly_cents: U32, active: Bool}
18
19# v2: a price per billing period, a three-state status, and an optional support note.
20type V2 is Type:
21 V2{id: U32, plan: Bytes.Bytes, cents: U32, period: Period, status: Status, note: Maybe<&1, Bytes.Bytes>}
22
23# v3: v2 without the support note.
24type V3 is Type:
25 V3{id: U32, plan: Bytes.Bytes, cents: U32, period: Period, status: Status}
26
27# A decoded record and the version it was written in.
28type Record is Type:
29 R1{r: V1}
30 R2{r: V2}
31 R3{r: V3}
32
33# What a subscription means to billing: who, which plan, the price per period, and the status.
34type Terms is Type:
35 Terms{id: U32, plan: Bytes.Bytes, cents: U32, period: Period, status: Status}
36
37# What v2 to v3 discards.
38type Dropped is Type:
39 Dropped{note: Maybe<&1, Bytes.Bytes>}
40
41# The JSON field names of every version.
42type Key is Data:
43 KVersion{}
44 KId{}
45 KPlan{}
46 KMonthlyCents{}
47 KActive{}
48 KCents{}
49 KPeriod{}
50 KStatus{}
51 KNote{}
52
53type Error is Type:
54 Malformed{}
55 Unsupported{version: U32}
56 Unknown{key: Bytes.Bytes}
57 Unexpected{key: Key}
58 Duplicate{key: Key}
59 Missing{key: Key}
60 BadField{key: Key}
61 Invalid{key: Key}
62 NotInV1{key: Key}
63
64# Meaning
65# -------
66
67# v1 had no pause: an inactive v1 subscription is cancelled.
68def Status.of_active(a: Bool) -> Status:
69 match a:
70 case True{}:
71 Active{}
72 case False{}:
73 Cancelled{}
74
75def V1.terms(r: V1) -> Terms:
76 V1{i, p, c, a} = r
77 Terms{i, p, c, Monthly{}, Status.of_active(a)}
78
79def V2.terms(r: V2) -> Terms:
80 V2{i, p, c, pe, s, n} = r
81 Terms{i, p, c, pe, s}
82
83def V3.terms(r: V3) -> Terms:
84 V3{i, p, c, pe, s} = r
85 Terms{i, p, c, pe, s}
86
87def Record.terms(r: Record) -> Terms:
88 match r:
89 case R1{x}:
90 V1.terms(x)
91 case R2{x}:
92 V2.terms(x)
93 case R3{x}:
94 V3.terms(x)
95
96# The first business rule broken by a plan of len bytes and a price of c: an empty plan or a zero price.
97def Terms.problem(+len: U32, +c: U32) -> Maybe<&2, Key>:
98 Bool.pick(Maybe<&2, Key>, U32.is_eq(len, 0), Some{KPlan{}}, Bool.pick(Maybe<&2, Key>, U32.is_eq(c, 0), Some{KCents{}}, None{}))
99
100def none(m: Maybe<&2, Key>) -> Bool:
101 match m:
102 case None{}:
103 True{}
104 case Some{k}:
105 False{}
106
107def Terms.valid(t: Terms) -> Bool:
108 Terms{i, p, +c, pe, s} = t
109 Bytes.Bytes{+len, buf} = p
110 none(Terms.problem(len, c))
111
112# The record back, and the first business rule it breaks.
113def Record.problem(r: Record) -> Record & Maybe<&2, Key>:
114 match r:
115 case R1{V1{i, Bytes.Bytes{+len, buf}, +c, a}}:
116 (R1{V1{i, Bytes.Bytes{len, buf}, c, a}}, Terms.problem(len, c))
117 case R2{V2{i, Bytes.Bytes{+len, buf}, +c, pe, s, n}}:
118 (R2{V2{i, Bytes.Bytes{len, buf}, c, pe, s, n}}, Terms.problem(len, c))
119 case R3{V3{i, Bytes.Bytes{+len, buf}, +c, pe, s}}:
120 (R3{V3{i, Bytes.Bytes{len, buf}, c, pe, s}}, Terms.problem(len, c))
121
122def Record.valid.of(p: Record & Maybe<&2, Key>) -> Bool:
123 (r, m) = p
124 none(m)
125
126def Record.valid(r: Record) -> Bool:
127 Record.valid.of(Record.problem(r))
128
129# Migrations
130# ----------
131
132# v1 to v2. Total; V2.down undoes it.
133def V1.up(r: V1) -> V2:
134 V1{i, p, c, a} = r
135 V2{i, p, c, Monthly{}, Status.of_active(a), None{}}
136
137def V2.down.note(n: Maybe<&1, Bytes.Bytes>, i: U32, p: Bytes.Bytes, c: U32, a: Bool) -> Result<&1, &1, Error, V1>:
138 match n:
139 case None{}:
140 Done{V1{i, p, c, a}}
141 case Some{b}:
142 Fail{NotInV1{KNote{}}}
143
144def V2.down.status(s: Status, i: U32, p: Bytes.Bytes, c: U32, n: Maybe<&1, Bytes.Bytes>) -> Result<&1, &1, Error, V1>:
145 match s:
146 case Active{}:
147 V2.down.note(n, i, p, c, True{})
148 case Paused{}:
149 Fail{NotInV1{KStatus{}}}
150 case Cancelled{}:
151 V2.down.note(n, i, p, c, False{})
152
153def V2.down.period(pe: Period, i: U32, p: Bytes.Bytes, c: U32, s: Status, n: Maybe<&1, Bytes.Bytes>) -> Result<&1, &1, Error, V1>:
154 match pe:
155 case Monthly{}:
156 V2.down.status(s, i, p, c, n)
157 case Yearly{}:
158 Fail{NotInV1{KPeriod{}}}
159
160# v2 to v1, for a monthly record that is not paused and has no note.
161def V2.down(r: V2) -> Result<&1, &1, Error, V1>:
162 V2{i, p, c, pe, s, n} = r
163 V2.down.period(pe, i, p, c, s, n)
164
165# v2 to v3. Lossy: it drops the note and returns it.
166def V2.up(r: V2) -> V3 & Dropped:
167 V2{i, p, c, pe, s, n} = r
168 (V3{i, p, c, pe, s}, Dropped{n})
169
170# The v2 record that V2.up turned into r and d.
171def V3.restore(r: V3, d: Dropped) -> V2:
172 V3{i, p, c, pe, s} = r
173 Dropped{n} = d
174 V2{i, p, c, pe, s, n}
175
176# v3 to v2, with no note.
177def V3.down(r: V3) -> V2:
178 V3.restore(r, Dropped{None{}})
179
180# Any version to v3, with what the chain dropped.
181def Record.upgrade(r: Record) -> V3 & Dropped:
182 match r:
183 case R1{x}:
184 V2.up(V1.up(x))
185 case R2{x}:
186 V2.up(x)
187 case R3{x}:
188 (x, Dropped{None{}})
189
190# Names as bytes
191# --------------
192
193# A name of at most 16 bytes: its length and its first four little-endian words, zero past the end.
194type Tag is Data:
195 Tag{len: U32, w0: U32, w1: U32, w2: U32, w3: U32}
196
197def Tag.eq(a: Tag, b: Tag) -> Bool:
198 Tag{+l, +a0, +a1, +a2, +a3} = a
199 Tag{+m, +b0, +b1, +b2, +b3} = b
200 U32.is_eq(l, m) && U32.is_eq(a0, b0) && U32.is_eq(a1, b1) && U32.is_eq(a2, b2) && U32.is_eq(a3, b3)
201
202def Tag.word(ok: Bool, a: Array<U32>, +i: U32) -> Array<U32> & U32:
203 match ok:
204 case True{}:
205 Array.get(U32, a, i)
206 case False{}:
207 (a, 0)
208
209def Tag.of.w3(+len: U32, +w0: U32, +w1: U32, +w2: U32, r: Array<U32> & U32) -> Bytes.Bytes & Tag:
210 (a, +w3) = r
211 (Bytes.Bytes{len, a}, Tag{len, w0, w1, w2, w3})
212
213def Tag.of.w2(+len: U32, +n: U32, +w0: U32, +w1: U32, r: Array<U32> & U32) -> Bytes.Bytes & Tag:
214 (a, +w2) = r
215 Tag.of.w3(len, w0, w1, w2, Tag.word(U32.is_lt(3, n), a, 3))
216
217def Tag.of.w1(+len: U32, +n: U32, +w0: U32, r: Array<U32> & U32) -> Bytes.Bytes & Tag:
218 (a, +w1) = r
219 Tag.of.w2(len, n, w0, w1, Tag.word(U32.is_lt(2, n), a, 2))
220
221def Tag.of.w0(+len: U32, +n: U32, r: Array<U32> & U32) -> Bytes.Bytes & Tag:
222 (a, +w0) = r
223 Tag.of.w1(len, n, w0, Tag.word(U32.is_lt(1, n), a, 1))
224
225# The bytes back, and their tag. Bytes past 16 are not read, but the length still differs.
226def Tag.of(b: Bytes.Bytes) -> Bytes.Bytes & Tag:
227 Bytes.Bytes{+len, buf} = b
228 +n = Bytes.words(len)
229 Tag.of.w0(len, n, Tag.word(U32.is_lt(0, n), buf, 0))
230
231def Tag.set(ok: Bool, a: Array<U32>, +i: U32, +w: U32) -> Array<U32>:
232 match ok:
233 case True{}:
234 Array.set(U32, a, i, w)
235 case False{}:
236 a
237
238def Tag.bytes(t: Tag) -> Bytes.Bytes:
239 Tag{+len, +w0, +w1, +w2, +w3} = t
240 +n = Bytes.words(len)
241 Bytes.Bytes{len, Tag.set(U32.is_lt(3, n), Tag.set(U32.is_lt(2, n), Tag.set(U32.is_lt(1, n), Tag.set(U32.is_lt(0, n), Bytes.alloc(len), 0, w0), 1, w1), 2, w2), 3, w3)}
242
243def Key.tag(k: Key) -> Tag:
244 match k:
245 case KVersion{}:
246 Tag{7, 1936876918, 7237481, 0, 0}
247 case KId{}:
248 Tag{2, 25705, 0, 0, 0}
249 case KPlan{}:
250 Tag{4, 1851878512, 0, 0, 0}
251 case KMonthlyCents{}:
252 Tag{13, 1953394541, 1601793128, 1953391971, 115}
253 case KActive{}:
254 Tag{6, 1769235297, 25974, 0, 0}
255 case KCents{}:
256 Tag{5, 1953391971, 115, 0, 0}
257 case KPeriod{}:
258 Tag{6, 1769104752, 25711, 0, 0}
259 case KStatus{}:
260 Tag{6, 1952543859, 29557, 0, 0}
261 case KNote{}:
262 Tag{4, 1702129518, 0, 0, 0}
263
264# The key as UTF-8 JSON text.
265def Key.bytes(k: Key) -> Bytes.Bytes:
266 Tag.bytes(Key.tag(k))
267
268def Key.is(k: Key, +t: Tag, rest: Maybe<&2, Key>) -> Maybe<&2, Key>:
269 +k = k
270 Bool.pick(Maybe<&2, Key>, Tag.eq(Key.tag(k), t), Some{k}, rest)
271
272def Key.find(+t: Tag) -> Maybe<&2, Key>:
273 Key.is(KVersion{}, t, Key.is(KId{}, t, Key.is(KPlan{}, t, Key.is(KMonthlyCents{}, t, Key.is(KActive{}, t, Key.is(KCents{}, t, Key.is(KPeriod{}, t, Key.is(KStatus{}, t, Key.is(KNote{}, t, None{})))))))))
274
275def Period.tag(p: Period) -> Tag:
276 match p:
277 case Monthly{}:
278 Tag{7, 1953394541, 7957608, 0, 0}
279 case Yearly{}:
280 Tag{6, 1918985593, 31084, 0, 0}
281
282def Status.tag(s: Status) -> Tag:
283 match s:
284 case Active{}:
285 Tag{6, 1769235297, 25974, 0, 0}
286 case Paused{}:
287 Tag{6, 1937072496, 25701, 0, 0}
288 case Cancelled{}:
289 Tag{9, 1668178275, 1701604453, 100, 0}
290
291def Period.find(+t: Tag) -> Maybe<&2, Period>:
292 Bool.pick(Maybe<&2, Period>, Tag.eq(Period.tag(Monthly{}), t), Some{Monthly{}}, Bool.pick(Maybe<&2, Period>, Tag.eq(Period.tag(Yearly{}), t), Some{Yearly{}}, None{}))
293
294def Status.find(+t: Tag) -> Maybe<&2, Status>:
295 Bool.pick(Maybe<&2, Status>, Tag.eq(Status.tag(Active{}), t), Some{Active{}}, Bool.pick(Maybe<&2, Status>, Tag.eq(Status.tag(Paused{}), t), Some{Paused{}}, Bool.pick(Maybe<&2, Status>, Tag.eq(Status.tag(Cancelled{}), t), Some{Cancelled{}}, None{})))
296
297# Numbers as bytes
298# ----------------
299
300def u32.over(+acc: U32, +d: U32) -> Bool:
301 Bool.or(U32.is_lt(429496729, acc), Bool.and(U32.is_eq(acc, 429496729), U32.is_lt(5, d)))
302
303def u32.end(ok: Bool, +acc: U32) -> Maybe<&2, U32>:
304 match ok:
305 case True{}:
306 Some{acc}
307 case False{}:
308 None{}
309
310def u32.go(n: Nat, r: Array<U32> & U32, +i: U32, +acc: U32, ok: Bool) -> Maybe<&2, U32>:
311 match n:
312 case 0n:
313 u32.end(ok, acc)
314 case 1n+p:
315 (a, +c) = r
316 +d = (c - 48 : U32)
317 u32.go(p, Bytes.peek(a, (i + 1 : U32)), (i + 1 : U32), (acc * 10 + d : U32), ok && U32.is_lt(d, 10) && Bool.not(u32.over(acc, d)))
318
319def u32.first(+len: U32, r: Array<U32> & U32) -> Maybe<&2, U32>:
320 (a, +c) = r
321 +lead0 = U32.is_eq(c, 48) && U32.is_lt(1, len)
322 u32.go(U32.to_nat(len), (a, c), 0, 0, U32.is_lt(0, len) && U32.is_lt(len, 11) && Bool.not(lead0))
323
324# Decimal digits with no sign, fraction, exponent, or leading zero, below 2^32.
325def u32.read(b: Bytes.Bytes) -> Maybe<&2, U32>:
326 Bytes.Bytes{+len, buf} = b
327 u32.first(len, Bytes.peek(buf, 0))
328
329def u32.width(+n: U32) -> U32:
330 Bool.pick(U32, U32.is_lt(n, 10), 1, Bool.pick(U32, U32.is_lt(n, 100), 2, Bool.pick(U32, U32.is_lt(n, 1000), 3, Bool.pick(U32, U32.is_lt(n, 10000), 4, Bool.pick(U32, U32.is_lt(n, 100000), 5, Bool.pick(U32, U32.is_lt(n, 1000000), 6, Bool.pick(U32, U32.is_lt(n, 10000000), 7, Bool.pick(U32, U32.is_lt(n, 100000000), 8, Bool.pick(U32, U32.is_lt(n, 1000000000), 9, 10)))))))))
331
332def u32.fill(f: Nat, a: Array<U32>, +i: U32, +n: U32) -> Array<U32>:
333 match f:
334 case 0n:
335 a
336 case 1n+p:
337 +q = (n / 10 : U32)
338 u32.fill(p, Bytes.poke(a, i, (48 + n - q * 10 : U32)), (i - 1 : U32), q)
339
340# n in decimal digits.
341def u32.text(+n: U32) -> Bytes.Bytes:
342 +k = u32.width(n)
343 Bytes.Bytes{k, u32.fill(U32.to_nat(k), Bytes.alloc(k), (k - 1 : U32), n)}
344
345# Decode
346# ------
347
348# A JSON field value, reduced to what records use. A number keeps its digits until the record is built.
349type Scalar is Type:
350 SNum{s: Bytes.Bytes}
351 SStr{s: Bytes.Bytes}
352 SFlag{b: Bool}
353 SOther{}
354
355def scalar(v: Json.Val) -> Scalar:
356 match v:
357 case Json.Num{s}:
358 SNum{s}
359 case Json.Str{s}:
360 SStr{s}
361 case Json.Flag{b}:
362 SFlag{b}
363 case Json.Null{}:
364 SOther{}
365 case Json.Arr{xs}:
366 SOther{}
367 case Json.Obj{kvs}:
368 SOther{}
369
370# Every field of a record object, by key, before the version picks which ones it allows.
371type Slots is Type:
372 Slots{version: Maybe<&1, Scalar>, id: Maybe<&1, Scalar>, plan: Maybe<&1, Scalar>, monthly_cents: Maybe<&1, Scalar>, active: Maybe<&1, Scalar>, cents: Maybe<&1, Scalar>, period: Maybe<&1, Scalar>, status: Maybe<&1, Scalar>, note: Maybe<&1, Scalar>}
373
374def Slots.empty() -> Slots:
375 Slots{None{}, None{}, None{}, None{}, None{}, None{}, None{}, None{}, None{}}
376
377def slot.fill(m: Maybe<&1, Scalar>, x: Scalar, k: Key) -> Result<&1, &1, Error, Maybe<&1, Scalar>>:
378 match m:
379 case None{}:
380 Done{Some{x}}
381 case Some{y}:
382 Fail{Duplicate{k}}
383
384def Slots.set(k: Key, x: Scalar, s: Slots) -> Result<&1, &1, Error, Slots>:
385 match k:
386 case KVersion{}:
387 Slots{ve, i, p, mc, a, c, pe, st, n} = s
388 Result.map(&1, &1, Error, Maybe<&1, Scalar>, Slots, m => Slots{m, i, p, mc, a, c, pe, st, n}, slot.fill(ve, x, KVersion{}))
389 case KId{}:
390 Slots{ve, i, p, mc, a, c, pe, st, n} = s
391 Result.map(&1, &1, Error, Maybe<&1, Scalar>, Slots, m => Slots{ve, m, p, mc, a, c, pe, st, n}, slot.fill(i, x, KId{}))
392 case KPlan{}:
393 Slots{ve, i, p, mc, a, c, pe, st, n} = s
394 Result.map(&1, &1, Error, Maybe<&1, Scalar>, Slots, m => Slots{ve, i, m, mc, a, c, pe, st, n}, slot.fill(p, x, KPlan{}))
395 case KMonthlyCents{}:
396 Slots{ve, i, p, mc, a, c, pe, st, n} = s
397 Result.map(&1, &1, Error, Maybe<&1, Scalar>, Slots, m => Slots{ve, i, p, m, a, c, pe, st, n}, slot.fill(mc, x, KMonthlyCents{}))
398 case KActive{}:
399 Slots{ve, i, p, mc, a, c, pe, st, n} = s
400 Result.map(&1, &1, Error, Maybe<&1, Scalar>, Slots, m => Slots{ve, i, p, mc, m, c, pe, st, n}, slot.fill(a, x, KActive{}))
401 case KCents{}:
402 Slots{ve, i, p, mc, a, c, pe, st, n} = s
403 Result.map(&1, &1, Error, Maybe<&1, Scalar>, Slots, m => Slots{ve, i, p, mc, a, m, pe, st, n}, slot.fill(c, x, KCents{}))
404 case KPeriod{}:
405 Slots{ve, i, p, mc, a, c, pe, st, n} = s
406 Result.map(&1, &1, Error, Maybe<&1, Scalar>, Slots, m => Slots{ve, i, p, mc, a, c, m, st, n}, slot.fill(pe, x, KPeriod{}))
407 case KStatus{}:
408 Slots{ve, i, p, mc, a, c, pe, st, n} = s
409 Result.map(&1, &1, Error, Maybe<&1, Scalar>, Slots, m => Slots{ve, i, p, mc, a, c, pe, m, n}, slot.fill(st, x, KStatus{}))
410 case KNote{}:
411 Slots{ve, i, p, mc, a, c, pe, st, n} = s
412 Result.map(&1, &1, Error, Maybe<&1, Scalar>, Slots, m => Slots{ve, i, p, mc, a, c, pe, st, m}, slot.fill(n, x, KNote{}))
413
414def Slots.put(m: Maybe<&2, Key>, k: Bytes.Bytes, x: Scalar, s: Slots) -> Result<&1, &1, Error, Slots>:
415 match m:
416 case Some{key}:
417 Slots.set(key, x, s)
418 case None{}:
419 Fail{Unknown{k}}
420
421def Slots.key(p: Bytes.Bytes & Tag, x: Scalar, s: Slots) -> Result<&1, &1, Error, Slots>:
422 (k, +t) = p
423 Slots.put(Key.find(t), k, x, s)
424
425def collect.put(acc: Result<&1, &1, Error, Slots>, k: Bytes.Bytes, v: Json.Val) -> Result<&1, &1, Error, Slots>:
426 match acc:
427 case Fail{e}:
428 Fail{e}
429 case Done{s}:
430 Slots.key(Tag.of(k), scalar(v), s)
431
432# Files each field under its key. An unknown or repeated key stops the walk.
433def collect(kvs: List<&1, Bytes.Bytes & Json.Val>, acc: Result<&1, &1, Error, Slots>) -> Result<&1, &1, Error, Slots>:
434 match kvs:
435 case Nil{}:
436 acc
437 case Con{(k, v), t}:
438 collect(t, collect.put(acc, k, v))
439
440def need.num(m: Maybe<&1, Scalar>, k: Key) -> Result<&1, &1, Error, Bytes.Bytes>:
441 match m:
442 case None{}:
443 Fail{Missing{k}}
444 case Some{SNum{b}}:
445 Done{b}
446 case Some{s}:
447 Fail{BadField{k}}
448
449def need.flag(m: Maybe<&1, Scalar>, k: Key) -> Result<&1, &1, Error, Bool>:
450 match m:
451 case None{}:
452 Fail{Missing{k}}
453 case Some{SFlag{b}}:
454 Done{b}
455 case Some{s}:
456 Fail{BadField{k}}
457
458def need.str(m: Maybe<&1, Scalar>, k: Key) -> Result<&1, &1, Error, Bytes.Bytes>:
459 match m:
460 case None{}:
461 Fail{Missing{k}}
462 case Some{SStr{b}}:
463 Done{b}
464 case Some{s}:
465 Fail{BadField{k}}
466
467def opt.str(m: Maybe<&1, Scalar>, k: Key) -> Result<&1, &1, Error, Maybe<&1, Bytes.Bytes>>:
468 match m:
469 case None{}:
470 Done{None{}}
471 case Some{SStr{b}}:
472 Done{Some{b}}
473 case Some{s}:
474 Fail{BadField{k}}
475
476# A field that this version does not define.
477def unexpected(m: Maybe<&1, Scalar>, k: Key) -> Result<&1, &1, Error, Unit>:
478 match m:
479 case None{}:
480 Done{Unit{}}
481 case Some{s}:
482 Fail{Unexpected{k}}
483
484def found(-A: Data, m: Maybe<&2, A>, k: Key) -> Result<&1, &1, Error, A>:
485 match m:
486 case Some{x}:
487 Done{x}
488 case None{}:
489 Fail{BadField{k}}
490
491def name.period(p: Bytes.Bytes & Tag) -> Result<&1, &1, Error, Period>:
492 (b, +t) = p
493 found(Period, Period.find(t), KPeriod{})
494
495def name.status(p: Bytes.Bytes & Tag) -> Result<&1, &1, Error, Status>:
496 (b, +t) = p
497 found(Status, Status.find(t), KStatus{})
498
499def need.period(m: Maybe<&1, Scalar>) -> Result<&1, &1, Error, Period>:
500 match m:
501 case None{}:
502 Fail{Missing{KPeriod{}}}
503 case Some{SStr{b}}:
504 name.period(Tag.of(b))
505 case Some{s}:
506 Fail{BadField{KPeriod{}}}
507
508# An absent v2 period is monthly.
509def opt.period(m: Maybe<&1, Scalar>) -> Result<&1, &1, Error, Period>:
510 match m:
511 case None{}:
512 Done{Monthly{}}
513 case Some{SStr{b}}:
514 name.period(Tag.of(b))
515 case Some{s}:
516 Fail{BadField{KPeriod{}}}
517
518def need.status(m: Maybe<&1, Scalar>) -> Result<&1, &1, Error, Status>:
519 match m:
520 case None{}:
521 Fail{Missing{KStatus{}}}
522 case Some{SStr{b}}:
523 name.status(Tag.of(b))
524 case Some{s}:
525 Fail{BadField{KStatus{}}}
526
527def V1.finish(mi: Maybe<&2, U32>, p: Bytes.Bytes, mc: Maybe<&2, U32>, a: Bool) -> Result<&1, &1, Error, Record>:
528 do Result<&1, &1, Error, Record>:
529 id : U32 <- found(U32, mi, KId{})
530 cents : U32 <- found(U32, mc, KMonthlyCents{})
531 return R1{V1{id, p, cents, a}}
532
533def build.v1(s: Slots) -> Result<&1, &1, Error, Record>:
534 Slots{ve, i, p, mc, a, c, pe, st, n} = s
535 do Result<&1, &1, Error, Record>:
536 u1 : Unit <- unexpected(c, KCents{})
537 u2 : Unit <- unexpected(pe, KPeriod{})
538 u3 : Unit <- unexpected(st, KStatus{})
539 u4 : Unit <- unexpected(n, KNote{})
540 id : Bytes.Bytes <- need.num(i, KId{})
541 plan : Bytes.Bytes <- need.str(p, KPlan{})
542 cents : Bytes.Bytes <- need.num(mc, KMonthlyCents{})
543 on : Bool <- need.flag(a, KActive{})
544 V1.finish(u32.read(id), plan, u32.read(cents), on)
545
546def V2.finish(mi: Maybe<&2, U32>, p: Bytes.Bytes, mc: Maybe<&2, U32>, pe: Period, s: Status, n: Maybe<&1, Bytes.Bytes>) -> Result<&1, &1, Error, Record>:
547 do Result<&1, &1, Error, Record>:
548 id : U32 <- found(U32, mi, KId{})
549 cents : U32 <- found(U32, mc, KCents{})
550 return R2{V2{id, p, cents, pe, s, n}}
551
552def build.v2(s: Slots) -> Result<&1, &1, Error, Record>:
553 Slots{ve, i, p, mc, a, c, pe, st, n} = s
554 do Result<&1, &1, Error, Record>:
555 u1 : Unit <- unexpected(mc, KMonthlyCents{})
556 u2 : Unit <- unexpected(a, KActive{})
557 id : Bytes.Bytes <- need.num(i, KId{})
558 plan : Bytes.Bytes <- need.str(p, KPlan{})
559 cents : Bytes.Bytes <- need.num(c, KCents{})
560 period : Period <- opt.period(pe)
561 status : Status <- need.status(st)
562 note : Maybe<&1, Bytes.Bytes> <- opt.str(n, KNote{})
563 V2.finish(u32.read(id), plan, u32.read(cents), period, status, note)
564
565def V3.finish(mi: Maybe<&2, U32>, p: Bytes.Bytes, mc: Maybe<&2, U32>, pe: Period, s: Status) -> Result<&1, &1, Error, Record>:
566 do Result<&1, &1, Error, Record>:
567 id : U32 <- found(U32, mi, KId{})
568 cents : U32 <- found(U32, mc, KCents{})
569 return R3{V3{id, p, cents, pe, s}}
570
571def build.v3(s: Slots) -> Result<&1, &1, Error, Record>:
572 Slots{ve, i, p, mc, a, c, pe, st, n} = s
573 do Result<&1, &1, Error, Record>:
574 u1 : Unit <- unexpected(mc, KMonthlyCents{})
575 u2 : Unit <- unexpected(a, KActive{})
576 u3 : Unit <- unexpected(n, KNote{})
577 id : Bytes.Bytes <- need.num(i, KId{})
578 plan : Bytes.Bytes <- need.str(p, KPlan{})
579 cents : Bytes.Bytes <- need.num(c, KCents{})
580 period : Period <- need.period(pe)
581 status : Status <- need.status(st)
582 V3.finish(u32.read(id), plan, u32.read(cents), period, status)
583
584def build.at(+v: U32, s: Slots) -> Result<&1, &1, Error, Record>:
585 match v:
586 case 1:
587 build.v1(s)
588 case 2:
589 build.v2(s)
590 case 3:
591 build.v3(s)
592 case _:
593 Fail{Unsupported{v}}
594
595def build.ver(m: Maybe<&2, U32>, s: Slots) -> Result<&1, &1, Error, Record>:
596 match m:
597 case None{}:
598 Fail{BadField{KVersion{}}}
599 case Some{v}:
600 build.at(v, s)
601
602def build.raw(r: Result<&1, &1, Error, Bytes.Bytes>, s: Slots) -> Result<&1, &1, Error, Record>:
603 match r:
604 case Fail{e}:
605 Fail{e}
606 case Done{b}:
607 build.ver(u32.read(b), s)
608
609def build(s: Slots) -> Result<&1, &1, Error, Record>:
610 Slots{ve, i, p, mc, a, c, pe, st, n} = s
611 build.raw(need.num(ve, KVersion{}), Slots{None{}, i, p, mc, a, c, pe, st, n})
612
613# The record an object describes, before the business rules.
614def decode.shape(v: Json.Val) -> Result<&1, &1, Error, Record>:
615 match v:
616 case Json.Obj{kvs}:
617 Result.bind(&1, &1, Error, Slots, Record, collect(kvs, Done{Slots.empty()}), build)
618 case _:
619 Fail{Malformed{}}
620
621def Record.check.if(m: Maybe<&2, Key>, r: Record) -> Result<&1, &1, Error, Record>:
622 match m:
623 case None{}:
624 Done{r}
625 case Some{k}:
626 Fail{Invalid{k}}
627
628def Record.check.of(p: Record & Maybe<&2, Key>) -> Result<&1, &1, Error, Record>:
629 (r, m) = p
630 Record.check.if(m, r)
631
632# r when it keeps the business rules.
633def Record.check(r: Record) -> Result<&1, &1, Error, Record>:
634 Record.check.of(Record.problem(r))
635
636# One record from a parsed JSON value. Only a valid record decodes.
637def decode.val(v: Json.Val) -> Result<&1, &1, Error, Record>:
638 Result.bind(&1, &1, Error, Record, Record, decode.shape(v), Record.check)
639
640def decode.parsed(m: Maybe<&1, Json.Val>) -> Result<&1, &1, Error, Record>:
641 match m:
642 case None{}:
643 Fail{Malformed{}}
644 case Some{v}:
645 decode.val(v)
646
647# One record from UTF-8 JSON: an object nested no deeper than one level.
648def decode(b: Bytes.Bytes) -> Result<&1, &1, Error, Record>:
649 decode.parsed(Json.parse.strict.bytes(b, 1))
650
651# Encode
652# ------
653
654def kv(k: Key, v: Json.Val) -> Bytes.Bytes & Json.Val:
655 (Key.bytes(k), v)
656
657def num(+n: U32) -> Json.Val:
658 Json.Num{u32.text(n)}
659
660def Period.json(p: Period) -> Json.Val:
661 Json.Str{Tag.bytes(Period.tag(p))}
662
663def Status.json(s: Status) -> Json.Val:
664 Json.Str{Tag.bytes(Status.tag(s))}
665
666def V1.json(r: V1) -> Json.Val:
667 V1{+i, p, +c, a} = r
668 Json.Obj{[kv(KVersion{}, num(1)), kv(KId{}, num(i)), kv(KPlan{}, Json.Str{p}), kv(KMonthlyCents{}, num(c)), kv(KActive{}, Json.Flag{a})]}
669
670def V2.note(n: Maybe<&1, Bytes.Bytes>) -> List<&1, Bytes.Bytes & Json.Val>:
671 match n:
672 case None{}:
673 Nil{}
674 case Some{b}:
675 [kv(KNote{}, Json.Str{b})]
676
677def V2.json(r: V2) -> Json.Val:
678 V2{+i, p, +c, pe, s, n} = r
679 Json.Obj{Con{kv(KVersion{}, num(2)), Con{kv(KId{}, num(i)), Con{kv(KPlan{}, Json.Str{p}), Con{kv(KCents{}, num(c)), Con{kv(KPeriod{}, Period.json(pe)), Con{kv(KStatus{}, Status.json(s)), V2.note(n)}}}}}}}
680
681def V3.json(r: V3) -> Json.Val:
682 V3{+i, p, +c, pe, s} = r
683 Json.Obj{[kv(KVersion{}, num(3)), kv(KId{}, num(i)), kv(KPlan{}, Json.Str{p}), kv(KCents{}, num(c)), kv(KPeriod{}, Period.json(pe)), kv(KStatus{}, Status.json(s))]}
684
685# The record as a JSON object in its own version.
686def Record.json(r: Record) -> Json.Val:
687 match r:
688 case R1{x}:
689 V1.json(x)
690 case R2{x}:
691 V2.json(x)
692 case R3{x}:
693 V3.json(x)
694
695def encode(r: Record) -> Bytes.Bytes:
696 Json.encode.bytes(Record.json(r))