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))