diff --git a/Cargo.lock b/Cargo.lock index 647f9c9..ce4a4bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "1.0.2" @@ -87,6 +96,30 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "backtrace-ext" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" +dependencies = [ + "backtrace", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -309,6 +342,12 @@ dependencies = [ "wasi", ] +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + [[package]] name = "hashbrown" version = "0.14.3" @@ -354,6 +393,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "is_ci" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb" + [[package]] name = "itertools" version = "0.10.5" @@ -421,8 +466,17 @@ version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e" dependencies = [ + "backtrace", + "backtrace-ext", + "is-terminal", "miette-derive", "once_cell", + "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", + "textwrap", "thiserror", "unicode-width", ] @@ -463,12 +517,27 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + [[package]] name = "phf" version = "0.8.0" @@ -666,6 +735,12 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + [[package]] name = "rustix" version = "0.38.7" @@ -745,12 +820,46 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "supports-color" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6398cde53adc3c4557306a96ce67b302968513830a77a95b2b17305d9719a89" +dependencies = [ + "is-terminal", + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84231692eb0d4d41e4cdd0cabfdd2e6cd9e255e65f80c9aa7c98dd502b4233d" +dependencies = [ + "is-terminal", +] + +[[package]] +name = "supports-unicode" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6c2cb240ab5dd21ed4906895ee23fe5a48acdbd15a3ce388e7b62a9b66baf7" +dependencies = [ + "is-terminal", +] + [[package]] name = "syn" version = "1.0.109" @@ -795,12 +904,33 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "termtree" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" +[[package]] +name = "textwrap" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7b3e525a49ec206798b40326a44121291b530c963cfb01018f63e135bac543d" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.51" @@ -827,6 +957,12 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + [[package]] name = "unicode-width" version = "0.1.11" diff --git a/Cargo.toml b/Cargo.toml index 3a3c5b2..b6e0901 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,4 +36,5 @@ serde_yaml = "0.9" [dev-dependencies] assert_cmd = "2.0" +miette = { version = "5.10", features = ["fancy"] } tempfile = "3" diff --git a/src/parse_deser/kdl.rs b/src/parse_deser/kdl.rs index 8cd0293..9aaf479 100644 --- a/src/parse_deser/kdl.rs +++ b/src/parse_deser/kdl.rs @@ -43,7 +43,7 @@ pub enum ParseError { UnexpectedChild { child_name: String, allowed: String, - #[label("only {allowed} are allowed, not {child_name}")] + #[label("only {allowed} is allowed, not {child_name}")] span: SourceSpan, }, @@ -100,7 +100,7 @@ pub enum ParseError { InvalidType(String, #[label("unknown type {0}")] SourceSpan), } -type Result = std::result::Result; +type ParseResult = std::result::Result; /// Parse a string as KDL and convert it to a [`CommandInfo`] /// @@ -110,7 +110,9 @@ type Result = std::result::Result; /// - The document isn't valid KDL /// - The document doesn't have exactly one node /// - The format of the document doesn't match the shape of a [`CommandInfo`] -pub fn parse_from_str(text: &str) -> Result { +pub fn parse_from_str( + text: &str, +) -> std::result::Result { let doc: KdlDocument = text.parse()?; let nodes = doc.nodes(); if nodes.is_empty() { @@ -128,63 +130,34 @@ pub fn parse_from_str(text: &str) -> Result { /// Convert a KDL node representing a command to a [`CommandInfo`] /// /// Returns a list of all errors encountered along the way, if it failed -fn kdl_to_cmd_info( - node: &KdlNode, -) -> std::result::Result { +fn kdl_to_cmd_info(node: &KdlNode) -> ParseResult { let name = node.name().to_string(); let mut flags = vec![]; - let args = vec![]; // todo parse arg types at some point + let mut args = vec![]; let mut subcommands = vec![]; if let Some(doc) = node.children() { - let mut first_flags_node = None; - let mut first_subcmds_node = None; - - for node in doc.nodes() { - match node.name().to_string().as_str() { - "flags" => { - if let Some(prev_span) = first_flags_node { - return Err(ParseError::DuplicateChild { - child_name: "flags".to_string(), - span: *node.name().span(), - prev_span, - }); - } else { - first_flags_node = Some(*node.name().span()); - - let mut flag_spans = HashMap::new(); - - if let Some(children) = node.children() { - for flag_node in children.nodes() { - flags.push(parse_flag(flag_node, &mut flag_spans)?); - } - } - } - } - "subcommands" => { - if let Some(prev_span) = first_subcmds_node { - return Err(ParseError::DuplicateChild { - child_name: "subcommands".to_string(), - span: *node.name().span(), - prev_span, - }); - } else { - first_subcmds_node = Some(*node.name().span()); - - if let Some(children) = node.children() { - for subcmd_node in children.nodes() { - subcommands.push(kdl_to_cmd_info(subcmd_node)?); - } - } - } - } - name => { - return Err(ParseError::UnexpectedChild { - child_name: name.to_string(), - allowed: "flags and subcommands".to_string(), - span: *node.name().span(), - }); - } + let nodes = get_nodes(doc, &["flags", "args", "desc"])?; + + if let Some(flags_doc) = nodes.get("flags").and_then(|node| node.children()) + { + let mut flag_spans = HashMap::new(); + for flag_node in flags_doc.nodes() { + flags.push(parse_flag(flag_node, &mut flag_spans)?); + } + } + + if let Some(arg_doc) = nodes.get("args").and_then(|node| node.children()) { + for node in arg_doc.nodes() { + args.push(parse_type(node)?); + } + } + + if let Some(subcmds_doc) = + nodes.get("subcommands").and_then(|node| node.children()) + { + for subcmd_node in subcmds_doc.nodes() { + subcommands.push(kdl_to_cmd_info(subcmd_node)?); } } } @@ -202,7 +175,7 @@ fn kdl_to_cmd_info( fn parse_flag( node: &KdlNode, flag_spans: &mut HashMap, -) -> std::result::Result { +) -> ParseResult { let mut forms = vec![]; let mut desc = None; let mut typ = None; @@ -248,71 +221,32 @@ fn parse_flag( } if let Some(doc) = node.children() { - let mut first_desc_node = None; - let mut first_type_node = None; - for node in doc.nodes() { - match node.name().to_string().as_str() { - "desc" => { - if let Some(prev_span) = first_desc_node { - return Err(ParseError::DuplicateChild { - child_name: "desc".to_string(), - span: *node.name().span(), - prev_span, - }); - } else { - first_desc_node = Some(*node.name().span()); - if node.entries().len() == 1 { - // todo account for invalid entry with name - desc = Some(strip_quotes(&node.entries()[0].value().to_string())); - } else { - todo!() - } - } - } - "type" => { - if let Some(prev_span) = first_type_node { - return Err(ParseError::DuplicateChild { - child_name: "flags".to_string(), - span: *node.name().span(), - prev_span, - }); - } else { - first_type_node = Some(*node.name().span()); - - if let Some(children) = node.children() { - let mut types = Vec::new(); - for type_node in children.nodes() { - let typ = match type_node.name().to_string().as_str() { - "path" => ArgType::Path, - "dir" => ArgType::Dir, - // todo handle other variants - typ => { - return Err(ParseError::InvalidType( - typ.to_string(), - *type_node.name().span(), - )); - } - }; - types.push(typ); - } + let nodes = get_nodes(doc, &["desc", "type"])?; - if types.len() == 1 { - typ = Some(types.pop().unwrap()); - } else { - typ = Some(ArgType::Any(types)); - } - } else { - return Err(ParseError::EmptyType(*node.span())); - } - } - } - name => { - return Err(ParseError::UnexpectedChild { - child_name: name.to_string(), - allowed: "desc and type".to_string(), - span: *node.name().span(), - }); + if let Some(desc_node) = nodes.get("desc") { + if desc_node.entries().len() == 1 { + // todo account for invalid entry with name + desc = Some(strip_quotes(&desc_node.entries()[0].value().to_string())); + } else { + todo!() + } + } + + if let Some(type_node) = nodes.get("type") { + if let Some(children) = type_node.children() { + let types = children + .nodes() + .iter() + .map(|node| parse_type(node)) + .collect::>>()?; + + if types.len() == 1 { + typ = Some(types.into_iter().next().unwrap()); + } else { + typ = Some(ArgType::Any(types)); } + } else { + return Err(ParseError::EmptyType(*type_node.span())); } } } @@ -320,21 +254,37 @@ fn parse_flag( Ok(Flag { forms, desc, typ }) } -/// Get nodes with the given names, and error if there are duplicates or +/// Helper to treat a node as an [`ArgType`] +fn parse_type(node: &KdlNode) -> ParseResult { + let typ = match node.name().to_string().as_str() { + "path" => ArgType::Path, + "dir" => ArgType::Dir, + // todo handle other variants + typ => { + return Err(ParseError::InvalidType( + typ.to_string(), + *node.name().span(), + )); + } + }; + Ok(typ) +} + +/// Helper to get nodes with the given names. Errors if there are duplicates or /// unrecognized nodes -fn get_nodes( - doc: KdlDocument, - names: &[String], -) -> std::result::Result, ParseError> { - let mut nodes = HashMap::::new(); +fn get_nodes<'a>( + doc: &'a KdlDocument, + names: &[&str], +) -> ParseResult> { + let mut nodes = HashMap::::new(); - for node in doc.into_iter() { + for node in doc.nodes() { let name = node.name().to_string(); let span = *node.name().span(); - if !names.contains(&name) { + if !names.contains(&name.as_str()) { return Err(ParseError::UnexpectedChild { child_name: name, - allowed: names.join(", "), + allowed: format!("one of [{}]", names.join(", ")), span, }); } @@ -366,11 +316,11 @@ fn strip_quotes(flag: &str) -> String { #[cfg(test)] mod tests { - use super::{parse_from_str, Result}; + use super::parse_from_str; use crate::{ArgType, CommandInfo, Flag}; #[test] - fn test1() -> Result<()> { + fn test1() -> miette::Result<()> { assert_eq!( CommandInfo { name: "foo".to_string(), @@ -379,7 +329,7 @@ mod tests { desc: Some("Show help output".to_string()), typ: Some(ArgType::Path), }], - args: vec![], + args: vec![ArgType::Dir], subcommands: vec![] }, parse_from_str( @@ -388,9 +338,14 @@ mod tests { flags { "--help" "-h" { desc "Show help output" - type path + type { + path + } } } + args { + dir + } } "# )?