1 import Base 2 import ../json.bend as Json 3 import ../clock.bend as Clock 4 import ../policy/types.bend as T 5 import ./config.bend as Cfg 6 7 # cache/usage.json rows (contract 1.5) into the policy's `Usage`. Percentages 8 # become centi-percent Nat; epochs and ISO reset stamps become unix seconds. 9 10 def nat_opt(+m: Maybe<&2, U32>) -> Maybe<&2, Nat>: 11 match m: 12 case None{}: 13 None{} 14 case Some{n}: 15 Some{U32.to_nat(n)} 16 17 # "1792740000.1" -> 1792740000, "7" -> 7; None for anything else. 18 def epoch_text(+t: String) -> Maybe<&2, U32>: 19 Cfg.read_u32(Maybe.default(&2, String, List.head(&2, String, String.split(t, '.')), "")) 20 21 def num_text(+m: Maybe<&2, Json.Json>) -> Maybe<&2, String>: 22 match m: 23 case Some{Json.JNum{t}}: 24 Some{t} 25 case _: 26 None{} 27 28 def str_text(+m: Maybe<&2, Json.Json>) -> Maybe<&2, String>: 29 match m: 30 case Some{Json.JStr{s}}: 31 Some{s} 32 case _: 33 None{} 34 35 def bind_s(+m: Maybe<&2, String>, f: String -> Maybe<&2, U32>) -> Maybe<&2, U32>: 36 match m: 37 case None{}: 38 None{} 39 case Some{s}: 40 f(s) 41 42 def epoch_of(+j: Json.Json, +key: String) -> Maybe<&2, Nat>: 43 nat_opt(bind_s(num_text(Json.get(key, j)), t => epoch_text(t))) 44 45 def pct_of(+j: Json.Json) -> Maybe<&2, Nat>: 46 nat_opt(bind_s(num_text(Json.get("pct", j)), t => Cfg.centi(t))) 47 48 def iso_of(+j: Json.Json) -> Maybe<&2, Nat>: 49 nat_opt(bind_s(str_text(Json.get("resets_at", j)), t => Clock.parse_iso8601(t))) 50 51 def or_null(+m: Maybe<&2, Json.Json>) -> Json.Json: 52 match m: 53 case Some{w}: 54 w 55 case None{}: 56 Json.JNull{} 57 58 def win(+j: Json.Json, +key: String) -> Json.Json: 59 or_null(Json.get(key, j)) 60 61 # ---- scoped windows ----------------------------------------------------------------------- 62 63 def scoped2(+name: Maybe<&2, String>, +pct: Maybe<&2, Nat>, +reset: Maybe<&2, Nat>) -> Maybe<&2, T.Scoped>: 64 match name pct: 65 case Some{n} Some{p}: 66 Some{T.Scoped{n, p, reset}} 67 case _ _: 68 None{} 69 70 def scoped_one(+s: Json.Json) -> Maybe<&2, T.Scoped>: 71 scoped2(str_text(Json.get("name", s)), pct_of(s), iso_of(s)) 72 73 def scoped_push(+m: Maybe<&2, T.Scoped>, +rest: List<&2, T.Scoped>) -> List<&2, T.Scoped>: 74 match m: 75 case Some{s}: 76 s <> rest 77 case None{}: 78 rest 79 80 def scoped_chain(+items: Json.Json) -> List<&2, T.Scoped>: 81 match items: 82 case Json.JCons{h, t}: 83 scoped_push(scoped_one(h), scoped_chain(t)) 84 case _: 85 [] 86 87 def unwrap_arr(+j: Json.Json) -> Json.Json: 88 match j: 89 case Json.JArr{items}: 90 items 91 case _: 92 Json.JNil{} 93 94 def scoped_of(+good: Json.Json) -> List<&2, T.Scoped>: 95 scoped_chain(unwrap_arr(win(good, "scoped"))) 96 97 # ---- error and plan fields --------------------------------------------------------------------- 98 99 def err2(+is429: Bool) -> T.ErrKind: 100 match is429: 101 case True{}: 102 T.ErrHttp429{} 103 case False{}: 104 T.ErrOther{} 105 106 def err_kind(+m: Maybe<&2, String>) -> T.ErrKind: 107 match m: 108 case None{}: 109 T.ErrNone{} 110 case Some{e}: 111 err2(String.eq(e, "http-429")) 112 113 def zero_or(+m: Maybe<&2, Nat>) -> Nat: 114 match m: 115 case None{}: 116 0n 117 case Some{n}: 118 n 119 120 def u32_field(+j: Json.Json, +key: String) -> Nat: 121 zero_or(nat_opt(bind_s(num_text(Json.get(key, j)), t => Cfg.read_u32(Maybe.default(&2, String, List.head(&2, String, String.split(t, '.')), ""))))) 122 123 def usage2(+slot: Nat, +row: Json.Json, +good: Json.Json, +sentinel: T.Sentinel) -> T.Usage: 124 T.Usage{slot, pct_of(win(good, "five_hour")), iso_of(win(good, "five_hour")), pct_of(win(good, "seven_day")), iso_of(win(good, "seven_day")), 125 scoped_of(good), epoch_of(row, "fetchedAt"), u32_field(row, "backoffUntil"), err_kind(str_text(Json.get("lastError", row))), u32_field(row, "consecutiveFailures"), 126 epoch_of(row, "nextPollAt"), u32_field(row, "pollIntervalS"), u32_field(row, "authDeadStrikes"), u32_field(row, "heldUntil"), u32_field(row, "claimUntil"), sentinel} 127 128 # A row, as the policy reads it. `row` is JNull when the slot has none (or it 129 # belongs to another identity): never fetched. `sentinel` is the overlay the 130 # caller computed from the credential. 131 def usage_of(+slot: Nat, +row: Json.Json, +sentinel: T.Sentinel) -> T.Usage: 132 usage2(slot, row, win(row, "lastGood"), sentinel) 133 134 def empty_usage(+slot: Nat, +sentinel: T.Sentinel) -> T.Usage: 135 usage_of(slot, Json.JNull{}, sentinel)