Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Copy PackagesFilter into scarb-ui #583

Merged
merged 1 commit into from
Aug 22, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions extensions/scarb-cairo-run/Cargo.toml
Original file line number Diff line number Diff line change
@@ -12,9 +12,10 @@ cairo-lang-runner.workspace = true
cairo-lang-sierra.workspace = true
camino.workspace = true
clap.workspace = true
scarb-metadata = { path = "../../scarb-metadata", features = ["packages_filter"] }
serde_json.workspace = true
indoc.workspace = true
scarb-metadata = { path = "../../scarb-metadata" }
scarb-ui = { path = "../../utils/scarb-ui" }
serde_json.workspace = true

[dev-dependencies]
scarb-test-support = { path = "../../utils/scarb-test-support" }
2 changes: 1 addition & 1 deletion extensions/scarb-cairo-run/src/main.rs
Original file line number Diff line number Diff line change
@@ -12,8 +12,8 @@ use camino::Utf8PathBuf;
use clap::Parser;
use indoc::formatdoc;

use scarb_metadata::packages_filter::PackagesFilter;
use scarb_metadata::{MetadataCommand, ScarbCommand};
use scarb_ui::args::PackagesFilter;

/// Execute the main function of a package.
#[derive(Parser, Clone, Debug)]
3 changes: 2 additions & 1 deletion extensions/scarb-cairo-test/Cargo.toml
Original file line number Diff line number Diff line change
@@ -13,7 +13,8 @@ cairo-lang-filesystem.workspace = true
cairo-lang-starknet.workspace = true
cairo-lang-test-runner.workspace = true
clap.workspace = true
scarb-metadata = { path = "../../scarb-metadata", features = ["packages_filter"] }
scarb-metadata = { path = "../../scarb-metadata" }
scarb-ui = { path = "../../utils/scarb-ui" }
serde_json.workspace = true

[dev-dependencies]
2 changes: 1 addition & 1 deletion extensions/scarb-cairo-test/src/main.rs
Original file line number Diff line number Diff line change
@@ -13,8 +13,8 @@ use cairo_lang_test_runner::plugin::TestPlugin;
use cairo_lang_test_runner::TestRunner;
use clap::Parser;

use scarb_metadata::packages_filter::PackagesFilter;
use scarb_metadata::{CompilationUnitMetadata, Metadata, MetadataCommand, PackageId};
use scarb_ui::args::PackagesFilter;

/// Execute all unit tests of a local package.
#[derive(Parser, Clone, Debug)]
2 changes: 1 addition & 1 deletion scarb/src/bin/scarb/args.rs
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ use scarb::compiler::Profile;
use scarb::core::PackageName;
use scarb::manifest_editor::DepId;
use scarb::version;
use scarb_metadata::packages_filter::PackagesFilter;
use scarb_ui::args::PackagesFilter;
use scarb_ui::OutputFormat;

/// The Cairo package manager.
2 changes: 1 addition & 1 deletion scarb/src/core/package/mod.rs
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@ use serde::Deserialize;

pub use id::*;
pub use name::*;
use scarb_metadata::packages_filter::WithManifestPath;
use scarb_ui::args::WithManifestPath;

use crate::core::manifest::Manifest;
use crate::core::Target;
3 changes: 1 addition & 2 deletions scarb/src/core/workspace.rs
Original file line number Diff line number Diff line change
@@ -3,8 +3,7 @@ use std::fmt;

use anyhow::{anyhow, bail, Result};
use camino::{Utf8Path, Utf8PathBuf};

use scarb_metadata::packages_filter::PackagesSource;
use scarb_ui::args::PackagesSource;

use crate::compiler::Profile;
use crate::core::config::Config;
2 changes: 1 addition & 1 deletion scarb/src/ops/metadata.rs
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ use semver::Version;
use smol_str::SmolStr;

use scarb_metadata as m;
use scarb_metadata::packages_filter::PackagesSource;
use scarb_ui::args::PackagesSource;

use crate::compiler::CompilationUnit;
use crate::core::{ManifestDependency, Package, PackageId, SourceId, Target, Workspace};
3 changes: 3 additions & 0 deletions utils/scarb-ui/Cargo.toml
Original file line number Diff line number Diff line change
@@ -6,8 +6,11 @@ publish = false

[dependencies]
anyhow.workspace = true
camino.workspace = true
clap.workspace = true
console.workspace = true
indicatif.workspace = true
indoc.workspace = true
scarb-metadata = { version = "*", path = "../../scarb-metadata" }
serde.workspace = true
serde_json.workspace = true
3 changes: 3 additions & 0 deletions utils/scarb-ui/src/args/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub use packages_filter::*;

mod packages_filter;
231 changes: 231 additions & 0 deletions utils/scarb-ui/src/args/packages_filter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
use std::fmt;

use anyhow::{bail, ensure, Result};
use camino::{Utf8Path, Utf8PathBuf};
use indoc::{formatdoc, indoc};

use scarb_metadata::{Metadata, PackageMetadata};

/// [`clap`] structured arguments that provide package selection.
///
/// ## Usage
///
/// ```no_run
/// # use scarb_ui::args::PackagesFilter;
/// #[derive(clap::Parser)]
/// struct Args {
/// #[command(flatten)]
/// packages_filter: PackagesFilter,
/// }
/// ```
#[derive(clap::Parser, Clone, Debug)]
pub struct PackagesFilter {
/// Packages to run this command on, can be a concrete package name (`foobar`) or
/// a prefix glob (`foo*`).
#[arg(short, long, value_name = "SPEC", default_value = "*")]
package: String,
/// Run for all packages in the workspace.
#[arg(short, long, conflicts_with = "package")]
workspace: bool,
}

