diff --git a/crates/provisioning/Cargo.toml b/crates/provisioning/Cargo.toml index e21991f..9ed5056 100644 --- a/crates/provisioning/Cargo.toml +++ b/crates/provisioning/Cargo.toml @@ -4,10 +4,11 @@ version = "0.1.0" edition = "2021" [dev-dependencies] +miette = { workspace = true } [dependencies] kdl = { workspace = true, features = ["span"] } -miette.workspace = true +miette = { workspace = true } itertools = { workspace = true } phf = { workspace = true, features = ["macros"] } thiserror.workspace = true diff --git a/crates/provisioning/src/commands.rs b/crates/provisioning/src/commands.rs index b14be25..6854059 100644 --- a/crates/provisioning/src/commands.rs +++ b/crates/provisioning/src/commands.rs @@ -2,29 +2,17 @@ // // SPDX-License-Identifier: MPL-2.0 -use std::sync::Arc; - -use kdl::KdlNode; -use miette::NamedSource; +use crate::Context; mod create_partition; mod create_partition_table; mod find_disk; -/// Command evaluation context -pub(crate) struct Context<'a> { - /// The document being parsed - pub(crate) document: &'a NamedSource>, - - /// The node being parsed - pub(crate) node: &'a KdlNode, -} - /// A command #[derive(Debug)] pub enum Command { CreatePartition, - CreatePartitionTable, + CreatePartitionTable(Box), FindDisk, } @@ -33,8 +21,8 @@ type CommandExec = for<'a> fn(Context<'a>) -> Result; /// Map of command names to functions static COMMANDS: phf::Map<&'static str, CommandExec> = phf::phf_map! { - "find-disk" => find_disk::parse, - "create-partition" => create_partition::parse, + //"find-disk" => find_disk::parse, + //"create-partition" => create_partition::parse, "create-partition-table" => create_partition_table::parse, }; @@ -42,10 +30,8 @@ static COMMANDS: phf::Map<&'static str, CommandExec> = phf::phf_map! { pub(crate) fn parse_command(context: Context<'_>) -> Result { let name = context.node.name().value(); let func = COMMANDS.get(name).ok_or_else(|| crate::UnsupportedNode { - src: context.document.clone(), at: context.node.span(), - id: name.to_string(), - advice: None, + name: name.into(), })?; func(context) diff --git a/crates/provisioning/src/commands/create_partition.rs b/crates/provisioning/src/commands/create_partition.rs index 6c2a989..e87419e 100644 --- a/crates/provisioning/src/commands/create_partition.rs +++ b/crates/provisioning/src/commands/create_partition.rs @@ -2,7 +2,8 @@ // // SPDX-License-Identifier: MPL-2.0 -use super::{Command, Context}; +use super::Command; +use crate::Context; /// Generate a command to create a partition pub(crate) fn parse(_context: Context<'_>) -> Result { diff --git a/crates/provisioning/src/commands/create_partition_table.rs b/crates/provisioning/src/commands/create_partition_table.rs index 862a0c7..6e3561a 100644 --- a/crates/provisioning/src/commands/create_partition_table.rs +++ b/crates/provisioning/src/commands/create_partition_table.rs @@ -2,9 +2,20 @@ // // SPDX-License-Identifier: MPL-2.0 -use super::{Command, Context}; +use crate::Context; +use crate::{get_kdl_property, FromKdlProperty, PartitionTableType}; + +/// Command to create a partition table +#[derive(Debug)] +pub struct Command { + /// The type of partition table to create + pub table_type: PartitionTableType, +} /// Generate a command to create a partition table -pub(crate) fn parse(_context: Context<'_>) -> Result { - unimplemented!("Command not implemented"); +pub(crate) fn parse(context: Context<'_>) -> Result { + let kind = get_kdl_property(context.node, "type")?; + let table_type = PartitionTableType::from_kdl_property(kind)?; + + Ok(super::Command::CreatePartitionTable(Box::new(Command { table_type }))) } diff --git a/crates/provisioning/src/commands/find_disk.rs b/crates/provisioning/src/commands/find_disk.rs index ad07893..3b2896a 100644 --- a/crates/provisioning/src/commands/find_disk.rs +++ b/crates/provisioning/src/commands/find_disk.rs @@ -2,7 +2,8 @@ // // SPDX-License-Identifier: MPL-2.0 -use super::{Command, Context}; +use super::Command; +use crate::Context; /// Generate a command to find a disk pub(crate) fn parse(_context: Context<'_>) -> Result { diff --git a/crates/provisioning/src/errors.rs b/crates/provisioning/src/errors.rs index 15baeb0..c031287 100644 --- a/crates/provisioning/src/errors.rs +++ b/crates/provisioning/src/errors.rs @@ -7,8 +7,6 @@ use std::{io, sync::Arc}; use miette::{Diagnostic, NamedSource, SourceSpan}; use thiserror::Error; -use crate::KdlType; - /// Error type for the provisioning crate #[derive(Diagnostic, Debug, Error)] pub enum Error { @@ -22,6 +20,9 @@ pub enum Error { #[error("unknown type")] UnknownType, + #[error("unknown variant")] + UnknownVariant, + #[diagnostic(transparent)] #[error(transparent)] InvalidType(#[from] InvalidType), @@ -36,14 +37,16 @@ pub enum Error { #[diagnostic(transparent)] #[error(transparent)] - ParseError(#[from] ParseError), + UnsupportedValue(#[from] UnsupportedValue), } /// Merged error for parsing failures /// Returns a list of diagnostics for the user #[derive(Debug, Diagnostic, Error)] #[error("failed to parse KDL")] +#[diagnostic(severity(error))] pub struct ParseError { + #[source_code] pub src: NamedSource>, #[related] pub diagnostics: Vec, @@ -51,54 +54,45 @@ pub struct ParseError { /// Error for invalid types #[derive(Debug, Diagnostic, Error)] -#[error("property {id} should be {expected_type}, not {found_type}")] +#[error("invalid type")] #[diagnostic(severity(error))] pub struct InvalidType { - #[source_code] - pub src: NamedSource>, - - #[label("here")] + #[label] pub at: SourceSpan, - - #[help] - pub advice: Option, - - pub id: &'static str, - pub expected_type: KdlType, - pub found_type: KdlType, } /// Error for missing mandatory properties #[derive(Debug, Diagnostic, Error)] -#[error("{name} is missing mandatory property: {id}")] +#[error("missing property: {id}")] #[diagnostic(severity(error))] pub struct MissingProperty { - #[source_code] - pub src: NamedSource>, - - #[label("here")] + #[label] pub at: SourceSpan, - // The name of the node - pub name: String, - - // The name of the missing property pub id: &'static str, + + #[help] + pub advice: Option, } /// Error for unsupported node types #[derive(Debug, Diagnostic, Error)] -#[error("unsupported node: {id}")] +#[error("unsupported node: {name}")] #[diagnostic(severity(warning))] pub struct UnsupportedNode { - #[source_code] - pub src: NamedSource>, - - #[label("here")] + #[label] pub at: SourceSpan, - // The name of the node - pub id: String, + pub name: String, +} + +/// Error for unsupported values +#[derive(Debug, Diagnostic, Error)] +#[error("unsupported value")] +#[diagnostic(severity(error))] +pub struct UnsupportedValue { + #[label] + pub at: SourceSpan, #[help] pub advice: Option, diff --git a/crates/provisioning/src/helpers.rs b/crates/provisioning/src/helpers.rs index fd53231..0ef0b0d 100644 --- a/crates/provisioning/src/helpers.rs +++ b/crates/provisioning/src/helpers.rs @@ -2,35 +2,30 @@ // // SPDX-License-Identifier: MPL-2.0 -use std::sync::Arc; +use kdl::{KdlEntry, KdlNode}; -use kdl::KdlNode; -use miette::NamedSource; +use crate::{Error, InvalidType, MissingProperty}; -use crate::{Error, InvalidType, KdlType, MissingProperty}; - -// Get a string property from a node -pub(crate) fn get_property_str( - ns: &NamedSource>, - node: &KdlNode, - name: &'static str, -) -> Result { +// Get a property from a node +pub(crate) fn get_kdl_property<'a>(node: &'a KdlNode, name: &'static str) -> Result<&'a KdlEntry, Error> { let entry = node.entry(name).ok_or_else(|| MissingProperty { - name: node.name().to_string(), - src: ns.clone(), at: node.span(), id: name, + advice: Some(format!("add `{name}=...` to bind the property")), })?; - let value = entry.value(); - let kind = KdlType::for_value(value)?; - let value = entry.value().as_string().ok_or(InvalidType { - src: ns.clone(), - at: entry.span(), - id: name, - expected_type: KdlType::String, - found_type: kind, - advice: Some("try using a quoted string".to_string()), - })?; + Ok(entry) +} + +// Get a string property from a value +pub(crate) fn kdl_value_to_string(entry: &kdl::KdlEntry) -> Result { + let value = entry.value().as_string().ok_or(InvalidType { at: entry.span() })?; + + Ok(value.to_owned()) +} + +// Get a string property from a node +pub(crate) fn get_property_str(node: &KdlNode, name: &'static str) -> Result { + let value = get_kdl_property(node, name).and_then(kdl_value_to_string)?; Ok(value.to_owned()) } diff --git a/crates/provisioning/src/lib.rs b/crates/provisioning/src/lib.rs index a0ca64b..2f2f39c 100644 --- a/crates/provisioning/src/lib.rs +++ b/crates/provisioning/src/lib.rs @@ -15,11 +15,17 @@ mod helpers; use helpers::*; mod types; -use types::*; +pub use types::*; mod commands; use commands::*; +/// Command evaluation context +pub struct Context<'a> { + /// The node being parsed + pub(crate) node: &'a KdlNode, +} + /// A strategy definition #[derive(Debug)] pub struct StrategyDefinition { @@ -37,67 +43,107 @@ pub struct StrategyDefinition { } /// A parser for provisioning strategies +#[derive(Debug)] pub struct Parser { pub strategies: Vec, } impl Parser { /// Create a new parser from a file path - pub fn new_for_path

