μson (uson) is a shorthand for JSON
at main 8.9 kB view raw
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}