impl PackagesFilter {
/// Find *exactly one* package matching the filter.
///
/// Returns an error if no or more than one packages were found.
pub fn match_one<S: PackagesSource>(&self, source: &S) -> Result<S::Package> {
let spec = Spec::parse(&self.package)?;

// Check for current package.
// If none (in case of virtual workspace), run for all members.
if self.current_selected(&spec) {
if let Some(pkg) = self.current_package(source)? {
return Ok(pkg);
}
}

let members = source.members();

if (self.workspace || matches!(spec, Spec::All)) && members.len() > 1 {
bail!(indoc! {r#"
could not determine which package to work on
help: use the `--package` option to specify the package
"#});
}

let found = Self::do_match::<S>(&spec, self.workspace, members.into_iter())?;

ensure!(
found.len() <= 1,
formatdoc! {r#"
workspace has multiple members matching `{spec}`
help: use the `--package` option to specify single package
"#}
);

Ok(found.into_iter().next().unwrap())
}

/// Find *at least one* package matching the filter.
///
/// Returns an error if no packages were found.
pub fn match_many<S: PackagesSource>(&self, source: &S) -> Result<Vec<S::Package>> {
let spec = Spec::parse(&self.package)?;

// Check for current package.
// If none (in case of virtual workspace), run for all members.
if self.current_selected(&spec) {
if let Some(pkg) = self.current_package(source)? {
return Ok(vec![pkg]);
}
}

let members = source.members();
Self::do_match::<S>(&spec, self.workspace, members.into_iter())
}

fn current_package<S: PackagesSource>(&self, source: &S) -> Result<Option<S::Package>> {
Ok(source
.members()
.iter()
.find(|m| m.manifest_path() == source.runtime_manifest())
.cloned())
}

fn current_selected(&self, spec: &Spec<'_>) -> bool {
!self.workspace && matches!(spec, Spec::All)
}

fn do_match<S: PackagesSource>(
spec: &Spec<'_>,
workspace: bool,
members: impl Iterator<Item = S::Package>,
) -> Result<Vec<S::Package>> {
let mut members = members.peekable();

ensure!(members.peek().is_some(), "workspace has no members");

let matches = if workspace {
members.collect::<Vec<_>>()
} else {
members
.filter(|pkg| spec.matches(S::package_name_of(pkg)))
.collect::<Vec<_>>()
};

if matches.is_empty() {
match spec {
Spec::One(package_name) => bail!("package `{package_name}` not found in workspace"),
Spec::All | Spec::Glob(_) => bail!("no workspace members match `{spec}`"),
}
}

Ok(matches)
}
}

enum Spec<'a> {
All,
One(&'a str),
Glob(&'a str),
}

impl<'a> Spec<'a> {
fn parse(string: &'a str) -> Result<Self> {
let string = string.trim();

if !string.contains('*') {
return Ok(Self::One(string));
}

ensure!(
string.chars().filter(|c| *c == '*').count() == 1,
"invalid package spec: * character can only occur once in the pattern"
);
ensure!(
string.ends_with('*'),
"invalid package spec: only `prefix*` patterns are allowed"
);

let string = string.trim_end_matches('*');

if string.is_empty() {
Ok(Self::All)
} else {
Ok(Self::Glob(string))
}
}

fn matches(&self, name: &str) -> bool {
match self {
Spec::All => true,
Spec::One(pat) => name == *pat,
Spec::Glob(pat) => name.starts_with(pat),
}
}
}

impl<'a> fmt::Display for Spec<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Spec::All => write!(f, "*"),
Spec::One(name) => write!(f, "{name}"),
Spec::Glob(pat) => write!(f, "{pat}*"),
}
}
}

/// Generic interface used by [`PackagesSource`] to pull information from.
///
/// This trait is Scarb's internal implementation detail, **do not implement for your own types**.
pub trait WithManifestPath {
#[doc(hidden)]
fn manifest_path(&self) -> &Utf8Path;
}

impl WithManifestPath for PackageMetadata {
fn manifest_path(&self) -> &Utf8Path {
&self.manifest_path
}
}

/// Generic interface used by [`PackagesFilter`] to pull information from.
///
/// This trait is Scarb's internal implementation detail, **do not implement for your own types**.
/// Inside Scarb there are implementations for Scarb's core types, which allows Scarb to re-use
/// [`PackagesFilter`] logic.
pub trait PackagesSource {
/// Type which represents a Scarb package in this source.
type Package: Clone + WithManifestPath;

#[doc(hidden)]
fn package_name_of(package: &Self::Package) -> &str;

#[doc(hidden)]
fn members(&self) -> Vec<Self::Package>;

#[doc(hidden)]
fn runtime_manifest(&self) -> Utf8PathBuf;
}

impl PackagesSource for Metadata {
type Package = PackageMetadata;

#[inline(always)]
fn package_name_of(package: &Self::Package) -> &str {
&package.name
}

#[inline(always)]
fn members(&self) -> Vec<Self::Package> {
self.packages
.iter()
.filter(|pkg| self.workspace.members.contains(&pkg.id))
.cloned()
.collect()
}

fn runtime_manifest(&self) -> Utf8PathBuf {
self.runtime_manifest.clone()
}
}
1 change: 1 addition & 0 deletions utils/scarb-ui/src/lib.rs
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@ pub use widget::*;

use crate::components::TypedMessage;

pub mod args;
pub mod components;
mod message;
mod verbosity;