fmt/out.bend

cswap-bend-cli@0.1.0.0 · 178 lines · 24 definitions

1import Base
2import cswap-bend-cli@0.1.0.0/json.bend as Json
3import cswap-bend-cli@0.1.0.0/cmd.bend as Cmd
4import cswap-bend-cli@0.1.0.0/fmt/shape.bend as Shape
5import cswap-bend-cli@0.1.0.0/fmt/yaml.bend as Yaml
6import cswap-bend-cli@0.1.0.0/fmt/toon.bend as Toon
7import cswap-bend-cli@0.1.0.0/fmt/code.bend as Code
8
9# Output formats: the `--format` names, a JSON document rendered in a format,
10# and a command's output converted to a format. Pure.
11
12type Fmt is Data:
13  FText{}
14  FJson{}
15  FCsv{}
16  FYaml{}
17  FToon{}
18
19# The format a code (code.bend) stands for.
20def of_code(+n: U32) -> Fmt:
21  Bool.pick(Fmt, U32.is_eq(n, 1), FJson{},
22  Bool.pick(Fmt, U32.is_eq(n, 2), FCsv{},
23  Bool.pick(Fmt, U32.is_eq(n, 3), FYaml{},
24  Bool.pick(Fmt, U32.is_eq(n, 4), FToon{}, FText{}))))
25
26def of_name.code(m: Maybe<&2, U32>) -> Maybe<&2, Fmt>:
27  match m:
28    case Some{n}:
29      Some{of_code(n)}
30    case None{}:
31      None{}
32
33def of_name(+s: String) -> Maybe<&2, Fmt>:
34  of_name.code(Code.of_name(s))
35
36def name(f: Fmt) -> String:
37  match f:
38    case FText{}:
39      "text"
40    case FJson{}:
41      "json"
42    case FCsv{}:
43      "csv"
44    case FYaml{}:
45      "yaml"
46    case FToon{}:
47      "toon"
48
49def is_text(f: Fmt) -> Bool:
50  match f:
51    case FText{}:
52      True{}
53    case _:
54      False{}
55
56def is_json(f: Fmt) -> Bool:
57  match f:
58    case FJson{}:
59      True{}
60    case _:
61      False{}
62
63# The document in the format, ending in a newline. `key` names the CSV shape
64# (see shape.bend).
65def render(f: Fmt, +key: String, +j: Json.Json) -> String:
66  match f:
67    case FCsv{}:
68      Shape.csv_of(key, j)
69    case FYaml{}:
70      Yaml.emit(j) ++ "\n"
71    case FToon{}:
72      Toon.emit(j) ++ "\n"
73    case _:
74      Json.emit_pretty(j) ++ "\n"
75
76# ---- a command's output as a document -----------------------------------------------
77
78def is_container(j: Json.Json) -> Bool:
79  match j:
80    case Json.JObj{x}:
81      True{}
82    case Json.JArr{x}:
83      True{}
84    case _:
85      False{}
86
87def is_doc(r: Result<&2, &2, String, Json.Json>) -> Bool:
88  match r:
89    case Done{j}:
90      is_container(j)
91    case Fail{e}:
92      False{}
93
94def unwrap_doc(r: Result<&2, &2, String, Json.Json>) -> Json.Json:
95  match r:
96    case Done{j}:
97      j
98    case Fail{e}:
99      Json.JNull{}
100
101def message_doc(+m: String) -> Json.Json:
102  Json.of_pairs([("schemaVersion", Json.JNum{"1"}), ("message", Json.JStr{m})])
103
104def strip_error(+m: String) -> String:
105  Bool.pick(String, String.starts_with(m, "Error: "), String.drop(m, 7n), m)
106
107def error_doc(+m: String) -> Json.Json:
108  Json.of_pairs([("schemaVersion", Json.JNum{"1"}), ("error", Json.of_pairs([("type", Json.JStr{"Error"}), ("message", Json.JStr{strip_error(String.trim_end(m))})]))])
109
110def text_of(+out: String, +err: String) -> String:
111  Bool.pick(String, String.is_empty(String.trim(out)), String.trim_end(err), String.trim_end(out))
112
113def plain(+code: U32, +out: String, +err: String) -> Maybe<&2, Json.Json>:
114  Bool.pick(Maybe<&2, Json.Json>, U32.is_eq(code, 0), Some{message_doc(text_of(out, err))},
115  Bool.pick(Maybe<&2, Json.Json>, U32.is_eq(code, 1) && String.is_empty(out) && Bool.not(String.is_empty(err)), Some{error_doc(err)}, None{}))
116
117def doc2(ok: Bool, r: Result<&2, &2, String, Json.Json>, +code: U32, +out: String, +err: String) -> Maybe<&2, Json.Json>:
118  match ok:
119    case True{}:
120      Some{unwrap_doc(r)}
121    case False{}:
122      plain(code, out, err)
123
124# The JSON document a command's output stands for: its own JSON when stdout
125# holds an object or array; else `{schemaVersion, message}` for a success and
126# `{schemaVersion, error:{type, message}}` for an exit-1 error that went to
127# stderr; none for a usage error (exit 2).
128def doc_of(o: Cmd.Out) -> Maybe<&2, Json.Json>:
129  match o:
130    case Cmd.Out{code, +out, err}:
131      doc2(is_doc(Json.parse(out)), Json.parse(out), code, out, err)
132
133def keep_err(code: U32, out: String, err: String) -> String:
134  Bool.pick(String, U32.is_eq(code, 0) && Bool.not(String.is_empty(String.trim(out))), err, "")
135
136def conv2(f: Fmt, key: String, o: Cmd.Out, j: Json.Json) -> Cmd.Out:
137  match o:
138    case Cmd.Out{+code, out, err}:
139      Cmd.Out{code, render(f, key, j), keep_err(code, out, err)}
140
141def conv(f: Fmt, key: String, o: Cmd.Out, m: Maybe<&2, Json.Json>) -> Cmd.Out:
142  match m:
143    case None{}:
144      o
145    case Some{j}:
146      conv2(f, key, o, j)
147
148# The command's output in format `f` (text leaves it alone, and so does json
149# when the command already printed JSON: that output is the --json output).
150def convert(f: Fmt, key: String, +o: Cmd.Out) -> Cmd.Out:
151  match f:
152    case FText{}:
153      o
154    case _:
155      conv(f, key, o, doc_of(o))
156
157# ---- the `auto` event stream -------------------------------------------------------------
158
159# Printed once before the first event (CSV: the header line).
160def stream_head(f: Fmt) -> String:
161  match f:
162    case FCsv{}:
163      Shape.event_header()
164    case _:
165      ""
166
167# One event as the text IO.print puts on its own line(s): a JSON line, a CSV
168# row, a `---` YAML document, a TOON document followed by a blank line.
169def event_text(f: Fmt, +e: Json.Json) -> String:
170  match f:
171    case FCsv{}:
172      Shape.event_row(e)
173    case FYaml{}:
174      "---\n" ++ Yaml.emit(e)
175    case FToon{}:
176      Toon.emit(e) ++ "\n"
177    case _:
178      Json.emit_py(e)