fmt/shape.bend

cswap-bend-cli@0.1.0.0 · 257 lines · 39 definitions

1import Base
2import cswap-bend-cli@0.1.0.0/json.bend as Json
3import cswap-bend-cli@0.1.0.0/fmt/csv.bend as Csv
4
5# The tabular shape of a command's JSON document, for `--format csv`.
6#
7#   list           one row per account
8#   status         one row for the active account (no row when none)
9#   config         one row per setting: key,value,is_set
10#   profile list   one row per profile
11#   auto           one row per event, fixed columns
12#   export         one row per leaf: key,value (dotted keys)
13#   anything else  one row: the document's leaves as dotted-key columns
14#
15# A cell is empty for null, true/false, the number's text, the string itself;
16# an array of scalars is its items joined by `;`, anything else compact JSON.
17# Pure.
18
19type Col is Data:
20  Col{h: String, p: List<&2, String>}
21
22type Leaf is Data:
23  Leaf{k: String, v: String}
24
25# ---- cells -------------------------------------------------------------------
26
27def or_null(m: Maybe<&2, Json.Json>) -> Json.Json:
28  match m:
29    case None{}:
30      Json.JNull{}
31    case Some{v}:
32      v
33
34def get_or_null(+j: Json.Json, +k: String) -> Json.Json:
35  or_null(Json.get(k, j))
36
37# The value at a key path; null when any step is missing.
38def path(ks: List<&2, String>, +j: Json.Json) -> Json.Json:
39  match ks:
40    case Nil{}:
41      j
42    case Con{k, t}:
43      path(t, get_or_null(j, k))
44
45def is_scalar(j: Json.Json) -> Bool:
46  match j:
47    case Json.JArr{x}:
48      False{}
49    case Json.JObj{x}:
50      False{}
51    case _:
52      True{}
53
54# A scalar as its text; a container as compact JSON.
55def scal(+j: Json.Json) -> String:
56  match j:
57    case Json.JNull{}:
58      ""
59    case Json.JBool{b}:
60      Json.bool.show(b)
61    case Json.JNum{t}:
62      t
63    case Json.JStr{s}:
64      s
65    case _:
66      Json.emit(j)
67
68def all_scalar(items: Json.Json) -> Bool:
69  match items:
70    case Json.JCons{h, t}:
71      is_scalar(h) && all_scalar(t)
72    case _:
73      True{}
74
75def sep_of(t: Json.Json) -> String:
76  match t:
77    case Json.JNil{}:
78      ""
79    case _:
80      ";"
81
82def joined(items: Json.Json) -> String:
83  match items:
84    case Json.JCons{h, +t}:
85      scal(h) ++ sep_of(t) ++ joined(t)
86    case _:
87      ""
88
89def cell.arr(ok: Bool, +items: Json.Json) -> String:
90  match ok:
91    case True{}:
92      joined(items)
93    case False{}:
94      Json.emit(Json.JArr{items})
95
96def cell(+j: Json.Json) -> String:
97  match j:
98    case Json.JArr{+items}:
99      cell.arr(all_scalar(items), items)
100    case _:
101      scal(j)
102
103# ---- columns --------------------------------------------------------------------
104
105def col(h: String, p: List<&2, String>) -> Col:
106  Col{h, p}
107
108def headers(cols: List<&2, Col>) -> List<&2, String>:
109  match cols:
110    case Nil{}:
111      []
112    case Con{Col{h, p}, t}:
113      h <> headers(t)
114
115def cells(+j: Json.Json, cols: List<&2, Col>) -> List<&2, String>:
116  match cols:
117    case Nil{}:
118      []
119    case Con{Col{h, p}, t}:
120      cell(path(p, j)) <> cells(j, t)
121
122def rows(items: List<&2, Json.Json>, +cols: List<&2, Col>) -> List<&2, List<&2, String>>:
123  match items:
124    case Nil{}:
125      []
126    case Con{+h, t}:
127      cells(h, cols) <> rows(t, cols)
128
129def table(+cols: List<&2, Col>, items: List<&2, Json.Json>) -> String:
130  Csv.table(headers(cols) <> rows(items, cols))
131
132def account_cols() -> List<&2, Col>:
133  [col("slot", ["number"]), col("email", ["email"]), col("org", ["organizationName"]), col("kind", ["usageStatus"]), col("active", ["active"]),
134   col("5h_pct", ["usage", "fiveHour", "pct"]), col("5h_reset", ["usage", "fiveHour", "resetsAt"]),
135   col("7d_pct", ["usage", "sevenDay", "pct"]), col("7d_reset", ["usage", "sevenDay", "resetsAt"]),
136   col("fetched_at", ["usageFetchedAt"]), col("error", ["usageError"])]
137
138def setting_cols() -> List<&2, Col>:
139  [col("key", ["key"]), col("value", ["value"]), col("is_set", ["isSet"])]
140
141def profile_cols() -> List<&2, Col>:
142  [col("name", ["name"]), col("builtin", ["builtin"]), col("stored", ["stored"]), col("active", ["active"]), col("strategy", ["strategy"]),
143   col("threshold", ["effective", "threshold"]), col("models", ["effective", "models"]), col("cooldown_s", ["effective", "cooldown_s"]), col("interval_s", ["effective", "interval_s"])]
144
145def event_cols() -> List<&2, Col>:
146  [col("event", ["event"]), col("ts", ["ts"]), col("profile", ["profile"]), col("strategy", ["strategy"]),
147   col("active_slot", ["active", "number"]), col("from_slot", ["from", "number"]), col("to_slot", ["to", "number"]),
148   col("trigger", ["trigger"]), col("reason", ["reason"]), col("detail", ["detail"]), col("message", ["message"]),
149   col("threshold", ["threshold"]), col("seconds", ["seconds"]), col("until", ["until"]),
150   col("slot", ["number"]), col("email", ["email"]), col("dry_run", ["dryRun"]), col("earliest_reset_at", ["earliestResetAt"])]
151
152# The header line of the `auto` event stream.
153def event_header() -> String:
154  Csv.row(headers(event_cols())) ++ "\n"
155
156# One event as a CSV row (no header, no line end).
157def event_row(+e: Json.Json) -> String:
158  Csv.row(cells(e, event_cols()))
159
160# ---- leaves -----------------------------------------------------------------------
161
162def dotted(+prefix: String, +k: String) -> String:
163  Bool.pick(String, String.is_empty(prefix), k, prefix ++ "." ++ k)
164
165def is_empty_obj(f: Json.Json) -> Bool:
166  match f:
167    case Json.JNil{}:
168      True{}
169    case _:
170      False{}
171
172def leaf.pick(empty: Bool, prefix: String, rest: List<&2, Leaf>) -> List<&2, Leaf>:
173  match empty:
174    case True{}:
175      Leaf{prefix, "{}"} <> rest
176    case False{}:
177      rest
178
179# The scalar leaves of a document with dotted keys; an array is one leaf.
180def leaves(j: Json.Json, +prefix: String) -> List<&2, Leaf>:
181  match j:
182    case Json.JObj{+f}:
183      leaf.pick(is_empty_obj(f), prefix, leaves(f, prefix))
184    case Json.JField{k, v, +t}:
185      List.append(&2, Leaf, leaves(v, dotted(prefix, k)), leaves(t, prefix))
186    case Json.JNil{}:
187      []
188    case _:
189      Leaf{prefix, cell(j)} <> []
190
191def leaf_headers(ls: List<&2, Leaf>) -> List<&2, String>:
192  match ls:
193    case Nil{}:
194      []
195    case Con{Leaf{k, v}, t}:
196      k <> leaf_headers(t)
197
198def leaf_values(ls: List<&2, Leaf>) -> List<&2, String>:
199  match ls:
200    case Nil{}:
201      []
202    case Con{Leaf{k, v}, t}:
203      v <> leaf_values(t)
204
205def leaf_pairs(ls: List<&2, Leaf>) -> List<&2, List<&2, String>>:
206  match ls:
207    case Nil{}:
208      []
209    case Con{Leaf{k, v}, t}:
210      [k, v] <> leaf_pairs(t)
211
212def wide.of(+ls: List<&2, Leaf>) -> String:
213  Csv.table([leaf_headers(ls), leaf_values(ls)])
214
215# One row: the leaves as columns.
216def wide(+j: Json.Json) -> String:
217  wide.of(leaves(j, ""))
218
219# key,value rows.
220def kv(+j: Json.Json) -> String:
221  Csv.table(["key", "value"] <> leaf_pairs(leaves(j, "")))
222
223# ---- the dispatch ------------------------------------------------------------------
224
225def items_at(+j: Json.Json, +k: String) -> List<&2, Json.Json>:
226  Json.to_list(get_or_null(j, k))
227
228# The active account of a status document as a list of at most one row.
229def status_rows(a: Json.Json) -> List<&2, Json.Json>:
230  match a:
231    case Json.JObj{f}:
232      [Json.set("active", Json.JBool{True{}}, Json.JObj{f})]
233    case _:
234      []
235
236def some(m: Maybe<&2, Json.Json>) -> Bool:
237  match m:
238    case Some{e}:
239      True{}
240    case None{}:
241      False{}
242
243def has_error(+j: Json.Json) -> Bool:
244  some(Json.get("error", j))
245
246def by_key(+key: String, +j: Json.Json) -> String:
247  Bool.pick(String, String.eq(key, "list"), table(account_cols(), items_at(j, "accounts")),
248  Bool.pick(String, String.eq(key, "status"), table(account_cols(), status_rows(get_or_null(j, "active"))),
249  Bool.pick(String, String.eq(key, "config-list"), table(setting_cols(), items_at(j, "settings")),
250  Bool.pick(String, String.eq(key, "profile-list"), table(profile_cols(), items_at(j, "profiles")),
251  Bool.pick(String, String.eq(key, "export"), kv(j), wide(j))))))
252
253# The CSV text of a command's document. `key` names the command shape
254# (list, status, config-list, profile-list, export); a document that is an
255# error envelope is always one wide row.
256def csv_of(+key: String, +j: Json.Json) -> String:
257  Bool.pick(String, has_error(j), wide(j), by_key(key, j))