From 76f518080f8c7ca0ca7005873d95144681f1d32f Mon Sep 17 00:00:00 2001 From: Jeff Dickey <216188+jdx@users.noreply.github.com> Date: Sat, 13 Jan 2024 22:43:48 -0600 Subject: [PATCH] wip --- cli/src/cli/generate/completion.rs | 2 +- cli/src/cli/generate/markdown.rs | 16 +++- cli/src/cli/generate/mod.rs | 2 +- examples/MISE_README.md | 4 +- src/error.rs | 18 ++-- src/parse/arg.rs | 47 +++++----- src/parse/cmd.rs | 117 ++++++++++++------------ src/parse/config.rs | 88 +++++++++--------- src/parse/context.rs | 23 +++++ src/parse/flag.rs | 56 +++++------- src/parse/helpers.rs | 67 +++++++++----- src/parse/mod.rs | 1 + src/parse/spec.rs | 142 +++++++++++++++-------------- 13 files changed, 313 insertions(+), 270 deletions(-) create mode 100644 src/parse/context.rs diff --git a/cli/src/cli/generate/completion.rs b/cli/src/cli/generate/completion.rs index 7a6609d..79ab427 100644 --- a/cli/src/cli/generate/completion.rs +++ b/cli/src/cli/generate/completion.rs @@ -23,7 +23,7 @@ impl Completion { let (spec, _) = Spec::parse_file(file)?; spec } else { - self.spec.as_ref().unwrap().parse()? + Spec::parse_spec(self.spec.as_ref().unwrap())? }; let script = match self.shell.as_str() { "bash" => usage::complete::bash::complete_bash(&spec), diff --git a/cli/src/cli/generate/markdown.rs b/cli/src/cli/generate/markdown.rs index b2bab60..44b7ad7 100644 --- a/cli/src/cli/generate/markdown.rs +++ b/cli/src/cli/generate/markdown.rs @@ -1,7 +1,7 @@ use std::fmt::{Display, Formatter}; -use std::fs; use std::path::{Path, PathBuf}; use std::sync::Mutex; +use std::{env, fs}; use clap::Args; use contracts::requires; @@ -54,7 +54,7 @@ impl Markdown { context::set_load_root(inject.parent().unwrap().to_path_buf()); let out = parse_readme_directives(inject, &raw)? .into_iter() - .try_fold(UsageMdContext::new(), |ctx, d| d.run(ctx))? + .try_fold(UsageMdContext::new(inject), |ctx, d| d.run(ctx))? .out .lock() .unwrap() @@ -259,18 +259,20 @@ impl Display for UsageMdDirective { struct UsageMdContext { plain: bool, + root: PathBuf, spec: Option, out: Mutex>, tera: tera::Context, } impl UsageMdContext { - fn new() -> Self { + fn new(inject: &Path) -> Self { Self { plain: true, spec: None, out: Mutex::new(vec![]), tera: tera::Context::new(), + root: inject.parent().unwrap().to_path_buf(), } } @@ -286,7 +288,10 @@ impl UsageMdDirective { fn run(&self, mut ctx: UsageMdContext) -> miette::Result { match self { UsageMdDirective::Load { file } => { - let file = context::prepend_load_root(file); + let file = match file.is_relative() { + true => ctx.root.join(file), + false => file.to_path_buf(), + }; let spec: Spec = Spec::parse_file(&file)?.0; ctx.tera.insert("spec", &spec.clone()); let commands: Vec<_> = gather_subcommands(&[&spec.cmd]) @@ -318,7 +323,8 @@ impl UsageMdDirective { let args = spec.cmd.args.iter().filter(|a| !a.hide).collect::>(); if !args.is_empty() { for arg in args { - let name = &arg.usage(); + // let name = &arg.usage(); + let name = "USAGE"; if let Some(about) = &arg.long_help { ctx.push(format!("### {name}", name = name)); ctx.push(about.to_string()); diff --git a/cli/src/cli/generate/mod.rs b/cli/src/cli/generate/mod.rs index 5245fc5..c5e78b3 100644 --- a/cli/src/cli/generate/mod.rs +++ b/cli/src/cli/generate/mod.rs @@ -33,6 +33,6 @@ pub fn file_or_spec(file: &Option, spec: &Option) -> Result -# {spec.name} +# mise @@ -9,7 +9,7 @@ ## Usage ```bash -mise [flags] [args] +Usage: mise [OPTIONS] ``` diff --git a/src/error.rs b/src/error.rs index 6e43ac1..066a1e0 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,3 @@ -use crate::Spec; use miette::{Diagnostic, NamedSource, SourceSpan}; use thiserror::Error; @@ -33,21 +32,16 @@ pub enum UsageErr { XXError(#[from] xx::error::XXError), } -impl UsageErr { - pub fn new(msg: String, span: &SourceSpan) -> Self { - let named_source = Spec::get_parsing_file(); - Self::InvalidInput(msg, *span, named_source) - } -} - #[macro_export] macro_rules! bail_parse { - ($span:expr, $fmt:literal) => {{ + ($ctx:expr, $span:expr, $fmt:literal) => {{ let msg = format!($fmt); - return std::result::Result::Err(UsageErr::new(msg, $span.span())); + let err = $ctx.build_err(msg, $span); + return std::result::Result::Err(err); }}; - ($span:expr, $fmt:literal, $($arg:tt)*) => {{ + ($ctx:expr, $span:expr, $fmt:literal, $($arg:tt)*) => {{ let msg = format!($fmt, $($arg)*); - return std::result::Result::Err(UsageErr::new(msg, $span.span())); + let err = $ctx.build_err(msg, $span); + return std::result::Result::Err(err); }}; } diff --git a/src/parse/arg.rs b/src/parse/arg.rs index 51ed1c3..04bc81b 100644 --- a/src/parse/arg.rs +++ b/src/parse/arg.rs @@ -6,6 +6,7 @@ use kdl::{KdlEntry, KdlNode}; use serde::Serialize; use crate::error::UsageErr; +use crate::parse::context::ParsingContext; use crate::parse::helpers::NodeHelper; #[derive(Debug, Default, Serialize, Clone)] @@ -23,7 +24,28 @@ pub struct Arg { } impl Arg { - pub fn usage(&self) -> String { + pub(crate) fn parse(ctx: &ParsingContext, node: &NodeHelper) -> Result { + let mut arg: Arg = node.arg(0)?.ensure_string()?.parse()?; + for (k, v) in node.props() { + match k { + "help" => arg.help = Some(v.ensure_string()?), + "long_help" => arg.long_help = Some(v.ensure_string()?), + "required" => arg.required = v.ensure_bool()?, + "var" => arg.var = v.ensure_bool()?, + "hide" => arg.hide = v.ensure_bool()?, + "var_min" => arg.var_min = v.ensure_i64().map(Some)?, + "var_max" => arg.var_max = v.ensure_i64().map(Some)?, + "default" => arg.default = v.ensure_string().map(Some)?, + k => bail_parse!(ctx, *v.entry.span(), "unsupported arg key {k}"), + } + } + arg.usage = arg.usage(); + Ok(arg) + } +} + +impl Arg { + pub(crate) fn usage(&self) -> String { let mut name = if self.required { format!("<{}>", self.name) } else { @@ -65,29 +87,6 @@ impl From<&Arg> for KdlNode { } } -impl TryFrom<&KdlNode> for Arg { - type Error = UsageErr; - fn try_from(node: &KdlNode) -> Result { - let hnode: NodeHelper = node.into(); - let mut arg: Arg = hnode.arg(0)?.ensure_string()?.parse()?; - for (k, v) in hnode.props() { - match k { - "help" => arg.help = Some(v.ensure_string()?), - "long_help" => arg.long_help = Some(v.ensure_string()?), - "required" => arg.required = v.ensure_bool()?, - "var" => arg.var = v.ensure_bool()?, - "hide" => arg.hide = v.ensure_bool()?, - "var_min" => arg.var_min = v.ensure_i64().map(Some)?, - "var_max" => arg.var_max = v.ensure_i64().map(Some)?, - "default" => arg.default = v.ensure_string().map(Some)?, - k => bail_parse!(v.entry, "unsupported key {k}"), - } - } - arg.usage = arg.usage(); - Ok(arg) - } -} - impl From<&str> for Arg { fn from(input: &str) -> Self { let mut arg = Arg { diff --git a/src/parse/cmd.rs b/src/parse/cmd.rs index 86bc348..4a83e3b 100644 --- a/src/parse/cmd.rs +++ b/src/parse/cmd.rs @@ -1,4 +1,5 @@ use crate::error::UsageErr; +use crate::parse::context::ParsingContext; use crate::parse::helpers::NodeHelper; use crate::{Arg, Flag, Spec}; use indexmap::IndexMap; @@ -24,6 +25,63 @@ pub struct SchemaCmd { pub after_long_help: Option, } +impl SchemaCmd { + pub(crate) fn parse(ctx: &ParsingContext, node: &NodeHelper) -> Result { + node.ensure_args_count(1, 1)?; + let mut cmd = Self { + name: node.arg(0)?.ensure_string()?.to_string(), + ..Default::default() + }; + for (k, v) in node.props() { + match k { + "help" => cmd.help = Some(v.ensure_string()?), + "long_help" => cmd.long_help = Some(v.ensure_string()?), + "before_help" => cmd.before_help = Some(v.ensure_string()?), + "before_long_help" => cmd.before_long_help = Some(v.ensure_string()?), + "after_help" => cmd.after_help = Some(v.ensure_string()?), + "after_long_help" => { + cmd.after_long_help = Some(v.ensure_string()?); + } + "subcommand_required" => cmd.subcommand_required = v.ensure_bool()?, + "hide" => cmd.hide = v.ensure_bool()?, + k => bail_parse!(ctx, node.span(), "unsupported cmd key {k}"), + } + } + for child in node.children() { + let child: NodeHelper = child.into(); + match child.name() { + "flag" => cmd.flags.push(Flag::parse(ctx, &child)?), + "arg" => cmd.args.push(Arg::parse(ctx, &child)?), + "cmd" => { + let node = SchemaCmd::parse(ctx, &child)?; + cmd.subcommands.insert(node.name.to_string(), node); + } + "alias" => { + let alias = child + .node + .entries() + .iter() + .filter_map(|e| e.value().as_string().map(|v| v.to_string())) + .collect::>(); + let hide = child + .props() + .get("hide") + .map(|n| n.ensure_bool()) + .transpose()? + .unwrap_or(false); + if hide { + cmd.hidden_aliases.extend(alias); + } else { + cmd.aliases.extend(alias); + } + } + k => bail_parse!(ctx, *child.node.span(), "unsupported cmd key {k}"), + } + } + Ok(cmd) + } +} + impl From<&SchemaCmd> for KdlNode { fn from(cmd: &SchemaCmd) -> Self { let mut node = Self::new("cmd"); @@ -92,65 +150,6 @@ impl From<&SchemaCmd> for KdlNode { } } -impl TryFrom<&KdlNode> for SchemaCmd { - type Error = UsageErr; - fn try_from(node: &KdlNode) -> Result { - let hnode: NodeHelper = node.into(); - hnode.ensure_args_count(1, 1)?; - let mut cmd = Self { - name: hnode.arg(0)?.ensure_string()?.to_string(), - ..Default::default() - }; - for (k, v) in hnode.props() { - match k { - "help" => cmd.help = Some(v.ensure_string()?), - "long_help" => cmd.long_help = Some(v.ensure_string()?), - "before_help" => cmd.before_help = Some(v.ensure_string()?), - "before_long_help" => cmd.before_long_help = Some(v.ensure_string()?), - "after_help" => cmd.after_help = Some(v.ensure_string()?), - "after_long_help" => { - cmd.after_long_help = Some(v.ensure_string()?); - } - "subcommand_required" => cmd.subcommand_required = v.ensure_bool()?, - "hide" => cmd.hide = v.ensure_bool()?, - k => bail_parse!(node, "unsupported key {k}"), - } - } - for child in node.children().map(|c| c.nodes()).unwrap_or_default() { - let child: NodeHelper = child.into(); - match child.name() { - "flag" => cmd.flags.push(child.node.try_into()?), - "arg" => cmd.args.push(child.node.try_into()?), - "cmd" => { - let node: SchemaCmd = child.node.try_into()?; - cmd.subcommands.insert(node.name.to_string(), node); - } - "alias" => { - let alias = child - .node - .entries() - .iter() - .filter_map(|e| e.value().as_string().map(|v| v.to_string())) - .collect::>(); - let hide = child - .props() - .get("hide") - .map(|n| n.ensure_bool()) - .transpose()? - .unwrap_or(false); - if hide { - cmd.hidden_aliases.extend(alias); - } else { - cmd.aliases.extend(alias); - } - } - k => bail_parse!(child.node, "unsupported key {k}"), - } - } - Ok(cmd) - } -} - #[cfg(feature = "clap")] impl From<&clap::Command> for SchemaCmd { fn from(cmd: &clap::Command) -> Self { diff --git a/src/parse/config.rs b/src/parse/config.rs index cde50bc..d392d51 100644 --- a/src/parse/config.rs +++ b/src/parse/config.rs @@ -1,10 +1,11 @@ use std::collections::BTreeMap; -use kdl::{KdlDocument, KdlEntry, KdlNode, }; -use serde::{Serialize}; +use kdl::{KdlDocument, KdlEntry, KdlNode}; +use serde::Serialize; use crate::bail_parse; use crate::error::UsageErr; +use crate::parse::context::ParsingContext; use crate::parse::data_types::SpecDataTypes; use crate::parse::helpers::NodeHelper; @@ -13,6 +14,44 @@ pub struct SpecConfig { pub props: BTreeMap, } +impl SpecConfig { + pub(crate) fn parse(ctx: &ParsingContext, node: &NodeHelper) -> Result { + let mut config = Self::default(); + for node in node.children() { + node.ensure_args_count(1, 1)?; + match node.name() { + "prop" => { + let key = node.arg(0)?; + let key = key.ensure_string()?.to_string(); + let mut prop = SpecConfigProp::default(); + for (k, v) in node.props() { + match k { + "default" => prop.default = v.value.to_string().into(), + "default_note" => prop.default_note = Some(v.ensure_string()?), + "data_type" => prop.data_type = v.ensure_string()?.parse()?, + "env" => prop.env = v.ensure_string()?.to_string().into(), + "help" => prop.help = v.ensure_string()?.to_string().into(), + "long_help" => prop.long_help = v.ensure_string()?.to_string().into(), + k => bail_parse!(ctx, node.span(), "unsupported config prop key {k}"), + } + } + config.props.insert(key, prop); + } + k => bail_parse!(ctx, *node.node.name().span(), "unsupported config key {k}"), + } + } + Ok(config) + } + + pub(crate) fn merge(&mut self, other: &Self) { + for (key, prop) in &other.props { + self.props + .entry(key.to_string()) + .or_insert_with(|| prop.clone()); + } + } +} + impl SpecConfig { pub fn is_empty(&self) -> bool { self.props.is_empty() @@ -52,42 +91,6 @@ impl SpecConfigProp { } } -impl TryFrom<&KdlNode> for SpecConfig { - type Error = UsageErr; - fn try_from(doc: &KdlNode) -> Result { - let mut config = Self::default(); - if let Some(children) = doc.children().map(|doc| doc.nodes()) { - for node in children { - let ph = NodeHelper::new(node); - ph.ensure_args_count(1, 1)?; - match ph.name() { - "prop" => { - let key = ph.arg(0)?; - let key = key.ensure_string()?.to_string(); - let mut prop = SpecConfigProp::default(); - for (k, v) in ph.props() { - match k { - "default" => prop.default = v.value.to_string().into(), - "default_note" => prop.default_note = Some(v.ensure_string()?), - "data_type" => prop.data_type = v.ensure_string()?.parse()?, - "env" => prop.env = v.ensure_string()?.to_string().into(), - "help" => prop.help = v.ensure_string()?.to_string().into(), - "long_help" => { - prop.long_help = v.ensure_string()?.to_string().into() - } - _ => bail_parse!(ph.node, "unsupported key {k}"), - } - } - config.props.insert(key, prop); - } - k => bail_parse!(node.name(), "unsupported key {k}"), - } - } - } - Ok(config) - } -} - impl Default for SpecConfigProp { fn default() -> Self { Self { @@ -118,7 +121,9 @@ mod tests { #[test] fn test_config_defaults() { - let spec: Spec = r#" + let spec = Spec::parse( + &Default::default(), + r#" config { prop "color" default=true env="COLOR" help="Enable color output" prop "user" default="admin" env="USER" help="User to run as" @@ -126,9 +131,10 @@ config { prop "timeout" default=1.5 env="TIMEOUT" help="Timeout in seconds" \ long_help="Timeout in seconds, can be fractional" } - "# - .parse() + "#, + ) .unwrap(); + assert_display_snapshot!(spec, @r###" config { prop "color" default="true" env="COLOR" help="Enable color output" diff --git a/src/parse/context.rs b/src/parse/context.rs new file mode 100644 index 0000000..837704c --- /dev/null +++ b/src/parse/context.rs @@ -0,0 +1,23 @@ +use crate::error::UsageErr; +use miette::{NamedSource, SourceSpan}; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Default)] +pub struct ParsingContext { + pub(crate) file: PathBuf, + pub(crate) spec: String, +} + +impl ParsingContext { + pub(crate) fn new(file: &Path, spec: &str) -> Self { + Self { + file: file.to_path_buf(), + spec: spec.to_string(), + } + } + + pub(crate) fn build_err(&self, msg: String, span: SourceSpan) -> UsageErr { + let source = NamedSource::new(self.file.to_string_lossy(), self.spec.clone()); + UsageErr::InvalidInput(msg, span, source) + } +} \ No newline at end of file diff --git a/src/parse/flag.rs b/src/parse/flag.rs index 38adc4f..d8d49e6 100644 --- a/src/parse/flag.rs +++ b/src/parse/flag.rs @@ -6,6 +6,7 @@ use serde::Serialize; use crate::error::UsageErr; use crate::error::UsageErr::InvalidFlag; +use crate::parse::context::ParsingContext; use crate::parse::helpers::NodeHelper; use crate::{bail_parse, Arg}; @@ -27,6 +28,30 @@ pub struct Flag { } impl Flag { + pub(crate) fn parse(ctx: &ParsingContext, node: &NodeHelper) -> Result { + let mut flag: Self = node.arg(0)?.ensure_string()?.parse()?; + for (k, v) in node.props() { + match k { + "help" => flag.help = Some(v.ensure_string()?), + "long_help" => flag.long_help = Some(v.ensure_string()?), + "required" => flag.required = v.ensure_bool()?, + "var" => flag.var = v.ensure_bool()?, + "hide" => flag.hide = v.ensure_bool()?, + "global" => flag.global = v.ensure_bool()?, + "count" => flag.count = v.ensure_bool()?, + "default" => flag.default = v.ensure_string().map(Some)?, + k => bail_parse!(ctx, *v.entry.span(), "unsupported flag key {k}"), + } + } + for child in node.children() { + match child.name() { + "arg" => flag.arg = Some(Arg::parse(ctx, &child)?), + k => bail_parse!(ctx, *child.node.span(), "unsupported flag value key {k}"), + } + } + flag.usage = flag.usage(); + Ok(flag) + } pub fn usage(&self) -> String { let mut name = self .short @@ -82,37 +107,6 @@ impl From<&Flag> for KdlNode { } } -impl TryFrom<&KdlNode> for Flag { - type Error = UsageErr; - fn try_from(node: &KdlNode) -> Result { - let hnode: NodeHelper = node.into(); - let mut flag: Self = hnode.arg(0)?.ensure_string()?.parse()?; - for (k, v) in hnode.props() { - match k { - "help" => flag.help = Some(v.ensure_string()?), - "long_help" => flag.long_help = Some(v.ensure_string()?), - "required" => flag.required = v.ensure_bool()?, - "var" => flag.var = v.ensure_bool()?, - "hide" => flag.hide = v.ensure_bool()?, - "global" => flag.global = v.ensure_bool()?, - "count" => flag.count = v.ensure_bool()?, - "default" => flag.default = v.ensure_string().map(Some)?, - k => bail_parse!(v.entry, "unsupported key {k}"), - } - } - let children = node.children().map(|c| c.nodes()).unwrap_or_default(); - for child in children { - let child: NodeHelper = child.into(); - match child.name() { - "arg" => flag.arg = Some(child.node.try_into()?), - k => bail_parse!(child.node, "unsupported key {k}"), - } - } - flag.usage = flag.usage(); - Ok(flag) - } -} - impl FromStr for Flag { type Err = UsageErr; fn from_str(input: &str) -> std::result::Result { diff --git a/src/parse/helpers.rs b/src/parse/helpers.rs index 0a7da13..5bea665 100644 --- a/src/parse/helpers.rs +++ b/src/parse/helpers.rs @@ -1,21 +1,27 @@ use indexmap::IndexMap; use kdl::{KdlEntry, KdlNode, KdlValue}; +use miette::SourceSpan; use crate::error::UsageErr; +use crate::parse::context::ParsingContext; #[derive(Debug)] pub struct NodeHelper<'a> { pub(crate) node: &'a KdlNode, + pub(crate) ctx: &'a ParsingContext, } impl<'a> NodeHelper<'a> { - pub(crate) fn new(node: &'a KdlNode) -> Self { - Self { node } + pub(crate) fn new(ctx: &'a ParsingContext, node: &'a KdlNode) -> Self { + Self { node, ctx } } pub(crate) fn name(&self) -> &str { self.node.name().value() } + pub(crate) fn span(&self) -> SourceSpan { + *self.node.span() + } pub(crate) fn ensure_args_count(&self, min: usize, max: usize) -> Result<(), UsageErr> { let count = self .node @@ -24,79 +30,92 @@ impl<'a> NodeHelper<'a> { .filter(|e| e.name().is_none()) .count(); if count < min || count > max { - bail_parse!( - self.node, - "expected {} to {} arguments, got {}", - min, - max, - count - ) + let ctx = self.ctx; + let span = self.span(); + bail_parse!(ctx, span, "expected {min} to {max} arguments, got {count}") } Ok(()) } pub(crate) fn arg(&self, i: usize) -> Result { if let Some(entry) = self.node.entries().get(i) { if entry.name().is_some() { - bail_parse!(entry, "expected argument, got param: {}", entry.to_string()) + let ctx = self.ctx; + let span = *entry.span(); + let param = entry.to_string(); + bail_parse!(ctx, span, "expected argument, got param: {param}") } - return Ok(entry.into()); + return Ok(ParseEntry::new(self.ctx, entry)); } - bail_parse!(self.node, "missing argument") + bail_parse!(self.ctx, self.span(), "missing argument") } pub(crate) fn props(&self) -> IndexMap<&str, ParseEntry> { self.node .entries() .iter() - .filter_map(|e| e.name().map(|key| (key.value(), e.into()))) + .filter_map(|e| { + e.name() + .map(|key| (key.value(), ParseEntry::new(self.ctx, e))) + }) .collect() } -} - -impl<'a> From<&'a KdlNode> for NodeHelper<'a> { - fn from(node: &'a KdlNode) -> Self { - Self { node } + pub(crate) fn children(&self) -> Vec { + self.node + .children() + .map(|c| { + c.nodes() + .iter() + .map(|n| NodeHelper::new(&self.ctx, n)) + .collect() + }) + .unwrap_or_default() } } #[derive(Debug)] pub(crate) struct ParseEntry<'a> { + pub(crate) ctx: &'a ParsingContext, pub(crate) entry: &'a KdlEntry, pub(crate) value: &'a KdlValue, } -impl<'a> From<&'a KdlEntry> for ParseEntry<'a> { - fn from(entry: &'a KdlEntry) -> Self { +impl<'a> ParseEntry<'a> { + fn new(ctx: &'a ParsingContext, entry: &'a KdlEntry) -> Self { Self { + ctx, entry, value: entry.value(), } } + + fn span(&self) -> SourceSpan { + *self.entry.span() + } } impl<'a> ParseEntry<'a> { pub fn ensure_i64(&self) -> Result { match self.value.as_i64() { Some(i) => Ok(i), - None => bail_parse!(self.entry, "expected integer"), + None => bail_parse!(self.ctx, self.span(), "expected integer"), } } #[allow(dead_code)] pub fn ensure_f64(&self) -> Result { match self.value.as_f64() { Some(f) => Ok(f), - None => bail_parse!(self.entry, "expected float"), + None => bail_parse!(self.ctx, self.span(), "expected float"), } } pub fn ensure_bool(&self) -> Result { match self.value.as_bool() { Some(b) => Ok(b), - None => bail_parse!(self.entry, "expected bool"), + None => bail_parse!(self.ctx, self.span(), "expected bool"), } } pub fn ensure_string(&self) -> Result { match self.value.as_string() { Some(s) => Ok(s.to_string()), - None => bail_parse!(self.entry, "expected string"), + None => bail_parse!(self.ctx, self.span(), "expected string"), } } } diff --git a/src/parse/mod.rs b/src/parse/mod.rs index db0dcea..fa081cb 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -1,6 +1,7 @@ pub mod arg; pub mod cmd; pub mod config; +mod context; mod data_types; pub mod flag; pub(crate) mod helpers; diff --git a/src/parse/spec.rs b/src/parse/spec.rs index 3a21994..27f6178 100644 --- a/src/parse/spec.rs +++ b/src/parse/spec.rs @@ -1,18 +1,17 @@ use std::fmt::{Display, Formatter}; -use std::path::{Path, PathBuf}; -use std::str::FromStr; +use std::iter::once; +use std::path::Path; use kdl::{KdlDocument, KdlEntry, KdlNode}; - -use xx::{context, file}; +use serde::Serialize; +use xx::file; use crate::error::UsageErr; use crate::parse::cmd::SchemaCmd; use crate::parse::config::SpecConfig; -use miette::NamedSource; -use serde::Serialize; -use std::cell::RefCell; -use std::iter::once; +use crate::parse::context::ParsingContext; +use crate::parse::helpers::NodeHelper; +use crate::{Arg, Flag}; #[derive(Debug, Default, Clone, Serialize)] pub struct Spec { @@ -28,34 +27,65 @@ pub struct Spec { } impl Spec { - thread_local! { - static PARSING_FILE: RefCell> = RefCell::new(None); - } - pub fn parse_file(file: &Path) -> Result<(Spec, String), UsageErr> { let (spec, body) = split_script(file)?; - Self::set_parsing_file(Some((file.to_path_buf(), spec.clone()))); - let mut schema = Self::from_str(&spec)?; + let ctx = ParsingContext::new(file, &spec); + let mut schema = Self::parse(&ctx, &spec)?; if schema.bin.is_empty() { schema.bin = file.file_name().unwrap().to_str().unwrap().to_string(); } if schema.name.is_empty() { schema.name = schema.bin.clone(); } - Self::set_parsing_file(None); Ok((schema, body)) } - - pub fn get_parsing_file() -> NamedSource { - Self::PARSING_FILE.with(|f| { - f.borrow() - .as_ref() - .map(|(p, s)| NamedSource::new(p.to_string_lossy(), s.clone())) - .unwrap_or_else(|| NamedSource::new("", "".to_string())) - }) + pub fn parse_spec(input: &str) -> Result { + Self::parse(&Default::default(), input) } - fn set_parsing_file(file: Option<(PathBuf, String)>) { - Self::PARSING_FILE.with(|f| *f.borrow_mut() = file); + + pub(crate) fn parse(ctx: &ParsingContext, input: &str) -> Result { + let kdl: KdlDocument = input + .parse() + .map_err(|err: kdl::KdlError| UsageErr::KdlError(err))?; + let mut schema = Self { + ..Default::default() + }; + for node in kdl.nodes().iter().map(|n| NodeHelper::new(ctx, n)) { + match node.name() { + "name" => schema.name = node.arg(0)?.ensure_string()?, + "bin" => schema.bin = node.arg(0)?.ensure_string()?, + "version" => schema.version = Some(node.arg(0)?.ensure_string()?), + "about" => schema.about = Some(node.arg(0)?.ensure_string()?), + "long_about" => schema.long_about = Some(node.arg(0)?.ensure_string()?), + "usage" => schema.usage = node.arg(0)?.ensure_string()?, + "arg" => schema.cmd.args.push(Arg::parse(ctx, &node)?), + "flag" => schema.cmd.flags.push(Flag::parse(ctx, &node)?), + "cmd" => { + let node: SchemaCmd = SchemaCmd::parse(ctx, &node)?; + schema.cmd.subcommands.insert(node.name.to_string(), node); + } + "config" => schema.config = SpecConfig::parse(ctx, &node)?, + "include" => { + let file = node + .props() + .get("file") + .map(|v| v.ensure_string()) + .transpose()? + .ok_or_else(|| ctx.build_err("missing file".into(), node.span()))?; + let file = Path::new(&file); + let file = match file.is_relative() { + true => ctx.file.parent().unwrap().join(file), + false => file.to_path_buf(), + }; + info!("include: {}", file.display()); + let (other, _) = Self::parse_file(&file)?; + schema.merge(other); + } + k => bail_parse!(ctx, node.span(), "unsupported spec key {k}"), + } + } + set_subcommand_ancestors(&mut schema.cmd, &[]); + Ok(schema) } fn merge(&mut self, other: Spec) { @@ -65,6 +95,18 @@ impl Spec { if !other.bin.is_empty() { self.bin = other.bin; } + if !other.usage.is_empty() { + self.usage = other.usage; + } + if other.about.is_some() { + self.about = other.about; + } + if other.long_about.is_some() { + self.long_about = other.long_about; + } + if !other.config.is_empty() { + self.config.merge(&other.config); + } for flag in other.cmd.flags { self.cmd.flags.push(flag); } @@ -86,12 +128,6 @@ fn split_script(file: &Path) -> Result<(String, String), UsageErr> { Ok((schema, body)) } -fn get_string_prop(node: &KdlNode, name: &str) -> Option { - node.get(name) - .and_then(|entry| entry.value().as_string()) - .map(|s| s.to_string()) -} - fn set_subcommand_ancestors(cmd: &mut SchemaCmd, ancestors: &[String]) { let ancestors = ancestors.to_vec(); for subcmd in cmd.subcommands.values_mut() { @@ -104,42 +140,6 @@ fn set_subcommand_ancestors(cmd: &mut SchemaCmd, ancestors: &[String]) { } } -impl FromStr for Spec { - type Err = UsageErr; - fn from_str(input: &str) -> miette::Result { - let kdl: KdlDocument = input - .parse() - .map_err(|err: kdl::KdlError| UsageErr::KdlError(err))?; - let mut schema = Self { - ..Default::default() - }; - for node in kdl.nodes() { - match node.name().to_string().as_str() { - "name" => schema.name = node.entries()[0].value().as_string().unwrap().to_string(), - "bin" => schema.bin = node.entries()[0].value().as_string().unwrap().to_string(), - "arg" => schema.cmd.args.push(node.try_into()?), - "flag" => schema.cmd.flags.push(node.try_into()?), - "cmd" => { - let node: SchemaCmd = node.try_into()?; - schema.cmd.subcommands.insert(node.name.to_string(), node); - } - "config" => schema.config = node.try_into()?, - "include" => { - let file = get_string_prop(node, "file") - .map(context::prepend_load_root) - .ok_or_else(|| UsageErr::new(node.to_string(), node.span()))?; - info!("include: {}", file.display()); - let (spec, _) = split_script(&file)?; - schema.merge(spec.parse()?); - } - k => bail_parse!(node, "unsupported key {k}"), - } - } - set_subcommand_ancestors(&mut schema.cmd, &[]); - Ok(schema) - } -} - impl Display for Spec { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let mut doc = KdlDocument::new(); @@ -230,7 +230,9 @@ mod tests { #[test] fn test_display() { - let spec: Spec = r#" + let spec = Spec::parse( + &Default::default(), + r#" name "Usage CLI" bin "usage" arg "arg1" @@ -241,8 +243,8 @@ cmd "config" { arg "value" } } - "# - .parse() + "#, + ) .unwrap(); assert_display_snapshot!(spec, @r###" name "Usage CLI"