1 # Versioned subscription records: validated JSON decode, proved migrations, and encode. Source: https://github.com/paymog/bend-kit/tree/main/migrate 2 import Base 3 import bend-kit-bytes@0.3.3.0/bytes.bend as Bytes 4 import bend-kit-json@0.7.0.0/json.bend as Json 5 6 type Period is Data: 7 Monthly{} 8 Yearly{} 9 10 type Status is Data: 11 Active{} 12 Paused{} 13 Cancelled{} 14 15 # v1: a monthly price and an on/off flag. 16 type 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. 20 type 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. 24 type 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. 28 type 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. 34 type Terms is Type: 35 Terms{id: U32, plan: Bytes.Bytes, cents: U32, period: Period, status: Status} 36 37 # What v2 to v3 discards. 38 type Dropped is Type: 39 Dropped{note: Maybe<&1, Bytes.Bytes>} 40 41 # The JSON field names of every version. 42 type Key is Data: 43 KVersion{} 44 KId{} 45 KPlan{} 46 KMonthlyCents{} 47 KActive{} 48 KCents{} 49 KPeriod{} 50 KStatus{} 51 KNote{} 52 53 type 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. 68 def Status.of_active(a: Bool) -> Status: 69 match a: 70 case True{}: 71 Active{} 72 case False{}: 73 Cancelled{} 74 75 def V1.terms(r: V1) -> Terms: 76 V1{i, p, c, a} = r 77 Terms{i, p, c, Monthly{}, Status.of_active(a)} 78 79 def V2.terms(r: V2) -> Terms: 80 V2{i, p, c, pe, s, n} = r 81 Terms{i, p, c, pe, s} 82 83 def V3.terms(r: V3) -> Terms: 84 V3{i, p, c, pe, s} = r 85 Terms{i, p, c, pe, s} 86 87 def 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. 97 def 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 100 def none(m: Maybe<&2, Key>) -> Bool: 101 match m: 102 case None{}: 103 True{} 104 case Some{k}: 105 False{} 106 107 def 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. 113 def 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 122 def Record.valid.of(p: Record & Maybe<&2, Key>) -> Bool: 123 (r, m) = p 124 none(m) 125 126 def 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. 133 def V1.up(r: V1) -> V2: 134 V1{i, p, c, a} = r 135 V2{i, p, c, Monthly{}, Status.of_active(a), None{}} 136 137 def 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 144 def 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 153 def 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. 161 def 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. 166 def 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. 171 def 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. 177 def V3.down(r: V3) -> V2: 178 V3.restore(r, Dropped{None{}}) 179 180 # Any version to v3, with what the chain dropped. 181 def 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. 194 type Tag is Data: 195 Tag{len: U32, w0: U32, w1: U32, w2: U32, w3: U32} 196 197 def 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 202 def Tag.word(ok: Bool, a: Array, +i: U32) -> Array & U32: 203 match ok: 204 case True{}: 205 Array.get(U32, a, i) 206 case False{}: 207 (a, 0) 208 209 def Tag.of.w3(+len: U32, +w0: U32, +w1: U32, +w2: U32, r: Array & U32) -> Bytes.Bytes & Tag: 210 (a, +w3) = r 211 (Bytes.Bytes{len, a}, Tag{len, w0, w1, w2, w3}) 212 213 def Tag.of.w2(+len: U32, +n: U32, +w0: U32, +w1: U32, r: Array & 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 217 def Tag.of.w1(+len: U32, +n: U32, +w0: U32, r: Array & 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 221 def Tag.of.w0(+len: U32, +n: U32, r: Array & 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. 226 def 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 231 def Tag.set(ok: Bool, a: Array, +i: U32, +w: U32) -> Array: 232 match ok: 233 case True{}: 234 Array.set(U32, a, i, w) 235 case False{}: 236 a 237 238 def 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 243 def 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. 265 def Key.bytes(k: Key) -> Bytes.Bytes: 266 Tag.bytes(Key.tag(k)) 267 268 def 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 272 def 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 275 def 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 282 def 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 291 def 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 294 def 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 300 def 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 303 def u32.end(ok: Bool, +acc: U32) -> Maybe<&2, U32>: 304 match ok: 305 case True{}: 306 Some{acc} 307 case False{}: 308 None{} 309 310 def u32.go(n: Nat, r: Array & 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 319 def u32.first(+len: U32, r: Array & 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. 325 def u32.read(b: Bytes.Bytes) -> Maybe<&2, U32>: 326 Bytes.Bytes{+len, buf} = b 327 u32.first(len, Bytes.peek(buf, 0)) 328 329 def 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 332 def u32.fill(f: Nat, a: Array, +i: U32, +n: U32) -> Array: 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. 341 def 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. 349 type Scalar is Type: 350 SNum{s: Bytes.Bytes} 351 SStr{s: Bytes.Bytes} 352 SFlag{b: Bool} 353 SOther{} 354 355 def 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. 371 type 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 374 def Slots.empty() -> Slots: 375 Slots{None{}, None{}, None{}, None{}, None{}, None{}, None{}, None{}, None{}} 376 377 def 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 384 def 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 414 def 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 421 def 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 425 def 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. 433 def 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 440 def 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 449 def 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 458 def 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 467 def 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. 477 def 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 484 def 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 491 def name.period(p: Bytes.Bytes & Tag) -> Result<&1, &1, Error, Period>: 492 (b, +t) = p 493 found(Period, Period.find(t), KPeriod{}) 494 495 def name.status(p: Bytes.Bytes & Tag) -> Result<&1, &1, Error, Status>: 496 (b, +t) = p 497 found(Status, Status.find(t), KStatus{}) 498 499 def 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. 509 def 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 518 def 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 527 def 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 533 def 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 546 def 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 552 def 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 565 def 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 571 def 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 584 def 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 595 def 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 602 def 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 609 def 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. 614 def 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 621 def 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 628 def 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. 633 def 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. 637 def decode.val(v: Json.Val) -> Result<&1, &1, Error, Record>: 638 Result.bind(&1, &1, Error, Record, Record, decode.shape(v), Record.check) 639 640 def 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. 648 def decode(b: Bytes.Bytes) -> Result<&1, &1, Error, Record>: 649 decode.parsed(Json.parse.strict.bytes(b, 1)) 650 651 # Encode 652 # ------ 653 654 def kv(k: Key, v: Json.Val) -> Bytes.Bytes & Json.Val: 655 (Key.bytes(k), v) 656 657 def num(+n: U32) -> Json.Val: 658 Json.Num{u32.text(n)} 659 660 def Period.json(p: Period) -> Json.Val: 661 Json.Str{Tag.bytes(Period.tag(p))} 662 663 def Status.json(s: Status) -> Json.Val: 664 Json.Str{Tag.bytes(Status.tag(s))} 665 666 def 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 670 def 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 677 def 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 681 def 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. 686 def 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 695 def encode(r: Record) -> Bytes.Bytes: 696 Json.encode.bytes(Record.json(r))