//! Parse the query language for the CLI and other tools //! //! The `cargo-ws2` query language (`ws2ql`) allows users to specify a //! set of inputs, and an operation to execute on it. //! //! ## Basic rules //! //! * Inside `[]` are sets (meaning items de-dup) that don't require //! reparations //! * IF `[]` contains a `/` anywhere _but_ the beginning AND end, //! query becomes a path glob //! * IF `[]` contains `/` at start and end, query becomes a regex //! * An operation is parsed in the order of the fields in it's struct //! (so publish order is `type mod devel`, etc) //! * Inside `{}` you can create dependency maps with arrows. //! * `{ foo < }` represents all crates that depend on `foo` //! * `{ foo < bar < }` represents all crates that depend on `foo` AND `bar` //! * `{ foo < bar |< }` represents all crates that depend on `foo` but NOT on `bar` //! //! ... etc. use crate::models::{CrateId, DepGraph}; mod executor; /// A query on the dependency graph #[derive(Debug)] pub enum Query { /// Simple set of names `[ a b c ]` Set(Vec), /// Simple path glob query `./foo/*` Path(String), /// Regular expression done on paths in tree `/foo\$/` Regex(String), /// A dependency graph query (see documentation for rules) DepGraph(Vec), } impl Query { /// Parse an argument iterator (provided by `std::env::args`) pub fn parse<'a>(line: impl Iterator) -> (Self, Vec) { let parser = QueryParser::new(line.skip(1).collect()); match parser.run() { Some((q, rest)) => (q, rest), None => { eprintln!("Failed to parse query!"); std::process::exit(2); } } } /// Execute a query with a dependency graph pub fn execute(self, g: &DepGraph) -> Vec { match self { Self::Set(ref crates) => executor::set(crates, g), Self::DepGraph(deps) => executor::deps(deps, g), _ => todo!(), } } } /// Express a dependency constraint #[derive(Debug)] pub struct DepConstraint { pub _crate: String, pub constraint: Constraint, } /// All constraints can be negated #[derive(Debug)] pub enum Constraint { Initial(bool), And(bool), Or, } struct QueryParser { line: Vec, } impl QueryParser { fn new(line: Vec) -> Self { Self { line } } /// Run the parser until it yields an error or finished query fn run(mut self) -> Option<(Query, Vec)> { let line: Vec = std::mem::replace(&mut self.line, vec![]) .into_iter() .fold(vec![], |mut vec, line| { line.split(" ").for_each(|seg| { vec.push(seg.into()); }); vec }); // This is here to get around infinately sized enums #[derive(Debug)] enum Brace { Block, Curly, } // Track the state of the query braces #[derive(Debug)] enum BraceState { Missing, BlockOpen, CurlyOpen, Done(Brace), } use {Brace::*, BraceState::*}; let mut bs = Missing; let mut buf = vec![]; let mut cbuf = String::new(); // Store a single crate name as a buffer let mut skip = 1; // Parse the crate set for elem in &line { match (&bs, elem.as_str()) { // The segment starts with a [ (Missing, e) if e.starts_with("[") => { bs = BlockOpen; // Handle the case where we need to grab the crate from this segment if let Some(_crate) = e.strip_prefix("[") { if _crate != "" { buf.push(_crate.to_string()); } } } // The segment starts with a { (Missing, e) if e.starts_with("{") => { bs = CurlyOpen; if let Some(_crate) = e.strip_prefix("{") { if _crate != "" { cbuf = _crate.into(); } } } (BlockOpen, e) if e.ends_with("]") => { if let Some(_crate) = e.strip_suffix("]") { if _crate != "" { buf.push(_crate.to_string()); } } bs = Done(Block); break; } (BlockOpen, _crate) => buf.push(_crate.to_string()), (CurlyOpen, e) if e.ends_with("}") && cbuf == "" => { bs = Done(Curly); break; } (CurlyOpen, e) if e.ends_with("}") && cbuf != "" => { eprintln!("[ERROR]: Out of place `}}`, expected operand!"); std::process::exit(2); } (CurlyOpen, op) if cbuf != "" => { buf.push(format!("{} $ {}", cbuf, op)); cbuf = "".into(); } (CurlyOpen, _crate) => { cbuf = _crate.into(); } (_, _) => {} } skip += 1; } let rest = line.into_iter().skip(skip).collect(); match bs { Done(Block) => Some((Query::Set(buf), rest)), Done(Curly) => { let mut init = true; let c: Vec<_> = buf .into_iter() .map(|val| { let mut s: Vec<_> = val.split("$").collect(); let _crate = s.remove(0).trim().to_string(); let c = s.remove(0).trim().to_string(); DepConstraint { _crate, constraint: match c.as_str() { "<" if init => { init = false; Constraint::Initial(true) } "!<" if init => { init = false; Constraint::Initial(false) } "&<" => Constraint::And(true), "!&<" => Constraint::And(false), "|<" => Constraint::Or, c => { eprintln!("[ERROR]: Invalid constraint: `{}`", c); std::process::exit(2); } }, } }) .collect(); if c.len() < 1 { eprintln!("[ERROR]: Provided an empty graph set: {{ }}. At least one dependency required!"); std::process::exit(2); } Some((Query::DepGraph(c), rest)) } _ if rest.len() < 1 => crate::cli::render_help(2), _line => { eprintln!("[ERROR]: You reached some unimplemented code in cargo-ws2! \ This might be a bug, or it might be a missing feature. Contact me with your query, \ and we can see which one it is :)"); std::process::exit(2); } } } } #[test] fn block_parser_spaced() { let _ = QueryParser::new( vec!["", "[", "foo", "bar", "baz", "]", "publish", "minor"] .into_iter() .map(Into::into) .collect(), ) .run(); } #[test] fn block_parser_offset_front() { let _ = QueryParser::new( vec!["my-program", "[foo", "bar", "baz", "]", "publish", "minor"] .into_iter() .map(Into::into) .collect(), ) .run(); } #[test] fn block_parser_offset_back() { let _ = QueryParser::new( vec!["my-program", "[", "foo", "bar", "baz]", "publish", "minor"] .into_iter() .map(Into::into) .collect(), ) .run(); } #[test] fn block_parser_offset_both() { let _ = QueryParser::new( vec!["my-program", "[foo", "bar", "baz]", "publish", "minor"] .into_iter() .map(Into::into) .collect(), ) .run(); } #[test] fn curly_parser_simple() { let _ = QueryParser::new( vec!["my-program", "{ foo < bar &< }", "print"] .into_iter() .map(Into::into) .collect(), ) .run(); }