1use clap::Parser;
2use std::collections::HashMap;
3use std::fs;
4use std::io::{self, Read};
5use std::path::PathBuf;
6use uson::{parse, apply_builtin_types, Value};
7use base64::Engine;
8
9#[derive(Parser)]
10#[command(name = "uson")]
11#[command(author, version)]
12#[command(about = "μson (uson) is a shorthand for JSON", long_about = None)]
13struct Cli {
14 /// μson expression to parse (can be multiple words without quotes)
15 #[arg(value_name = "EXPRESSION", num_args = 0..)]
16 expression: Vec<String>,
17
18 /// "object" mode - combines all expressions into one object
19 #[arg(short, long)]
20 object: bool,
21
22 /// "json" mode (default)
23 #[arg(short, long)]
24 json: bool,
25
26 /// Load data from file
27 #[arg(short, long, value_name = "FILE")]
28 input: Option<PathBuf>,
29
30 /// Write output to file
31 #[arg(long, value_name = "FILE")]
32 output: Option<PathBuf>,
33
34 /// Pretty print output
35 #[arg(short, long)]
36 pretty: bool,
37
38 /// Output format: json (default), uson, form, yaml
39 #[arg(short = 'F', long, value_name = "FORMAT", default_value = "json")]
40 format: String,
41
42 /// Return output in form query-string
43 #[arg(short, long)]
44 #[cfg(feature = "form")]
45 form: bool,
46
47 /// Return output in YAML
48 #[arg(short = 'y', long)]
49 #[cfg(feature = "yaml")]
50 yaml: bool,
51
52 /// Use custom usonrc file instead of ~/.usonrc.toml
53 #[arg(short, long, value_name = "FILE")]
54 usonrc: Option<PathBuf>,
55
56 /// Output in hex encoding
57 #[arg(long)]
58 hex: bool,
59
60 /// Output in base64 encoding
61 #[arg(long)]
62 base64: bool,
63}
64
65#[derive(Debug, serde::Deserialize)]
66struct UsonConfig {
67 #[serde(default)]
68 types: HashMap<String, String>,
69}
70
71fn main() {
72 let cli = Cli::parse();
73
74 if let Err(e) = run(cli) {
75 eprintln!("Error: {}", e);
76 std::process::exit(1);
77 }
78}
79
80fn run(cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
81 // Extract all values we need from cli before any moves
82 let expression = if !cli.expression.is_empty() {
83 Some(cli.expression.join(" "))
84 } else {
85 None
86 };
87 let input_file = cli.input;
88 let object_mode = cli.object;
89 let output_file = cli.output;
90 let hex_encode = cli.hex;
91 let base64_encode = cli.base64;
92 let pretty = cli.pretty;
93 let usonrc_path = cli.usonrc;
94 let format = cli.format.to_lowercase();
95
96 #[cfg(feature = "yaml")]
97 let yaml_output = cli.yaml;
98
99 #[cfg(feature = "form")]
100 let form_output = cli.form;
101
102 // Load configuration
103 let config = load_config(usonrc_path)?;
104
105 // Get input
106 let input = get_input(expression, input_file)?;
107
108 // Parse
109 let mut values = parse(&input)?;
110
111 // Apply built-in type handlers first
112 values = values.into_iter().map(apply_builtin_types).collect();
113
114 // Apply custom type handlers from config
115 if !config.types.is_empty() {
116 values = apply_type_handlers(values, &config)?;
117 }
118
119 // Format output based on specified format
120 let output = match format.as_str() {
121 "uson" | "microson" => {
122 // Output in μson format
123 format_uson(&values, object_mode, pretty)?
124 }
125 "json" => {
126 // Convert to output format based on mode
127 let result = if object_mode {
128 merge_to_object(values)?
129 } else {
130 serde_json::to_value(&values)?
131 };
132
133 format_output(
134 &result,
135 pretty,
136 #[cfg(feature = "yaml")]
137 yaml_output,
138 #[cfg(feature = "form")]
139 form_output,
140 )?
141 }
142 _ => {
143 return Err(format!("Unknown format: {}", format).into());
144 }
145 };
146
147 // Encode if needed
148 let final_output = if hex_encode {
149 hex::encode(&output)
150 } else if base64_encode {
151 base64::engine::general_purpose::STANDARD.encode(&output)
152 } else {
153 output
154 };
155
156 // Write output
157 write_output(&final_output, output_file)?;
158
159 Ok(())
160}
161
162fn format_uson(values: &[Value], object_mode: bool, pretty: bool) -> Result<String, Box<dyn std::error::Error>> {
163 if object_mode {
164 // Merge all into one object first
165 let mut merged = HashMap::new();
166 for value in values {
167 match value {
168 Value::Object(obj) => {
169 for (key, val) in obj {
170 merged.insert(key.clone(), val.clone());
171 }
172 }
173 _ => {
174 return Err("Object mode requires all values to be objects".into());
175 }
176 }
177 }
178 let merged_value = Value::Object(merged);
179 Ok(if pretty {
180 merged_value.to_uson_string_pretty()
181 } else {
182 merged_value.to_uson_string()
183 })
184 } else {
185 // Output each value
186 if values.len() == 1 {
187 Ok(if pretty {
188 values[0].to_uson_string_pretty()
189 } else {
190 values[0].to_uson_string()
191 })
192 } else {
193 let strings: Vec<String> = values.iter().map(|v| {
194 if pretty {
195 v.to_uson_string_pretty()
196 } else {
197 v.to_uson_string()
198 }
199 }).collect();
200 Ok(strings.join(if pretty { "\n\n" } else { " " }))
201 }
202 }
203}
204
205fn load_config(custom_path: Option<PathBuf>) -> Result<UsonConfig, Box<dyn std::error::Error>> {
206 let config_path = if let Some(path) = custom_path {
207 path
208 } else {
209 let home = dirs::home_dir().ok_or("Could not find home directory")?;
210 home.join(".usonrc.toml")
211 };
212
213 if config_path.exists() {
214 let content = fs::read_to_string(config_path)?;
215 let config: UsonConfig = toml::from_str(&content)?;
216 Ok(config)
217 } else {
218 Ok(UsonConfig {
219 types: HashMap::new(),
220 })
221 }
222}
223
224fn get_input(
225 expression: Option<String>,
226 input_file: Option<PathBuf>,
227) -> Result<String, Box<dyn std::error::Error>> {
228 if let Some(expr) = expression {
229 Ok(expr)
230 } else if let Some(file) = input_file {
231 Ok(fs::read_to_string(file)?)
232 } else {
233 // Read from stdin
234 let mut buffer = String::new();
235 io::stdin().read_to_string(&mut buffer)?;
236 Ok(buffer)
237 }
238}
239
240fn apply_type_handlers(
241 values: Vec<Value>,
242 _config: &UsonConfig,
243) -> Result<Vec<Value>, Box<dyn std::error::Error>> {
244 // Note: Custom type handlers would require dynamic evaluation
245 // For now, we'll keep typed values as-is
246 // In a real implementation, you might want to use a scripting engine like rhai or lua
247 Ok(values)
248}
249
250fn merge_to_object(values: Vec<Value>) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
251 let mut result = serde_json::Map::new();
252
253 for value in values {
254 match value {
255 Value::Object(obj) => {
256 for (key, val) in obj {
257 result.insert(key, serde_json::to_value(val)?);
258 }
259 }
260 _ => {
261 return Err("Object mode requires all values to be objects".into());
262 }
263 }
264 }
265
266 Ok(serde_json::Value::Object(result))
267}
268
269fn format_output(
270 value: &serde_json::Value,
271 pretty: bool,
272 #[cfg(feature = "yaml")]
273 yaml: bool,
274 #[cfg(feature = "form")]
275 form: bool,
276) -> Result<String, Box<dyn std::error::Error>> {
277 #[cfg(feature = "yaml")]
278 if yaml {
279 return Ok(serde_yaml::to_string(value)?);
280 }
281
282 #[cfg(feature = "form")]
283 if form {
284 return Ok(serde_qs::to_string(value)?);
285 }
286
287 // Default: JSON
288 if pretty {
289 Ok(serde_json::to_string_pretty(value)?)
290 } else {
291 Ok(serde_json::to_string(value)?)
292 }
293}
294
295fn write_output(
296 output: &str,
297 output_file: Option<PathBuf>,
298) -> Result<(), Box<dyn std::error::Error>> {
299 if let Some(file) = output_file {
300 fs::write(file, output)?;
301 } else {
302 println!("{}", output);
303 }
304 Ok(())
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310
311 #[test]
312 fn test_merge_to_object() {
313 let values = vec![
314 Value::Object({
315 let mut map = HashMap::new();
316 map.insert("user".to_string(), Value::String("john".to_string()));
317 map
318 }),
319 Value::Object({
320 let mut map = HashMap::new();
321 map.insert("age".to_string(), Value::Number(uson::Number::Integer(42)));
322 map
323 }),
324 ];
325
326 let result = merge_to_object(values).unwrap();
327 assert!(result.is_object());
328 let obj = result.as_object().unwrap();
329 assert_eq!(obj.get("user").unwrap().as_str().unwrap(), "john");
330 assert_eq!(obj.get("age").unwrap().as_i64().unwrap(), 42);
331 }
332}