(file: P) -> Result + pub fn new_for_path

(file: P) -> Result where P: AsRef, { let file = file.as_ref(); let name = file.to_string_lossy(); - let txt = fs::read_to_string(file)?; + let txt = fs::read_to_string(file).map_err(|e| ParseError { + src: NamedSource::new(&name, Arc::new("".to_string())), + diagnostics: vec![e.into()], + })?; Self::new(name.to_string(), txt) } /// Create a new parser from a string - pub fn new(name: String, contents: String) -> Result { + pub fn new(name: String, contents: String) -> Result { let source = Arc::new(contents.to_string()); let ns = NamedSource::new(name, source).with_language("KDL"); - let d = KdlDocument::parse_v2(ns.inner())?; + let mut errors = vec![]; + + // Parse the document and collect any errors + let d = KdlDocument::parse_v2(ns.inner()).map_err(|e| ParseError { + src: ns.clone(), + diagnostics: vec![e.into()], + })?; let mut strategies = vec![]; for node in d.nodes() { match node.name().value() { - "strategy" => { - strategies.push(Self::parse_strategy(&ns, node)?); - } - what => { - return Err(UnsupportedNode { - src: ns.clone(), - at: node.span(), - id: what.to_string(), - advice: Some("only 'strategy' nodes are supported".to_owned()), - })?; + "strategy" => match Self::parse_strategy(node) { + Ok(strategy) => strategies.push(strategy), + Err(e) => errors.extend(e), + }, + _ => { + errors.push( + UnsupportedNode { + at: node.span(), + name: node.name().to_string(), + } + .into(), + ); } } } + if !errors.is_empty() { + return Err(ParseError { + src: ns, + diagnostics: errors, + }); + } + Ok(Self { strategies }) } // Parse a strategy node - fn parse_strategy(ns: &NamedSource>, node: &KdlNode) -> Result { - let name = get_property_str(ns, node, "name")?; - let summary = get_property_str(ns, node, "summary")?; + fn parse_strategy(node: &KdlNode) -> Result> { + let mut errors = vec![]; + let name = match get_property_str(node, "name") { + Ok(name) => name, + Err(e) => { + errors.push(e); + Default::default() + } + }; + let summary = match get_property_str(node, "summary") { + Ok(summary) => summary, + Err(e) => { + errors.push(e); + Default::default() + } + }; let inherits = if node.entry("inherits").is_some() { - Some(get_property_str(ns, node, "inherits")?) + match get_property_str(node, "inherits") { + Ok(inherits) => Some(inherits), + Err(e) => { + errors.push(e); + None + } + } } else { None }; // Collect all failures in this strategy - let (commands, errors): (Vec<_>, Vec<_>) = + let (commands, child_errors): (Vec<_>, Vec<_>) = node.iter_children() - .partition_map(|node| match parse_command(Context { document: ns, node }) { + .partition_map(|node| match parse_command(Context { node }) { Ok(cmd) => Either::Left(cmd), Err(e) => Either::Right(e), }); + errors.extend(child_errors); + let fatal_errors = errors .iter() .filter(|e| matches!(e.severity().unwrap_or(Severity::Error), Severity::Error)); @@ -106,10 +152,7 @@ impl Parser { // TODO: Add an error sink to allow bubbling up of warnings/diagnostics // for tooling integration if fatal_errors.clone().next().is_some() { - return Err(ParseError { - src: ns.clone(), - diagnostics: errors, - })?; + return Err(errors); } let strategy = StrategyDefinition { @@ -128,8 +171,10 @@ mod tests { use crate::Parser; #[test] - #[should_panic] - fn test_basic() { - let _p = Parser::new_for_path("tests/use_whole_disk.kdl").unwrap(); + //#[should_panic] + fn test_basic() -> miette::Result<()> { + let _p = Parser::new_for_path("tests/use_whole_disk.kdl")?; + eprintln!("p: {_p:?}"); + Ok(()) } }