1use 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(DecodeArgs),
25 Encode(EncodeArgs),
27 Inspect(InspectArgs),
29}
30
31#[derive(Args, Debug)]
32pub(crate) struct DecodeArgs {
33 pub input: Option<String>,
35 #[arg(long)]
37 pub raw: bool,
38}
39
40#[derive(Args, Debug)]
41pub(crate) struct EncodeArgs {
42 pub input: Option<String>,
44}
45
46#[derive(Args, Debug)]
47pub(crate) struct InspectArgs {
48 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
154fn 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}