diff --git a/Cargo.lock b/Cargo.lock index 56339df..dc9ee2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,17 +24,29 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" name = "jsonkdl" version = "1.0.0" dependencies = [ - "kdl", + "kdl 6.3.4", "serde_json", ] +[[package]] +name = "kdl" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e03e2e96c5926fe761088d66c8c2aee3a4352a2573f4eaca50043ad130af9117" +dependencies = [ + "miette 5.10.0", + "nom", + "thiserror", +] + [[package]] name = "kdl" version = "6.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12661358400b02cbbf1fbd05f0a483335490e8a6bd1867620f2eeb78f304a22f" dependencies = [ - "miette", + "kdl 4.7.1", + "miette 7.6.0", "num", "thiserror", "winnow", @@ -46,6 +58,18 @@ version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +[[package]] +name = "miette" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e" +dependencies = [ + "miette-derive 5.10.0", + "once_cell", + "thiserror", + "unicode-width", +] + [[package]] name = "miette" version = "7.6.0" @@ -53,10 +77,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" dependencies = [ "cfg-if", - "miette-derive", + "miette-derive 7.6.0", "unicode-width", ] +[[package]] +name = "miette-derive" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "miette-derive" version = "7.6.0" @@ -68,6 +103,22 @@ dependencies = [ "syn", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "num" version = "0.4.3" @@ -141,6 +192,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + [[package]] name = "proc-macro2" version = "1.0.95" diff --git a/Cargo.toml b/Cargo.toml index daf5759..d36cb15 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,5 +8,5 @@ name = "jsonkdl" path = "src/main.rs" [dependencies] -kdl = "6.3.4" +kdl = { version = "6.3.4", features = ["v1"] } serde_json = "1.0.140" diff --git a/README.md b/README.md index 0f10bc1..59d1f42 100644 --- a/README.md +++ b/README.md @@ -5,14 +5,18 @@ Converts JSON to KDL. ## Usage ``` -JSON to KDL Converter -Usage: - jsonkdl [options] +Usage: jsonkdl [options] +Converts JSON to KDL. +By default, KDL spec v2 is used. This can be changed by specifying a flag. + Arguments: - Input JSON file - Output KDL file + Path to input json file + Path to output KDL file + Options: - -f, --force Overwrite existing files - -v, --verbose Verbose output + -1, --kdl-v1 Convert to KDL v1 + -2, --kdl-v2 Convert to KDL v2 + -f, --force Overwrite output if it exists + -v, --verbose Print extra information during conversion -h, --help Show this help message ``` diff --git a/src/cli.rs b/src/cli.rs index e31a6ed..646c9c8 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -4,15 +4,17 @@ use std::env; use std::path::Path; const HELP_TEXT: &str = "\ -jsonkdl - convert json to kdl -Usage: - jsonkdl [options] +Usage: jsonkdl [options] +Converts JSON to KDL. +By default, KDL spec v2 is used. This can be changed by specifying a flag. Arguments: Path to input json file - Path to output kdl file + Path to output KDL file -Aptions: +Options: + -1, --kdl-v1 Convert to KDL v1 + -2, --kdl-v2 Convert to KDL v2 -f, --force Overwrite output if it exists -v, --verbose Print extra information during conversion -h, --help Show this help message @@ -23,6 +25,7 @@ pub enum CliError { MissingInput, MissingOutput, HelpRequested, + MultipleKdlVersion, InvalidInputPath(String), FileExists(String), InputNotFound(String), @@ -37,6 +40,7 @@ pub struct Args { pub output: String, pub force: bool, pub verbose: bool, + pub kdl_version: i32, } impl std::fmt::Display for CliError { @@ -45,6 +49,7 @@ impl std::fmt::Display for CliError { CliError::MissingInput => writeln!(f, "missing input path"), CliError::MissingOutput => writeln!(f, "missing output path"), CliError::HelpRequested => writeln!(f, "help requested"), + CliError::MultipleKdlVersion => writeln!(f, "specify only one of --kdl-v1 or --kdl-v2"), CliError::InvalidInputPath(path) => writeln!(f, "not a file: {}", path), CliError::FileExists(path) => writeln!(f, "file exists: {} (use --force to overwrite)", path), CliError::InputNotFound(path) => writeln!(f, "no such file: {}", path), @@ -71,33 +76,60 @@ impl From for CliError { impl Args { fn parse() -> Result { let args: Vec = env::args().collect(); + let mut positional: Vec = vec![]; + let mut force = false; + let mut verbose = false; + let mut kdl_version = 0; - // If no args besides the program name, show help - if args.len() == 1 - || args.iter().any(|arg| arg == "--help" || arg == "-h") - { + if args.len() == 1 { return Err(CliError::HelpRequested); } - let force = args.iter().any(|arg| arg == "--force" || arg == "-f"); - let verbose = args.iter().any(|arg| arg == "--verbose" || arg == "-v"); + for arg in args.iter().skip(1) { + if !arg.starts_with('-') { + positional.push(arg.into()); + } else if arg == "-f" || arg == "--force" { + force = true; + } else if arg == "-v" || arg == "--verbose" { + verbose = true; + } else if arg == "-1" || arg == "--kdl-v1" { + if kdl_version != 0 { + return Err(CliError::MultipleKdlVersion); + } + + kdl_version = 1; + } else if arg == "-2" || arg == "--kdl-v2" { + if kdl_version != 0 { + return Err(CliError::MultipleKdlVersion); + } + + kdl_version = 2; + } + } + + if kdl_version == 0 { + kdl_version = 2; + }; - let positional: Vec = args - .iter() - .skip(1) - .filter(|arg| !arg.starts_with('-')) - .cloned() - .collect(); + let input = positional + .get(0) + .ok_or(CliError::MissingInput)? + .to_string(); - let input = positional.get(0).ok_or(CliError::MissingInput)?.to_string(); - let output = positional.get(1).ok_or(CliError::MissingOutput)?.to_string(); + let output = positional + .get(1) + .ok_or(CliError::MissingOutput)? + .to_string(); - Ok(Self { + let result = Self { input, output, force, verbose, - }) + kdl_version, + }; + + Ok(result) } } @@ -130,6 +162,6 @@ pub fn run() -> Result<()> { return Err(CliError::FileExists(args.output)); } - convert_file_contents(input_path, output_path, args.verbose)?; + convert_file_contents(input_path, output_path, args.verbose, args.kdl_version)?; Ok(()) } diff --git a/src/convert.rs b/src/convert.rs index 4f8795f..5a78bcb 100644 --- a/src/convert.rs +++ b/src/convert.rs @@ -43,10 +43,10 @@ impl From for ConversionError { pub type Result = std::result::Result; -pub fn convert_file_contents(input: &Path, output: &Path, verbose: bool) -> Result<()> { +pub fn convert_file_contents(input: &Path, output: &Path, verbose: bool, kdl_version: i32) -> Result<()> { let json_content = fs::read_to_string(input)?; let json_value: Value = serde_json::from_str(&json_content)?; - let kdl_doc = json_to_kdl(json_value)?; + let kdl_doc = json_to_kdl(json_value, Some(kdl_version))?; // Create output directory if needed if let Some(parent) = output.parent() { @@ -56,15 +56,15 @@ pub fn convert_file_contents(input: &Path, output: &Path, verbose: bool) -> Resu fs::write(output, kdl_doc.to_string())?; if verbose { - println!("Converted {} -> {}", input.display(), output.display()); + println!("converted {} -> {}", input.display(), output.display()); } Ok(()) } -pub fn json_to_kdl(json: Value) -> Result { +pub fn json_to_kdl(json: Value, kdl_version: Option) -> Result { let array = json.as_array().ok_or_else(|| { - ConversionError::InvalidStructure("Document root must be a JSON array".to_string()) + ConversionError::InvalidStructure("document root must be json array".to_string()) })?; let mut document = KdlDocument::new(); @@ -74,6 +74,17 @@ pub fn json_to_kdl(json: Value) -> Result { document.nodes_mut().push(node); } + // `kdl_version` will be provided for all root calls + if let Some(kdl_ver_num) = kdl_version { + if kdl_ver_num == 1 { + document.ensure_v1(); + } else { + document.ensure_v2(); + } + } + + document.autoformat(); + Ok(document) } @@ -81,14 +92,14 @@ fn json_value_to_node(value: &Value) -> Result { let name = value .get("name") .and_then(|n| n.as_str()) - .ok_or_else(|| ConversionError::InvalidStructure("`name` must exist and be a string".to_string()))?; + .ok_or_else(|| ConversionError::InvalidStructure("name must be non-empty string".to_string()))?; let mut node = KdlNode::new(name); // Handle arguments if let Some(arguments) = value.get("arguments") { let args = arguments.as_array().ok_or_else(|| { - ConversionError::InvalidStructure("`arguments` must be an array".to_string()) + ConversionError::InvalidStructure("arguments must be an array".to_string()) })?; for arg in args { @@ -100,7 +111,7 @@ fn json_value_to_node(value: &Value) -> Result { // Handle properties if let Some(properties) = value.get("properties") { let props = properties.as_object().ok_or_else(|| { - ConversionError::InvalidStructure("`properties` must be an object".to_string()) + ConversionError::InvalidStructure("properties must be an object".to_string()) })?; for (key, prop_value) in props { @@ -111,7 +122,7 @@ fn json_value_to_node(value: &Value) -> Result { // Handle children if let Some(children) = value.get("children") { - let child_doc = json_to_kdl(children.clone())?; + let child_doc = json_to_kdl(children.clone(), None)?; node.set_children(child_doc); } @@ -151,7 +162,7 @@ fn json_value_to_entry(value: &Value) -> Result { Value::String(s) => KdlValue::String(s.clone()), _ => { return Err(ConversionError::InvalidStructure( - "unsupported JSON value type for KDL conversion".to_string(), + "unsupported json value type for kdl conversion".to_string(), )) } };