1 import Base 2 import ../json.bend as Json 3 import ../cmd.bend as Cmd 4 import ./shape.bend as Shape 5 import ./yaml.bend as Yaml 6 import ./toon.bend as Toon 7 import ./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 12 type Fmt is Data: 13 FText{} 14 FJson{} 15 FCsv{} 16 FYaml{} 17 FToon{} 18 19 # The format a code (code.bend) stands for. 20 def 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 26 def 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 33 def of_name(+s: String) -> Maybe<&2, Fmt>: 34 of_name.code(Code.of_name(s)) 35 36 def 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 49 def is_text(f: Fmt) -> Bool: 50 match f: 51 case FText{}: 52 True{} 53 case _: 54 False{} 55 56 def 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). 65 def 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 78 def 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 87 def 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 94 def 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 101 def message_doc(+m: String) -> Json.Json: 102 Json.of_pairs([("schemaVersion", Json.JNum{"1"}), ("message", Json.JStr{m})]) 103 104 def strip_error(+m: String) -> String: 105 Bool.pick(String, String.starts_with(m, "Error: "), String.drop(m, 7n), m) 106 107 def 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 110 def 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 113 def 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 117 def 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). 128 def 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 133 def 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 136 def 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 141 def 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). 150 def 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). 160 def 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. 169 def 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)