Skip to main content

forge/
parcel.rs

1// #t(file: rust_println) CLI binary, terminal output is the purpose.
2
3//! Parcel subcommand: encode, decode, and inspect LibParcel-1.0 envelopes.
4
5use std::io::Read;
6
7use anyhow::{Context, Result};
8use clap::{Args, Subcommand};
9use wowlab_common::output;
10use wowlab_parsers::parcel::{
11    ResultExport, decode_result_export, encode_result_export, result_export_codec,
12};
13
14#[derive(Args, Debug)]
15pub(crate) struct ParcelArgs {
16    #[command(subcommand)]
17    pub command: ParcelCommand,
18}
19
20#[derive(Debug, Subcommand)]
21#[non_exhaustive]
22pub(crate) enum ParcelCommand {
23    /// Decode a wowlab parcel envelope and print the typed payload.
24    Decode(DecodeArgs),
25    /// Encode a JSON `ResultExport` into a wowlab parcel envelope.
26    Encode(EncodeArgs),
27    /// Print the SPEC ยง8 inspect summary for a parcel envelope.
28    Inspect(InspectArgs),
29}
30
31#[derive(Args, Debug)]
32pub(crate) struct DecodeArgs {
33    /// The encoded envelope. Pass `-` or omit to read from stdin.
34    pub input: Option<String>,
35    /// Print the inner JSON as serialized bytes instead of typed pretty JSON.
36    #[arg(long)]
37    pub raw: bool,
38}
39
40#[derive(Args, Debug)]
41pub(crate) struct EncodeArgs {
42    /// JSON `ResultExport` document. Pass `-` or omit to read from stdin.
43    pub input: Option<String>,
44}
45
46#[derive(Args, Debug)]
47pub(crate) struct InspectArgs {
48    /// The encoded envelope. Pass `-` or omit to read from stdin.
49    pub input: Option<String>,
50}
51
52pub(crate) fn run(args: &ParcelArgs) -> Result<()> {
53    match &args.command {
54        ParcelCommand::Decode(a) => run_decode(a),
55        ParcelCommand::Encode(a) => run_encode(a),
56        ParcelCommand::Inspect(a) => run_inspect(a),
57    }
58}
59
60fn run_decode(args: &DecodeArgs) -> Result<()> {
61    let encoded = repair_prefix(read_input(args.input.as_deref())?);
62    let codec = result_export_codec();
63    let decoded = codec
64        .decode(&encoded)
65        .map_err(|e| anyhow::anyhow!("decode failed: {}", e.code()))?;
66
67    output::header("Envelope");
68    output::kv("prefix", &decoded.meta.prefix);
69    output::kv("content_type", &decoded.meta.content_type);
70    output::kv("checksum", &decoded.meta.checksum);
71    output::kv("encoded_bytes", &decoded.meta.encoded_bytes.to_string());
72    output::kv(
73        "decoded_body_bytes",
74        &decoded.meta.decoded_body_bytes.to_string(),
75    );
76    output::kv("payload_bytes", &decoded.meta.payload_bytes.to_string());
77    output::blank();
78
79    if args.raw {
80        output::header("Raw payload");
81        let body = std::str::from_utf8(&decoded.payload).context("payload is not valid UTF-8")?;
82
83        println!("{body}");
84
85        return Ok(());
86    }
87
88    let export: ResultExport =
89        decode_result_export(&encoded).map_err(|e| anyhow::anyhow!("typed decode failed: {e}"))?;
90
91    output::header("Result export");
92    output::kv("schema_version", &export.v.to_string());
93    output::kv("kind", &format!("{:?}", export.kind));
94    output::kv("spec", &export.spec);
95    output::kv("baseline_dps", &format_dps(export.baseline_dps));
96    output::kv("winner_dps", &format_dps(export.winner_dps));
97    output::kv("items", &export.items.len().to_string());
98
99    if let Some(job_id) = &export.job_id {
100        output::kv("job_id", job_id);
101    }
102
103    if let Some(note) = &export.note {
104        output::kv("note", note);
105    }
106
107    output::blank();
108
109    output::header("JSON");
110    let pretty =
111        serde_json::to_string_pretty(&export).context("failed to pretty-print decoded export")?;
112
113    println!("{pretty}");
114
115    Ok(())
116}
117
118fn run_encode(args: &EncodeArgs) -> Result<()> {
119    let json = read_input(args.input.as_deref())?;
120    let export: ResultExport =
121        serde_json::from_str(&json).context("input is not a valid ResultExport JSON document")?;
122    let encoded =
123        encode_result_export(&export).map_err(|e| anyhow::anyhow!("encode failed: {e}"))?;
124
125    println!("{encoded}");
126
127    Ok(())
128}
129
130fn run_inspect(args: &InspectArgs) -> Result<()> {
131    let encoded = repair_prefix(read_input(args.input.as_deref())?);
132    let codec = result_export_codec();
133
134    println!("{}", codec.inspect(&encoded));
135
136    Ok(())
137}
138
139fn read_input(arg: Option<&str>) -> Result<String> {
140    let mut buf = String::new();
141
142    match arg {
143        None | Some("-") => {
144            std::io::stdin()
145                .read_to_string(&mut buf)
146                .context("failed to read stdin")?;
147        }
148        Some(s) => s.clone_into(&mut buf),
149    }
150
151    Ok(buf.trim().to_owned())
152}
153
154/// Recover the leading `!` if a shell ate it (common with `!history` expansion).
155fn repair_prefix(s: String) -> String {
156    if s.starts_with('!') {
157        s
158    } else {
159        format!("!{s}")
160    }
161}
162
163const DPS_DISPLAY_DECIMALS: usize = 1;
164
165fn format_dps(dps: f64) -> String {
166    format!("{dps:.DPS_DISPLAY_DECIMALS$}")
167}