diff options
Diffstat (limited to 'development/tools/cargo-workspace2/src')
21 files changed, 1725 insertions, 0 deletions
diff --git a/development/tools/cargo-workspace2/src/bin/cargo-ws2.rs b/development/tools/cargo-workspace2/src/bin/cargo-ws2.rs new file mode 100644 index 000000000000..8de5bc60f6a4 --- /dev/null +++ b/development/tools/cargo-workspace2/src/bin/cargo-ws2.rs @@ -0,0 +1,42 @@ +// extern crate cargo_ws_release; +// #[macro_use] +// extern crate clap; +// extern crate toml; +// extern crate toml_edit; + +// use cargo_ws_release::data_models::level::*; +// use cargo_ws_release::do_batch_release; +// use clap::{App, Arg}; +// use std::fs::File; +// use std::process; + +use cargo_workspace2 as ws2; + +use std::env::{args, current_dir}; +use ws2::cli::{self, CmdSet}; +use ws2::models::{CargoWorkspace, CrateId, Workspace}; +use ws2::{ops::Op, query::Query}; + +fn main() { + let CmdSet { debug, line } = cli::parse_env_args(); + let (q, rest) = Query::parse(line.into_iter()); + + let path = current_dir().unwrap(); + let ws = Workspace::process(match CargoWorkspace::open(path) { + Ok(c) => c, + Err(e) => { + eprintln!("An error occured: {}", e); + std::process::exit(1); + } + }); + + let set = ws.query(q); + let op = Op::parse(rest); + + mutate(ws, op, set); +} + +/// Consume a workspace to mutate it +fn mutate(mut ws: Workspace, op: Op, set: Vec<CrateId>) { + ws.execute(op, set); +} diff --git a/development/tools/cargo-workspace2/src/cargo/deps.rs b/development/tools/cargo-workspace2/src/cargo/deps.rs new file mode 100644 index 000000000000..d40170ccd841 --- /dev/null +++ b/development/tools/cargo-workspace2/src/cargo/deps.rs @@ -0,0 +1,75 @@ +use super::v_to_s; +use toml_edit::InlineTable; + +/// An intra-workspace dependency +/// +/// In a Cargo.toml file these are expressed as paths, and sometimes +/// also as versions. +/// +/// ```toml +/// [dependencies] +/// my-other-crate = { version = "0.1.0", path = "../other-crate" } +/// ``` +#[derive(Debug, Clone)] +pub struct Dependency { + pub name: String, + pub alias: Option<String>, + pub version: Option<String>, + pub path: Option<String>, +} + +impl Dependency { + pub(crate) fn parse(n: String, t: &InlineTable) -> Option<Self> { + let v = t.get("version").map(|s| v_to_s(s)); + let p = t.get("path").map(|s| v_to_s(s)); + + // If a `package` key is present, set it as the name, and set + // the `n` as the alias. When we look for keys later, the + // alias has precedence over the actual name, but this way + // `name` is always the actual crate name which is important + // for dependency resolution. + let (alias, name) = match t + .get("package") + .map(|s| v_to_s(s).replace("\"", "").trim().to_string()) + { + Some(alias) => (Some(n), alias), + None => (None, n), + }; + + match (v, p) { + (version @ Some(_), path @ Some(_)) => Some(Self { + name, + alias, + version, + path, + }), + (version @ Some(_), None) => Some(Self { + name, + alias, + version, + path: None, + }), + (None, path @ Some(_)) => Some(Self { + name, + alias, + version: None, + path, + }), + (None, None) => None, + } + } + + /// Check if the dependency has a provided version + pub fn has_version(&self) -> bool { + self.version.is_some() + } + + /// Check if the dependency has a provided path + pub fn has_path(&self) -> bool { + self.path.is_some() + } + + pub fn alias(&self) -> Option<String> { + self.alias.clone() + } +} diff --git a/development/tools/cargo-workspace2/src/cargo/error.rs b/development/tools/cargo-workspace2/src/cargo/error.rs new file mode 100644 index 000000000000..cf4b7e3d1671 --- /dev/null +++ b/development/tools/cargo-workspace2/src/cargo/error.rs @@ -0,0 +1,42 @@ +use std::{fmt, io}; +use toml_edit::TomlError; + +/// Errors occured while interacting with Cargo.toml files +#[derive(Debug)] +pub enum CargoError { + /// Failed to read or write a file + Io, + /// Error parsing Cargo.toml file + Parsing, + /// Provided Cargo.toml was no workspace + NoWorkspace, + /// Provided Cargo.toml had no dependencies + NoDependencies, +} + +impl fmt::Display for CargoError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{}", + match self { + Self::Io => "General I/O error", + Self::Parsing => "Parsing error", + Self::NoWorkspace => "Selected crate root is not a workspace!", + Self::NoDependencies => "No dependencies found!", + } + ) + } +} + +impl From<io::Error> for CargoError { + fn from(_: io::Error) -> Self { + Self::Io + } +} + +impl From<TomlError> for CargoError { + fn from(_: TomlError) -> Self { + Self::Parsing + } +} diff --git a/development/tools/cargo-workspace2/src/cargo/gen.rs b/development/tools/cargo-workspace2/src/cargo/gen.rs new file mode 100644 index 000000000000..4ba891e15835 --- /dev/null +++ b/development/tools/cargo-workspace2/src/cargo/gen.rs @@ -0,0 +1,34 @@ +//! Generate toml data + +use super::{CargoError, Result}; +use std::{fs::File, io::Write, path::PathBuf}; +use toml_edit::{value, Document, Item, Value}; + +/// Sync a document back into it's Cargo.toml +pub(crate) fn sync(doc: &mut Document, path: PathBuf) -> Result<()> { + if !path.exists() { + return Err(CargoError::Io); + } + + let mut f = File::create(path)?; + f.write_all(doc.to_string().as_bytes())?; + Ok(()) +} + +/// Takes a mutable document, dependency alias or name, and version +pub(crate) fn update_dependency(doc: &mut Document, dep: &String, ver: &String) { + match doc.as_table_mut().entry("dependencies") { + Item::Table(ref mut t) => match t.entry(dep.as_str()) { + Item::Value(Value::InlineTable(ref mut t)) => { + if let Some(v) = t.get_mut("version") { + *v = Value::from(ver.clone()); + } + return; + } + _ => {} + }, + _ => {} + } + + // eprintln!("Invalid dependency format!"); +} diff --git a/development/tools/cargo-workspace2/src/cargo/mod.rs b/development/tools/cargo-workspace2/src/cargo/mod.rs new file mode 100644 index 000000000000..deb54563d6ba --- /dev/null +++ b/development/tools/cargo-workspace2/src/cargo/mod.rs @@ -0,0 +1,25 @@ +//! A set of models directly related to `Cargo.toml` files + +mod deps; +pub use deps::Dependency; + +mod error; +pub use error::CargoError; + +mod parser; +pub(crate) use parser::{get_members, parse_dependencies, parse_root_toml, parse_toml}; + +mod gen; +pub(crate) use gen::{sync, update_dependency}; + +pub(crate) type Result<T> = std::result::Result<T, CargoError>; + +use toml_edit::Value; + +/// Turn a toml Value into a String, panic if it fails +pub(self) fn v_to_s(v: &Value) -> String { + match v { + Value::String(s) => s.to_string(), + _ => unreachable!(), + } +} diff --git a/development/tools/cargo-workspace2/src/cargo/parser.rs b/development/tools/cargo-workspace2/src/cargo/parser.rs new file mode 100644 index 000000000000..c3e67e2785fb --- /dev/null +++ b/development/tools/cargo-workspace2/src/cargo/parser.rs @@ -0,0 +1,69 @@ +use crate::cargo::{CargoError, Dependency, Result}; +use std::{fs::File, io::Read, path::PathBuf}; +use toml_edit::{Document, Item, Value}; + +/// Parse the root entry of a cargo workspace into a document +pub(crate) fn parse_root_toml(p: PathBuf) -> Result<Document> { + let mut buf = String::new(); + File::open(p)?.read_to_string(&mut buf)?; + Ok(buf.parse::<Document>()?) +} + +/// For a root-config, get the paths of member crates +pub(crate) fn get_members(doc: &Document) -> Result<Vec<String>> { + match &doc["workspace"]["members"] { + Item::Value(Value::Array(arr)) => Ok(arr + .iter() + .filter_map(|val| match val { + Value::String(name) => Some(format!("{}", name)), + val => { + eprintln!("Ignoring value `{:?}` in List of strings", val); + None + } + }) + .map(|s| s.replace("\"", "").trim().into()) + .map(|s: String| { + if s.trim().starts_with("#") { + s.split("\n").map(ToString::to_string).collect() + } else { + vec![s] + } + }) + // Holy shit this crate is useless! Why would it not just + // strip the commented lines, instead of doing some + // bullshit like this... + .fold(vec![], |mut vec, ovec| { + ovec.into_iter().for_each(|i| { + if !i.trim().starts_with("#") && i != "" { + vec.push(i.trim().to_string()); + } + }); + vec + })), + _ => Err(CargoError::NoWorkspace), + } +} + +/// Parse a member crate Cargo.toml file +pub(crate) fn parse_toml(p: PathBuf) -> Result<Document> { + let mut buf = String::new(); + File::open(p)?.read_to_string(&mut buf)?; + Ok(buf.parse::<Document>()?) +} + +/// Parse a member crate set of dependencies +/// +/// When a crate is not a table, it can't be a workspace member +/// dependency, so is skipped. +pub(crate) fn parse_dependencies(doc: &Document) -> Vec<Dependency> { + match &doc["dependencies"] { + Item::Table(ref t) => t + .iter() + .filter_map(|(n, v)| match v { + Item::Value(Value::InlineTable(ref t)) => Dependency::parse(n.to_string(), t), + _ => None, + }) + .collect(), + _ => vec![], + } +} diff --git a/development/tools/cargo-workspace2/src/cli.rs b/development/tools/cargo-workspace2/src/cli.rs new file mode 100644 index 000000000000..afea9e224176 --- /dev/null +++ b/development/tools/cargo-workspace2/src/cli.rs @@ -0,0 +1,91 @@ +//! Helpers and utilities to parse the CLI input + +use std::env; + +pub struct CmdSet { + pub debug: bool, + pub line: Vec<String>, +} + +fn get_nth(idx: usize) -> Option<String> { + env::args().nth(idx).as_ref().map(|s| s.to_owned()).clone() +} + +/// Call this instead of env::args() - it handles !commands too +pub fn parse_env_args() -> CmdSet { + let mut line: Vec<_> = env::args().collect(); + let mut debug = false; + + if line.len() == 1 { + render_help(2); + } + + find_bang_commands().into_iter().for_each(|(idx, bang)| { + let maybe_next = line.iter().nth(idx + 1).as_ref().map(|s| s.to_owned()); + + match bang.trim() { + "!help" => match maybe_next { + None => render_help(0), + Some(cmd) => crate::ops::render_help(cmd.to_string()), + }, + "!version" => render_version(), + "!debug" => { + debug = true; + line.remove(idx); + } + bang => { + if debug { + eprintln!("Unrecognised bang command: {}", bang); + } + + line.remove(idx); + } + } + }); + + CmdSet { line, debug } +} + +/// Get env::args() and look for any string with a `!` in front of it. +/// If it's not in the set of known bang commands, ignore it. +fn find_bang_commands() -> Vec<(usize, String)> { + env::args() + .enumerate() + .filter_map(|(idx, s)| { + if s.starts_with("!") { + Some((idx, s)) + } else { + None + } + }) + .collect() +} + +pub(crate) fn render_help(code: i32) -> ! { + eprintln!("cargo-ws v{}", env!("CARGO_PKG_VERSION")); + eprintln!("An expression language and command executor for cargo workspaces."); + eprintln!("Usage: cargo ws2 <QUERY LANG> <COMMAND> [COMMAND ARGS] [!debug]"); + eprintln!(" cargo ws2 [!version | !debug]"); + eprintln!(" cargo ws2 !help [COMMAND]"); + eprintln!(""); + + crate::ops::list_commands(); + eprintln!(""); + + eprintln!("Query language examples:\n"); + eprintln!(" - [ foo bar ]: select crates foo and bar"); + eprintln!(" - {{ foo < }}: select crates that depend on foo"); + eprintln!(" - {{ foo < bar &< }}: select crates that depend on foo AND bar"); + eprintln!(" - {{ foo < bar |< }}: select crates that depend on foo OR bar"); + eprintln!("\nIf you have any questions, or find bugs, please e-mail me: kookie@spacekookie.de"); + std::process::exit(code) +} + +fn render_version() -> ! { + eprintln!("cargo-ws v{}", env!("CARGO_PKG_VERSION")); + eprintln!( + "Build with: {}", + include_str!(concat!(env!("OUT_DIR"), "/rustc.version")) + ); + std::process::exit(0) +} diff --git a/development/tools/cargo-workspace2/src/lib.rs b/development/tools/cargo-workspace2/src/lib.rs new file mode 100644 index 000000000000..689c3601cd55 --- /dev/null +++ b/development/tools/cargo-workspace2/src/lib.rs @@ -0,0 +1,64 @@ +//! cargo-workspace2 is a library to help manage cargo workspaces +//! +//! Out of the box the `cargo` workspace experience leaves a lot to be +//! desired. Managing a repo with many crates in it can get out of +//! hand quickly. Moreover, other tools that try to solve these +//! issues often pick _one_ particular usecase of cargo workspaces, +//! and enforce very strict rules on how to use them. +//! +//! This library aims to solve some of the issues of dealing with +//! workspaces in a way that doesn't enforce a usage mode for the +//! user. +//! +//! This package also publishes a binary (cargo ws2), which is +//! recommended for most users. In case the binary handles a use-case +//! you have in a way that you don't like, this library aims to +//! provide a fallback so that you don't have to re-implement +//! everything from scratch. +//! +//! ## Using this library +//! +//! Parsing happens in stages. First you need to use the +//! [`cargo`](./cargo/index.html) module to parse the actual +//! `Cargo.toml` files. After that you can use the cargo models in +//! [`models`](models/index.html) to further process dependencies, and +//! create a [`DepGraph`](models/struct.DepGraph.html) to resolve queries and make changes. + +pub mod cargo; +pub mod models; +pub mod ops; +pub mod query; + +#[doc(hidden)] +pub mod cli; + +// extern crate toml; +// extern crate toml_edit; + +// pub use data_models::graph; +// use data_models::level::Level; +// use graph::DepGraph; +// use std::fs::File; +// pub use utilities::cargo_utils; +// pub use utilities::utils; + +// pub mod data_models; +// pub mod utilities; + +// pub fn do_batch_release(f: File, lvl: &Level) -> DepGraph { +// let members = cargo_utils::get_members(f); +// let configs = cargo_utils::batch_load_configs(&members); + +// let v = configs +// .iter() +// .map(|c| cargo_utils::parse_config(c, &members)) +// .fold(DepGraph::new(), |mut graph, (name, deps)| { +// graph.add_node(name.clone()); + +// deps.iter() +// .fold(graph, |graph, dep| graph.add_dependency(&name, dep)) +// }); + +// println!("{:#?}", v); +// v +// } diff --git a/development/tools/cargo-workspace2/src/models/_crate.rs b/development/tools/cargo-workspace2/src/models/_crate.rs new file mode 100644 index 000000000000..68d2baad2bad --- /dev/null +++ b/development/tools/cargo-workspace2/src/models/_crate.rs @@ -0,0 +1,112 @@ +use crate::models::{CargoCrate, CrateId, DepGraph}; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::{ + cmp::{self, Eq, Ord, PartialEq, PartialOrd}, + collections::BTreeSet, + path::PathBuf, +}; + +static ID_CTR: AtomicUsize = AtomicUsize::new(0); + +/// A crate in a cargo workspace +/// +/// Has a name, path (stored as the offset of the root), and set of +/// dependencies inside the workspace. To get the dependents of this +/// crate, query the dependency graph with the set of other crate IDs. +#[derive(Clone, Debug)] +pub struct Crate { + /// Numeric Id of this crate + pub id: CrateId, + /// Package name, not the folder name + pub name: String, + /// Path offset of the workspace root + pub cc: CargoCrate, + /// List of dependencies this crate has inside this workspace + pub dependencies: BTreeSet<CrateId>, +} + +impl PartialEq for Crate { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for Crate {} + +impl Ord for Crate { + fn cmp(&self, other: &Self) -> cmp::Ordering { + self.id.cmp(&other.id) + } +} + +impl PartialOrd for Crate { + fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> { + Some(self.cmp(other)) + } +} + +/// Increment the monotonicly increasing Id +fn incr_id() -> usize { + ID_CTR.fetch_add(1, Ordering::Relaxed) +} + +impl Crate { + pub fn new(cc: CargoCrate) -> Self { + Self { + id: incr_id(), + name: cc.name(), + cc, + dependencies: BTreeSet::default(), + } + } + + /// Call this function once all crates have been loaded into scope + pub fn process(&mut self, g: &DepGraph) { + let deps: Vec<_> = self + .cc + .dependencies + .iter() + .filter_map(|d| g.find_crate(&d.name)) + .collect(); + + deps.into_iter().for_each(|cid| self.add_dependency(cid)); + } + + /// Get the crate name + pub fn name(&self) -> &String { + &self.name + } + + /// Get the crate path + pub fn path(&self) -> &PathBuf { + &self.cc.path + } + + /// Get the current version + pub fn version(&self) -> String { + self.cc.version() + } + + /// Add a dependency of this crate + pub fn add_dependency(&mut self, id: CrateId) { + self.dependencies.insert(id); + } + + /// Check if this crate has a particular dependency + pub fn has_dependency(&self, id: CrateId) -> bool { + self.dependencies.contains(&id) + } + + pub fn change_dependency(&mut self, dep: &String, new_ver: &String) { + self.cc.change_dep(dep, new_ver); + } + + /// Publish a new version of this crate + pub fn publish(&mut self, new_version: String) { + self.cc.set_version(new_version); + } + + pub fn sync(&mut self) { + self.cc.sync(); + } +} diff --git a/development/tools/cargo-workspace2/src/models/cargo.rs b/development/tools/cargo-workspace2/src/models/cargo.rs new file mode 100644 index 000000000000..b020e82e418d --- /dev/null +++ b/development/tools/cargo-workspace2/src/models/cargo.rs @@ -0,0 +1,132 @@ +use crate::cargo::{self, Dependency, Result}; +use std::{fmt, path::PathBuf}; +use toml_edit::{value, Document, Item, Value}; + +/// Initial representation of a crate, before being parsed +#[derive(Clone)] +pub struct CargoCrate { + pub doc: Document, + pub path: PathBuf, + pub dependencies: Vec<Dependency>, +} + +impl fmt::Debug for CargoCrate { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.path.as_path().display()) + } +} + +impl CargoCrate { + /// Get the crate name from the inner document + pub fn name(&self) -> String { + match &self.doc["package"]["name"] { + Item::Value(Value::String(ref name)) => { + name.to_string().replace("\"", "").as_str().trim().into() + } + _ => panic!(format!("Invalid Cargo.toml: {:?}", self.path)), + } + } + + /// Get the current version + pub fn version(&self) -> String { + match &self.doc["package"]["version"] { + Item::Value(Value::String(ref name)) => { + name.to_string().replace("\"", "").as_str().trim().into() + } + _ => panic!(format!("Invalid Cargo.toml: {:?}", self.path)), + } + } + + /// Find a cargo dependency by name + pub fn dep_by_name(&self, name: &String) -> &Dependency { + self.dependencies + .iter() + .find(|c| &c.name == name) + .as_ref() + .unwrap() + } + + pub fn change_dep(&mut self, dep: &String, ver: &String) { + let dep = self + .dep_by_name(dep) + .alias() + .unwrap_or(dep.to_string()) + .clone(); + cargo::update_dependency(&mut self.doc, &dep, ver); + } + + pub fn all_deps_mut(&mut self) -> Vec<&mut Dependency> { + self.dependencies.iter_mut().collect() + } + + /// Check if this crate depends on a specific version of another + pub fn has_version(&self, name: &String) -> bool { + self.dep_by_name(name).has_version() + } + + /// Check if this crate depends on a specific path of another + pub fn has_path(&self, name: &String) -> bool { + self.dep_by_name(name).has_version() + } + + /// Set a new version for this crate + pub fn set_version(&mut self, version: String) { + self.doc["package"]["version"] = value(version); + } + + /// Sync any changes made to the document to disk + pub fn sync(&mut self) { + cargo::sync(&mut self.doc, self.path.join("Cargo.toml")).unwrap(); + } +} + +/// Initial representation of the workspate, before getting parsed +pub struct CargoWorkspace { + pub root: PathBuf, + pub crates: Vec<CargoCrate>, +} + +impl CargoWorkspace { + /// Open a workspace and parse dependency graph + /// + /// Point this to the root of the workspace, do the root + /// `Cargo.toml` file. + pub fn open(p: impl Into<PathBuf>) -> Result<Self> { + let path = p.into(); + + let root_cfg = cargo::parse_root_toml(path.join("Cargo.toml"))?; + let members = cargo::get_members(&root_cfg)?; + + let m_cfg: Vec<_> = members + .into_iter() + .filter_map( + |name| match cargo::parse_toml(path.join(&name).join("Cargo.toml")) { + Ok(doc) => Some(( + PathBuf::new().join(name), + cargo::parse_dependencies(&doc), + doc, + )), + Err(e) => { + eprintln!( + "Error occured while parsing member `{}`/`Cargo.toml`: {:?}", + name, e + ); + None + } + }, + ) + .collect(); + + Ok(Self { + root: path, + crates: m_cfg + .into_iter() + .map(|(path, dependencies, doc)| CargoCrate { + path, + dependencies, + doc, + }) + .collect(), + }) + } +} diff --git a/development/tools/cargo-workspace2/src/models/graph.rs b/development/tools/cargo-workspace2/src/models/graph.rs new file mode 100644 index 000000000000..867c463fb1e3 --- /dev/null +++ b/development/tools/cargo-workspace2/src/models/graph.rs @@ -0,0 +1,78 @@ +use crate::models::{CargoCrate, Crate, CrateId}; +use std::collections::{BTreeMap, BTreeSet}; +use std::path::PathBuf; + +/// Dependency graph in a workspace +pub struct DepGraph { + /// Mapping of crates in the workspace + members: BTreeMap<CrateId, Crate>, + /// Map of crates and the members that depend on them + dependents: BTreeMap<CrateId, BTreeSet<CrateId>>, +} + +impl DepGraph { + /// Create a new, empty dependency graph + pub fn new() -> Self { + Self { + members: Default::default(), + dependents: Default::default(), + } + } + + pub fn add_crate(&mut self, cc: CargoCrate) { + let cc = Crate::new(cc); + self.members.insert(cc.id, cc); + } + + /// Cache the dependents graph for all crates + pub fn finalise(&mut self) { + let mut members = self.members.clone(); + members.iter_mut().for_each(|(_, c)| c.process(&self)); + + members.iter().for_each(|(id, _crate)| { + _crate.dependencies.iter().for_each(|dep_id| { + self.dependents.entry(*dep_id).or_default().insert(*id); + }); + }); + let _ = std::mem::replace(&mut self.members, members); + } + + /// Get a crate by ID + pub fn get_crate(&self, id: CrateId) -> &Crate { + self.members.get(&id).as_ref().unwrap() + } + + /// Get mutable access to a crate by ID + pub fn mut_crate(&mut self, id: CrateId) -> &mut Crate { + self.members.get_mut(&id).unwrap() + } + + /// Find a crate via it's name + pub fn find_crate(&self, name: &String) -> Option<CrateId> { + self.members + .iter() + .find(|(_, c)| c.name() == name) + .map(|(id, _)| *id) + } + + /// Find a crate by it's path-offset in the workspace + pub fn find_crate_by_path(&self, name: &String) -> Option<CrateId> { + self.members + .iter() + .find(|(_, c)| c.path() == &PathBuf::new().join(name)) + .map(|(id, _)| *id) + } + + /// Get a crate's dependents + pub fn get_dependents(&self, id: CrateId) -> Vec<CrateId> { + self.dependents + .get(&id) + .as_ref() + .map(|set| set.iter().cloned().collect()) + .unwrap_or(vec![]) + } + + pub fn get_all(&self) -> Vec<&Crate> { + self.members.iter().map(|(_, c)| c).collect() + } +} diff --git a/development/tools/cargo-workspace2/src/models/mod.rs b/development/tools/cargo-workspace2/src/models/mod.rs new file mode 100644 index 000000000000..759b2703f9f0 --- /dev/null +++ b/development/tools/cargo-workspace2/src/models/mod.rs @@ -0,0 +1,50 @@ +//! Collection of cargo workspace data models. +//! +//! To start parsing types, construct a `CargoWorkspace`, which you +//! can then modify with commands found in [`ops`](../ops/index.html). + +mod cargo; +pub use cargo::{CargoCrate, CargoWorkspace}; + +mod _crate; +pub use _crate::Crate; + +mod publish; +pub use publish::{MutationSet, PubMutation}; + +mod graph; +pub use graph::DepGraph; + +pub type CrateId = usize; + +use crate::{ops::Op, query::Query}; +use std::path::PathBuf; + +/// A fully parsed workspace +pub struct Workspace { + pub root: PathBuf, + dgraph: DepGraph, +} + +impl Workspace { + /// Create a parsed workspace by passing in the stage1 parse data + pub fn process(cws: CargoWorkspace) -> Self { + let CargoWorkspace { root, crates } = cws; + + let mut dgraph = DepGraph::new(); + crates.into_iter().for_each(|cc| dgraph.add_crate(cc)); + dgraph.finalise(); + + Self { root, dgraph } + } + + /// Execute a query on this workspace to find crate IDs + pub fn query(&self, q: Query) -> Vec<CrateId> { + q.execute(&self.dgraph) + } + + /// Execute an operation on a set of crates this in workspace + pub fn execute(&mut self, op: Op, target: Vec<CrateId>) { + op.execute(target, self.root.clone(), &mut self.dgraph) + } +} diff --git a/development/tools/cargo-workspace2/src/models/publish.rs b/development/tools/cargo-workspace2/src/models/publish.rs new file mode 100644 index 000000000000..11984017d49f --- /dev/null +++ b/development/tools/cargo-workspace2/src/models/publish.rs @@ -0,0 +1,20 @@ +use crate::models::{Crate, CrateId}; + +/// A publishing mutation executed on the graph +pub struct PubMutation { + _crate: CrateId, + new_version: String, +} + +impl PubMutation { + /// Createa new motation from a crate a version string + pub fn new(c: &Crate, new_version: String) -> Self { + Self { + _crate: c.id, + new_version, + } + } +} + +/// A collection of mutations performed in a batch +pub struct MutationSet {} diff --git a/development/tools/cargo-workspace2/src/ops/error.rs b/development/tools/cargo-workspace2/src/ops/error.rs new file mode 100644 index 000000000000..3dde73d954d8 --- /dev/null +++ b/development/tools/cargo-workspace2/src/ops/error.rs @@ -0,0 +1,28 @@ +use std::fmt::{self, Display, Formatter}; + +/// Special result type that wraps an OpError +pub type Result<T> = std::result::Result<T, OpError>; + +/// An error that occured while running an operation +#[derive(Debug)] +pub enum OpError { + NoSuchCrate(String), + CircularDependency(String, String), +} + +impl Display for OpError { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!( + f, + "{}", + match self { + Self::NoSuchCrate(n) => format!("No crate `{}` was not found in the workspace", n), + Self::CircularDependency(a, b) => format!( + "Crates `{}` and `{}` share a hard circular dependency.\ + Operation not possible!", + a, b + ), + } + ) + } +} diff --git a/development/tools/cargo-workspace2/src/ops/executor.rs b/development/tools/cargo-workspace2/src/ops/executor.rs new file mode 100644 index 000000000000..90a4f68303b1 --- /dev/null +++ b/development/tools/cargo-workspace2/src/ops/executor.rs @@ -0,0 +1,33 @@ +//! An op executor + +use super::{versions, Print, Publish, PublishMod as Mod, PublishType}; +use crate::models::{CrateId, DepGraph}; +use semver::Version; +use std::path::PathBuf; + +#[inline] +fn abs_path<'a>(root: &PathBuf, c_path: PathBuf, abs: bool) -> String { + if abs { + root.join(c_path).display().to_string() + } else { + c_path.display().to_string() + } +} + +/// A simple print executor +pub(super) fn print(p: Print, set: Vec<CrateId>, root: PathBuf, g: &mut DepGraph) { + set.into_iter() + .map(|id| g.get_crate(id)) + .map(|_crate| { + let c_path = _crate.path().clone(); + + match p { + Print::Name => format!("{}", _crate.name()), + Print::Path { abs } => format!("{}", abs_path(&root, c_path, abs)), + Print::Both { abs } => { + format!("{}: {}", _crate.name(), abs_path(&root, c_path, abs)) + } + } + }) + .for_each(|s| println!("{}", s)); +} diff --git a/development/tools/cargo-workspace2/src/ops/mod.rs b/development/tools/cargo-workspace2/src/ops/mod.rs new file mode 100644 index 000000000000..026aaa0a140a --- /dev/null +++ b/development/tools/cargo-workspace2/src/ops/mod.rs @@ -0,0 +1,106 @@ +//! Atomic operations on a cargo workspace +//! +//! This module contains operations that can be executed on a +//! workspace. They take some set of inputs, modelled as fields, and +//! produce a shared output which is represented by `Result` + +mod publish; +pub(self) use publish::{versions, Publish, PublishMod, PublishType}; + +mod error; +mod executor; +mod parser; + +use crate::models::{CrateId, DepGraph}; +pub use error::{OpError, Result}; +use std::path::PathBuf; + +trait RenderHelp { + fn render_help(c: i32) -> !; +} + +/// Render the help-page for a particular command +pub(crate) fn render_help(cmd: String) -> ! { + match cmd.as_str() { + "print" => Print::render_help(0), + "publish" => Publish::render_help(0), + c => { + eprintln!("Unknown command `{}`", c); + std::process::exit(2); + } + } +} + +pub(crate) fn list_commands() { + eprintln!("Available commands:\n"); + eprintln!(" - print: echo the selected crate set"); + eprintln!(" - publish: release new crates on crates.io"); +} + +/// Differentiating operation enum +pub enum Op { + /// Publish a new version, according to some rules + Publish(Publish), + /// Print the query selection + Print(Print), +} + +impl Op { + /// Parse an arg line into an operation to execute + pub fn parse(line: Vec<String>) -> Self { + match parser::run(line) { + Some(op) => op, + None => std::process::exit(2), + } + } + + pub(crate) fn execute(self, set: Vec<CrateId>, root: PathBuf, g: &mut DepGraph) { + match self { + Self::Publish(p) => publish::run(p, set, g), + Self::Print(p) => executor::print(p, set, root, g), + } + } +} + +/// Ask the user to be sure +pub(self) fn verify_user() { + eprintln!("------"); + use std::io::{stdin, stdout, Write}; + let mut s = String::new(); + print!("Execute operations? [N|y]: "); + let _ = stdout().flush(); + stdin().read_line(&mut s).expect("Failed to read term!"); + + match s.trim() { + "Y" | "y" => {} + _ => std::process::exit(0), + } +} + +/// Selection of which type to print +pub enum Print { + /// Default: just the package name + Name, + /// The path inside the repo + Path { abs: bool }, + /// Both the name and path + Both { abs: bool }, +} + +impl Default for Print { + fn default() -> Self { + Self::Name + } +} + +impl RenderHelp for Print { + fn render_help(code: i32) -> ! { + eprintln!("Print the selected set of crates"); + eprintln!("Usage: cargo ws2 <...> print [OPTIONS]\n"); + eprintln!("Available options:\n"); + eprintln!(" - path: print the path of the crate, instead of the name"); + eprintln!(" - both: print the both the path and the name"); + eprintln!(" - abs: (If `path` or `both`) Use an absolute path, instead of relative"); + std::process::exit(code) + } +} diff --git a/development/tools/cargo-workspace2/src/ops/parser.rs b/development/tools/cargo-workspace2/src/ops/parser.rs new file mode 100644 index 000000000000..6b7d39b71851 --- /dev/null +++ b/development/tools/cargo-workspace2/src/ops/parser.rs @@ -0,0 +1,112 @@ +use super::{Op, Print, Publish, PublishMod as Mod, PublishType::*}; +use semver::Version; + +/// Parse a argument line into an operation +pub(super) fn run(mut line: Vec<String>) -> Option<Op> { + let op = line.remove(0); + + match op.as_str() { + "print" => parse_print(line), + "publish" => parse_publish(line), + op => { + eprintln!("[ERROR]: Unrecognised operation `{}`", op); + None + } + } +} + +fn parse_print(line: Vec<String>) -> Option<Op> { + let abs = line + .get(1) + .map(|abs| match abs.as_str() { + "abs" | "absolute" => true, + _ => false, + }) + .unwrap_or(false); + + match line.get(0).map(|s| s.as_str()) { + Some("path") => Some(Op::Print(Print::Path { abs })), + Some("both") => Some(Op::Print(Print::Both { abs })), + Some("name") | None => Some(Op::Print(Print::Name)), + Some(val) => { + eprintln!("[ERROR]: Unrecognised param: `{}`", val); + None + } + } +} + +fn publish_error() -> Option<Op> { + eprintln!( + "[ERROR]: Missing or invalid operands! +Usage: cargo ws2 <QUERY LANG> publish <level> [modifier] [devel]\n + Valid <level>: major, minor, patch, or '=<VERSION>' to set an override version.\n + Valid [modifier]: alpha, beta, rc\n + [devel]: after publishing, set crate version to <VERSION>-devel" + ); + None +} + +fn parse_publish(mut line: Vec<String>) -> Option<Op> { + line.reverse(); + let tt = match line.pop() { + Some(tt) => tt, + None => { + return publish_error(); + } + }; + + let _mod = line.pop(); + let tt = match (tt.as_str(), _mod.as_ref().map(|s| s.as_str())) { + ("major", Some("alpha")) => Major(Mod::Alpha), + ("major", Some("beta")) => Major(Mod::Beta), + ("major", Some("rc")) => Major(Mod::Rc), + ("minor", Some("alpha")) => Minor(Mod::Alpha), + ("minor", Some("beta")) => Minor(Mod::Beta), + ("minor", Some("rc")) => Minor(Mod::Rc), + ("patch", Some("alpha")) => Patch(Mod::Alpha), + ("patch", Some("beta")) => Patch(Mod::Beta), + ("patch", Some("rc")) => Patch(Mod::Rc), + ("major", Some(_)) | ("major", None) => Major(Mod::None), + ("minor", Some(_)) | ("minor", None) => Minor(Mod::None), + ("patch", Some(_)) | ("patch", None) => Patch(Mod::None), + (v, _) if v.trim().starts_with("=") => { + let vers = v.replace("=", "").to_string(); + if vers.as_str() == "" { + return publish_error(); + } else { + Fixed(vers) + } + } + (_, _) => { + return publish_error(); + } + }; + + let devel = match tt { + // Means _mod was not a mod + Major(Mod::None) | Minor(Mod::None) | Patch(Mod::None) => { + if let Some(devel) = _mod { + if devel == "devel" { + true + } else { + return publish_error(); + } + } else { + false + } + } + _ => { + if let Some(devel) = line.pop() { + if devel == "devel" { + true + } else { + false + } + } else { + false + } + } + }; + + Some(Op::Publish(Publish { tt, devel })) +} diff --git a/development/tools/cargo-workspace2/src/ops/publish/exec.rs b/development/tools/cargo-workspace2/src/ops/publish/exec.rs new file mode 100644 index 000000000000..3f2d36863bcf --- /dev/null +++ b/development/tools/cargo-workspace2/src/ops/publish/exec.rs @@ -0,0 +1,117 @@ +//! Publishing executor + +use super::{Publish, PublishMod as Mod, PublishType}; +use crate::{ + models::{CrateId, DepGraph}, + ops::verify_user, +}; +use semver::Version; +use textwrap; + +pub(in crate::ops) fn run(p: Publish, set: Vec<CrateId>, g: &mut DepGraph) { + let Publish { ref tt, devel } = p; + + let vec = set.into_iter().fold(vec![], |mut vec, id| { + let c = g.mut_crate(id); + let c_name = c.name().clone(); + let curr_ver = c.version(); + let new_ver = bump(&curr_ver, tt); + c.publish(new_ver.clone()); + eprintln!("Bumping `{}`: `{}` ==> `{}`", c.name(), &curr_ver, new_ver); + drop(c); + + g.get_dependents(id).into_iter().for_each(|id| { + let dep = g.mut_crate(id); + dep.change_dependency(&c_name, &new_ver); + eprintln!("Changing dependency `{}`: {} = {{ version = `{}`, ... }} ==> {{ version = `{}`, ... }}", + dep.name(), + c_name, + &curr_ver, + new_ver); + vec.push(id); + }); + + vec.push(id); + vec + }); + + // If we make it past this point the user is sure + verify_user(); + + vec.into_iter().for_each(|(id)| { + let c = g.mut_crate(id); + c.sync(); + }); + + eprintln!("{}", textwrap::fill("Publish complete. Check that everything is in order, \ + then run `cargo publish` to upload to crates.io!", 80)); +} + +/// Bump a version number according to some rules +fn bump(v: &String, tt: &PublishType) -> String { + let mut ver = Version::parse(&v).unwrap(); + let ver = match tt { + PublishType::Fixed(ref ver) => Version::parse(ver).unwrap(), + PublishType::Major(_) => { + ver.increment_major(); + ver + } + PublishType::Minor(_) => { + ver.increment_minor(); + ver + } + PublishType::Patch(_) => { + ver.increment_patch(); + ver + } + }; + + // If a mod applies, handle it... + if let Some(_mod) = tt._mod() { + if ver.is_prerelease() { + let v = ver.clone().to_string(); + let num = v.split(".").last().unwrap(); + let num: usize = num.parse().unwrap(); + ver.clone() + .to_string() + .replace(&format!("{}", num), &format!("{}", num + 1)) + .to_string() + } else { + format!( + "{}-{}", + ver.to_string(), + match _mod { + Mod::Alpha => "alpha.0", + Mod::Beta => "beta.0", + Mod::Rc => "rc.0", + _ => unreachable!(), + } + ) + } + } else { + ver.to_string() + } +} + +macro_rules! assert_bump { + ($input:expr, $level_mod:expr, $expect:expr) => { + assert_eq!(bump(&$input.to_owned(), $level_mod), $expect.to_string()); + }; +} + +#[cfg(test)] +use super::{PublishMod::*, PublishType::*}; + +#[test] +fn bump_major() { + assert_bump!("1.0.0", &Major(None), "2.0.0"); + assert_bump!("1.5.4", &Major(None), "2.0.0"); + assert_bump!("1.3.0", &Major(Alpha), "2.0.0-alpha.0"); +} + +#[test] +fn bump_minor() { + assert_bump!("1.1.0", &Minor(None), "1.2.0"); + assert_bump!("1.5.4", &Minor(Beta), "1.6.0-beta.0"); + assert_bump!("1.5.4-beta.0", &Minor(Beta), "1.6.0-beta.0"); +} diff --git a/development/tools/cargo-workspace2/src/ops/publish/mod.rs b/development/tools/cargo-workspace2/src/ops/publish/mod.rs new file mode 100644 index 000000000000..6022496af273 --- /dev/null +++ b/development/tools/cargo-workspace2/src/ops/publish/mod.rs @@ -0,0 +1,132 @@ +//! Publishing operation handling + +mod exec; +pub(super) use exec::run; + +use super::RenderHelp; + +/// Publish a single crate to crates.io +/// +/// This command does the following things +/// +/// 0. Determine if the git tree has modifications, `cargo publish` +/// will refuse to work otherwise. +/// 1. Find the crate in question +/// 2. Bump the version number according to the user input +/// 3. Find all dependent crates in the workspace with a version bound +/// 4. Update the version bound to the new version +pub struct Publish { + /// The version type to publish + pub tt: PublishType, + /// Whether to set a new devel version after publish + pub devel: bool, +} + +impl RenderHelp for Publish { + fn render_help(code: i32) -> ! { + eprintln!("Publish the selected set of crates"); + eprintln!("Usage: cargo ws2 <...> publish [=]<level> [OPTIONS]"); + eprintln!(""); + eprintln!("When prepending `=` to the level, bump crates in sync\n"); + eprintln!("Available levels:\n"); + eprintln!(" - major: Bump major version (1.0.0 -> 2.0.0)"); + eprintln!(" - minor: Bump minor version (0.5.0 -> 0.6.0)"); + eprintln!(" - patch: Bump patch version (0.5.0 -> 0.5.1)"); + eprintln!(""); + eprintln!("Available options:\n"); + eprintln!(" - alpha: Create a new alpha (append `-alpha.X`)"); + eprintln!(" - beta: Create a new beta (append `-beta.X`)"); + eprintln!(" - rc: Create a new rc (append `-rc.X`)"); + eprintln!(" - devel: Tag next version as -devel"); + std::process::exit(code) + } +} + +/// The level to which to update +/// +/// New versions are based on the previous version, and are always +/// computed on the fly. +/// +/// It's recommended you use the [`versions`](./versions/index.html) +/// builder functions. +pub enum PublishType { + /// A fixed version to set the set to + Fixed(String), + /// A major bump (e.g. 1.0 -> 2.0) + Major(PublishMod), + /// A minor bump (e.g. 0.1 -> 0.2) + Minor(PublishMod), + /// A patch bump (e.g. 1.5.0 -> 1.5.1) + Patch(PublishMod), +} + +impl PublishType { + pub(crate) fn _mod(&self) -> Option<&PublishMod> { + match self { + Self::Major(ref m) => Some(m), + Self::Minor(ref m) => Some(m), + Self::Patch(ref m) => Some(m), + Self::Fixed(_) => None, + } + .and_then(|_mod| match _mod { + PublishMod::None => None, + other => Some(other), + }) + } +} + +/// Version string modifier +/// +/// It's recommended you use the [`versions`](./versions/index.html) +/// builder functions. +pub enum PublishMod { + /// No version modification + None, + /// Append `-alpha$X` where `$X` is a continuously increasing number + Alpha, + /// Append `-beta$X` where `$X` is a continuously increasing number + Beta, + /// Append `-rc$X` where `$X` is a continuously increasing number + Rc, +} + +/// Version bump management +pub mod versions { + use super::{PublishMod, PublishType}; + + /// Create a major publish + pub fn major(_mod: PublishMod) -> PublishType { + PublishType::Major(_mod) + } + + /// Create a minor publish + pub fn minor(_mod: PublishMod) -> PublishType { + PublishType::Minor(_mod) + } + + /// Create a patch publish + pub fn patch(_mod: PublishMod) -> PublishType { + PublishType::Patch(_mod) + } + + /// Create a none modifier + pub fn mod_none() -> PublishMod { + PublishMod::None + } + + /// Create an alpha modifier + pub fn mod_alpha() -> PublishMod { + PublishMod::Alpha + } + + /// Create a beta modifier + pub fn mod_beta() -> PublishMod { + PublishMod::Beta + } + + /// Create an rc modifier + pub fn mod_rc() -> PublishMod { + PublishMod::Rc + } +} + diff --git a/development/tools/cargo-workspace2/src/query/executor.rs b/development/tools/cargo-workspace2/src/query/executor.rs new file mode 100644 index 000000000000..da67324de4c0 --- /dev/null +++ b/development/tools/cargo-workspace2/src/query/executor.rs @@ -0,0 +1,83 @@ +use super::{Constraint, DepConstraint}; +use crate::models::{CrateId, DepGraph}; +use std::collections::BTreeSet; + +/// Execute a simple set query +pub(crate) fn set(crates: &Vec<String>, g: &DepGraph) -> Vec<CrateId> { + crates + .iter() + .filter_map(|name| match g.find_crate(name) { + None => { + eprintln!("[ERROR]: Unable to find crate: `{}`", name); + None + } + some => some, + }) + .collect() +} + +/// Execute a search query on the dependency graph +pub(crate) fn deps(mut deps: Vec<DepConstraint>, g: &DepGraph) -> Vec<CrateId> { + // Parse the anchor point (first crate) + let DepConstraint { + ref _crate, + ref constraint, + } = deps.remove(0); + let init_id = get_crate_error(_crate, g); + + // Get it's dependents + let mut dependents: BTreeSet<_> = match constraint { + Constraint::Initial(true) => g.get_dependents(init_id).into_iter().collect(), + Constraint::Initial(false) => g + .get_all() + .iter() + .filter(|c| c.has_dependency(init_id)) + .map(|c| c.id) + .collect(), + _ => { + eprintln!("Invalid initial constraint! Only `<` and `!<` are allowed!"); + std::process::exit(2); + } + }; + + // Then loop over all other constraints and subtract crates from + // the dependents set until all constraints are met. + deps.reverse(); + while let Some(dc) = deps.pop() { + let DepConstraint { + ref _crate, + ref constraint, + } = dc; + + let id = get_crate_error(_crate, g); + let ldeps = g.get_dependents(id); + dependents = apply_constraint(dependents, ldeps, constraint); + } + + dependents.into_iter().collect() +} + +fn get_crate_error(_crate: &String, g: &DepGraph) -> CrateId { + match g.find_crate(&_crate.trim().to_string()) { + Some(id) => id, + None => { + eprintln!("[ERROR]: Crate `{}` not found in workspace!", _crate); + std::process::exit(2); + } + } +} + +fn apply_constraint( + init: BTreeSet<CrateId>, + cmp: Vec<CrateId>, + cnd: &Constraint, +) -> BTreeSet<CrateId> { + let cmp: BTreeSet<CrateId> = cmp.into_iter().collect(); + let init = init.into_iter(); + match cnd { + Constraint::And(true) => init.filter(|id| cmp.contains(id)).collect(), + Constraint::And(false) => init.filter(|id| !cmp.contains(id)).collect(), + Constraint::Or => init.chain(cmp.into_iter()).collect(), + _ => todo!(), + } +} diff --git a/development/tools/cargo-workspace2/src/query/mod.rs b/development/tools/cargo-workspace2/src/query/mod.rs new file mode 100644 index 000000000000..a888f6571663 --- /dev/null +++ b/development/tools/cargo-workspace2/src/query/mod.rs @@ -0,0 +1,280 @@ +//! 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<String>), + /// 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<DepConstraint>), +} + +impl Query { + /// Parse an argument iterator (provided by `std::env::args`) + pub fn parse<'a>(line: impl Iterator<Item = String>) -> (Self, Vec<String>) { + 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<CrateId> { + 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<String>, +} + +impl QueryParser { + fn new(line: Vec<String>) -> Self { + Self { line } + } + + /// Run the parser until it yields an error or finished query + fn run(mut self) -> Option<(Query, Vec<String>)> { + let line: Vec<String> = + 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(); +} |