From 905aa2ffae9082cc285aebe8b2d0a024afd23bec Mon Sep 17 00:00:00 2001 From: Musikid Date: Sun, 27 Oct 2019 10:37:27 +0100 Subject: [PATCH 1/2] Add JSON support for `list` command # Conflicts: # Cargo.lock # Cargo.toml # src/command/list/mod.rs --- Cargo.lock | 1 + Cargo.toml | 2 +- src/command/list/json.rs | 420 +++++++++++++++++++++++++++++++++++++++ src/command/list/mod.rs | 26 ++- 4 files changed, 443 insertions(+), 6 deletions(-) create mode 100644 src/command/list/json.rs diff --git a/Cargo.lock b/Cargo.lock index fd709fd62..c2abfc313 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1508,6 +1508,7 @@ version = "0.9.0" source = "git+https://github.com/mikrostew/semver?branch=new-parser#7583eb352dc181ccd09978fd2b16461c1b1669c1" dependencies = [ "semver-parser", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1d744ce3e..d0689601a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.78" lazy_static = "1.3.0" log = { version = "0.4", features = ["std"] } -semver = { git = "https://github.com/mikrostew/semver", branch = "new-parser" } +semver = { git = "https://github.com/mikrostew/semver", branch = "new-parser", features = ["serde"] } structopt = "0.2.14" cfg-if = "1.0" mockito = { version = "0.30.0", optional = true } diff --git a/src/command/list/json.rs b/src/command/list/json.rs new file mode 100644 index 000000000..df5ed0b24 --- /dev/null +++ b/src/command/list/json.rs @@ -0,0 +1,420 @@ +use serde_json::{to_string, to_string_pretty}; + +use super::{Node, Package, PackageManager, Toolchain}; + +pub(super) fn format(toolchain: &Toolchain) -> Option { + let (runtimes, package_managers, packages) = match toolchain { + Toolchain::Node(runtimes) => (describe_runtimes(&runtimes), None, None), + Toolchain::PackageManagers(package_managers) => { + (None, describe_package_managers(&package_managers), None) + } + Toolchain::Packages(packages) => (None, None, describe_packages(&packages)), + Toolchain::Tool { + name, + host_packages, + } => (None, None, Some(describe_tool_set(name, host_packages))), + Toolchain::Active { + runtime, + package_manager, + packages, + } => ( + runtime + .as_ref() + .and_then(|r| describe_runtimes(&[(**r).clone()])), + package_manager + .as_ref() + .and_then(|p| describe_package_managers(&[(**p).clone()])), + describe_packages(&packages), + ), + Toolchain::All { + runtimes, + package_managers, + packages, + } => ( + describe_runtimes(&runtimes), + describe_package_managers(&package_managers), + describe_packages(&packages), + ), + }; + + match (runtimes, package_managers, packages) { + (Some(runtimes), Some(package_managers), Some(packages)) => { + Some(format!("{},{},{}", runtimes, package_managers, packages)) + } + (Some(runtimes), Some(package_managers), None) => { + Some(format!("{},{}", runtimes, package_managers)) + } + (Some(runtimes), None, Some(packages)) => Some(format!("{},{}", runtimes, packages)), + (Some(runtimes), None, None) => Some(format!("{}", runtimes)), + (None, Some(package_managers), Some(packages)) => { + Some(format!("{},{}", package_managers, packages)) + } + (None, Some(package_managers), None) => Some(format!("{}", package_managers)), + (None, None, Some(packages)) => Some(format!("{}", packages)), + (None, None, None) => None, + } +} + +fn describe_runtimes(runtimes: &[Node]) -> Option { + #[derive(serde::Serialize)] + struct Runtimes<'a> { + runtimes: &'a [Node], + }; + if runtimes.is_empty() { + None + } else { + Some(to_string_pretty(&Runtimes { runtimes }).unwrap()) + } +} + +fn describe_package_managers(package_managers: &[PackageManager]) -> Option { + #[derive(serde::Serialize)] + struct PackageManagers<'a> { + package_managers: &'a [PackageManager], + }; + if package_managers.is_empty() { + None + } else { + Some(to_string_pretty(&PackageManagers { package_managers }).unwrap()) + } +} + +fn describe_packages(packages: &[Package]) -> Option { + #[derive(serde::Serialize)] + struct Packages<'a> { + packages: &'a [Package], + }; + if packages.is_empty() { + None + } else { + Some(to_string_pretty(&Packages { packages }).unwrap()) + } +} + +fn describe_tool_set(name: &str, hosts: &[Package]) -> String { + #[derive(serde::Serialize)] + struct Tool<'a> { + name: &'a str, + host: &'a Package, + }; + + hosts + .into_iter() + .map(|host| to_string_pretty(&Tool { name, host }).unwrap()) + .collect::>() + .join("\n") +} + +// These tests are organized by way of the *item* being printed, unlike in the +// `human` module, because the formatting is consistent across command formats. +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use lazy_static::lazy_static; + use semver::Version; + + use crate::command::list::PackageDetails; + + lazy_static! { + static ref NODE_VERSION: Version = Version::from((12, 4, 0)); + static ref TYPESCRIPT_VERSION: Version = Version::from((3, 4, 1)); + static ref YARN_VERSION: Version = Version::from((1, 16, 0)); + static ref PROJECT_PATH: PathBuf = PathBuf::from("/a/b/c"); + } + + mod package { + use super::super::*; + use super::*; + + #[test] + fn single_default() { + assert_eq!( + describe_packages(&[Package::Default { + details: PackageDetails { + name: "typescript".into(), + version: TYPESCRIPT_VERSION.clone(), + }, + node: NODE_VERSION.clone(), + tools: vec!["tsc".into(), "tsserver".into()] + }]) + .expect("Should always return a `String` if given a non-empty set") + .as_str(), + "{ + \"packages\": [ + { + \"Default\": { + \"details\": { + \"name\": \"typescript\", + \"version\": \"3.4.1\" + }, + \"node\": \"12.4.0\", + \"tools\": [ + \"tsc\", + \"tsserver\" + ] + } + } + ] +}" + ); + } + + #[test] + fn single_project() { + assert_eq!( + describe_packages(&[Package::Project { + details: PackageDetails { + name: "typescript".into(), + version: TYPESCRIPT_VERSION.clone(), + }, + path: PROJECT_PATH.clone(), + node: NODE_VERSION.clone(), + tools: vec!["tsc".into(), "tsserver".into()] + }]) + .expect("Should always return a `String` if given a non-empty set") + .as_str(), + "{ + \"packages\": [ + { + \"Project\": { + \"details\": { + \"name\": \"typescript\", + \"version\": \"3.4.1\" + }, + \"node\": \"12.4.0\", + \"tools\": [ + \"tsc\", + \"tsserver\" + ], + \"path\": \"/a/b/c\" + } + } + ] +}" + ); + } + + #[test] + fn mixed() { + assert_eq!( + describe_packages(&[ + Package::Project { + details: PackageDetails { + name: "typescript".into(), + version: TYPESCRIPT_VERSION.clone(), + }, + path: PROJECT_PATH.clone(), + node: NODE_VERSION.clone(), + tools: vec!["tsc".into(), "tsserver".into()] + }, + Package::Default { + details: PackageDetails { + name: "ember-cli".into(), + version: Version::from((3, 10, 0)), + }, + node: NODE_VERSION.clone(), + tools: vec!["ember".into()], + }, + Package::Fetched(PackageDetails { + name: "create-react-app".into(), + version: Version::from((1, 0, 0)), + }) + ]) + .expect("Should always return a `String` if given a non-empty set") + .as_str(), + "{ + \"packages\": [ + { + \"Project\": { + \"details\": { + \"name\": \"typescript\", + \"version\": \"3.4.1\" + }, + \"node\": \"12.4.0\", + \"tools\": [ + \"tsc\", + \"tsserver\" + ], + \"path\": \"/a/b/c\" + } + }, + { + \"Default\": { + \"details\": { + \"name\": \"ember-cli\", + \"version\": \"3.10.0\" + }, + \"node\": \"12.4.0\", + \"tools\": [ + \"ember\" + ] + } + }, + { + \"Fetched\": { + \"name\": \"create-react-app\", + \"version\": \"1.0.0\" + } + } + ] +}" + ); + } + + #[test] + fn installed_not_set() { + assert_eq!( + describe_packages(&[Package::Fetched(PackageDetails { + name: "typescript".into(), + version: TYPESCRIPT_VERSION.clone(), + })]) + .expect("Should always return a `String` if given a non-empty set") + .as_str(), + "{ + \"packages\": [ + { + \"Fetched\": { + \"name\": \"typescript\", + \"version\": \"3.4.1\" + } + } + ] +}" + ); + } + } + + mod toolchain { + use super::super::*; + use super::*; + use crate::command::list::{Node, PackageManager, PackageManagerKind, Source, Toolchain}; + + #[test] + fn full() { + assert_eq!( + format(&Toolchain::All { + runtimes: vec![ + Node { + source: Source::Default, + version: NODE_VERSION.clone() + }, + Node { + source: Source::None, + version: Version::from((8, 2, 4)) + } + ], + package_managers: vec![ + PackageManager { + kind: PackageManagerKind::Yarn, + source: Source::Project(PROJECT_PATH.clone()), + version: YARN_VERSION.clone() + }, + PackageManager { + kind: PackageManagerKind::Yarn, + source: Source::Default, + version: Version::from((1, 17, 0)) + } + ], + packages: vec![ + Package::Default { + details: PackageDetails { + name: "ember-cli".into(), + version: Version::from((3, 10, 2)), + }, + node: NODE_VERSION.clone(), + tools: vec!["ember".into()] + }, + Package::Project { + details: PackageDetails { + name: "ember-cli".into(), + version: Version::from((3, 8, 1)), + }, + path: PROJECT_PATH.clone(), + node: NODE_VERSION.clone(), + tools: vec!["ember".into()] + }, + Package::Default { + details: PackageDetails { + name: "typescript".into(), + version: TYPESCRIPT_VERSION.clone(), + }, + node: NODE_VERSION.clone(), + tools: vec!["tsc".into(), "tsserver".into()] + } + ] + }) + .expect("`format` with a non-empty toolchain returns `Some`") + .as_str(), + "{ + \"runtimes\": [ + { + \"source\": \"Default\", + \"version\": \"12.4.0\" + }, + { + \"source\": \"None\", + \"version\": \"8.2.4\" + } + ] +},{ + \"package_managers\": [ + { + \"kind\": \"Yarn\", + \"source\": { + \"Project\": \"/a/b/c\" + }, + \"version\": \"1.16.0\" + }, + { + \"kind\": \"Yarn\", + \"source\": \"Default\", + \"version\": \"1.17.0\" + } + ] +},{ + \"packages\": [ + { + \"Default\": { + \"details\": { + \"name\": \"ember-cli\", + \"version\": \"3.10.2\" + }, + \"node\": \"12.4.0\", + \"tools\": [ + \"ember\" + ] + } + }, + { + \"Project\": { + \"details\": { + \"name\": \"ember-cli\", + \"version\": \"3.8.1\" + }, + \"node\": \"12.4.0\", + \"tools\": [ + \"ember\" + ], + \"path\": \"/a/b/c\" + } + }, + { + \"Default\": { + \"details\": { + \"name\": \"typescript\", + \"version\": \"3.4.1\" + }, + \"node\": \"12.4.0\", + \"tools\": [ + \"tsc\", + \"tsserver\" + ] + } + } + ] +}" + ) + } + } +} diff --git a/src/command/list/mod.rs b/src/command/list/mod.rs index c81d4423b..238ebd4cc 100644 --- a/src/command/list/mod.rs +++ b/src/command/list/mod.rs @@ -1,4 +1,5 @@ mod human; +mod json; mod plain; mod toolchain; @@ -8,6 +9,7 @@ use semver::Version; use structopt::StructOpt; use crate::command::Command; +use serde::Serialize; use toolchain::Toolchain; use volta_core::error::{ExitCode, Fallible}; use volta_core::inventory::package_configs; @@ -18,6 +20,7 @@ use volta_core::tool::PackageConfig; #[derive(Copy, Clone, PartialEq)] enum Format { Human, + JSON, Plain, } @@ -27,6 +30,7 @@ impl FromStr for Format { fn from_str(s: &str) -> Result { match s { "human" => Ok(Format::Human), + "json" => Ok(Format::JSON), "plain" => Ok(Format::Plain), _ => Err("No".into()), } @@ -38,7 +42,7 @@ impl FromStr for Format { /// Note: this is distinct from `volta_core::platform::sourced::Source`, which /// represents the source only of a `Platform`, which is a composite structure. /// By contrast, this `Source` is concerned *only* with a single item. -#[derive(Clone, PartialEq, Debug)] +#[derive(Clone, PartialEq, Debug, Serialize)] enum Source { /// The item is from a project. The wrapped `PathBuf` is the path to the /// project's `package.json`. @@ -77,6 +81,7 @@ impl fmt::Display for Source { /// A package and its associated tools, for displaying to the user as part of /// their toolchain. +#[derive(Serialize)] struct PackageDetails { /// The name of the package. pub name: String, @@ -84,6 +89,7 @@ struct PackageDetails { pub version: Version, } +#[derive(Serialize)] enum Package { Default { details: PackageDetails, @@ -145,13 +151,13 @@ impl Package { } } -#[derive(Clone)] +#[derive(Clone, Serialize)] struct Node { pub source: Source, pub version: Version, } -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)] enum PackageManagerKind { Npm, Yarn, @@ -170,7 +176,7 @@ impl fmt::Display for PackageManagerKind { } } -#[derive(Clone)] +#[derive(Clone, Serialize)] struct PackageManager { kind: PackageManagerKind, source: Source, @@ -208,9 +214,18 @@ pub(crate) struct List { /// Specify the output format. /// /// Defaults to `human` for TTYs, `plain` otherwise. - #[structopt(long = "format", raw(possible_values = r#"&["human", "plain"]"#))] + #[structopt( + long = "format", + raw(possible_values = r#"&["human", "plain", "json"]"#) + )] format: Option, + /// Show a pretty version of the output (only for JSON format). + /// + /// + #[structopt(long = "pretty", short = "p")] + pretty: bool, + /// Show the currently-active tool(s). /// /// Equivalent to `volta list` when not specifying a specific tool. @@ -278,6 +293,7 @@ impl Command for List { let default_platform = session.default_platform()?; let format = match self.output_format() { Format::Human => human::format, + Format::JSON => json::format, Format::Plain => plain::format, }; From 64a51e2544dc01640644f65c855095c042717ed0 Mon Sep 17 00:00:00 2001 From: Alex LaFroscia Date: Sat, 5 Feb 2022 10:34:55 -0500 Subject: [PATCH 2/2] Get tests passing for JSON list format --- src/command/list/json.rs | 101 ++++++++++++++++----------------------- src/command/list/mod.rs | 6 --- 2 files changed, 40 insertions(+), 67 deletions(-) diff --git a/src/command/list/json.rs b/src/command/list/json.rs index df5ed0b24..0baa86a56 100644 --- a/src/command/list/json.rs +++ b/src/command/list/json.rs @@ -1,29 +1,21 @@ -use serde_json::{to_string, to_string_pretty}; +//! Define the "JSON" format style for list commands. + +use serde_json::to_string_pretty; use super::{Node, Package, PackageManager, Toolchain}; pub(super) fn format(toolchain: &Toolchain) -> Option { let (runtimes, package_managers, packages) = match toolchain { Toolchain::Node(runtimes) => (describe_runtimes(&runtimes), None, None), - Toolchain::PackageManagers(package_managers) => { - (None, describe_package_managers(&package_managers), None) - } - Toolchain::Packages(packages) => (None, None, describe_packages(&packages)), - Toolchain::Tool { - name, - host_packages, - } => (None, None, Some(describe_tool_set(name, host_packages))), Toolchain::Active { runtime, - package_manager, + package_managers, packages, } => ( runtime .as_ref() .and_then(|r| describe_runtimes(&[(**r).clone()])), - package_manager - .as_ref() - .and_then(|p| describe_package_managers(&[(**p).clone()])), + describe_package_managers(&package_managers), describe_packages(&packages), ), Toolchain::All { @@ -35,6 +27,14 @@ pub(super) fn format(toolchain: &Toolchain) -> Option { describe_package_managers(&package_managers), describe_packages(&packages), ), + Toolchain::PackageManagers { managers, .. } => { + (None, describe_package_managers(&managers), None) + } + Toolchain::Packages(packages) => (None, None, describe_packages(&packages)), + Toolchain::Tool { + name, + host_packages, + } => (None, None, Some(describe_tool_set(name, host_packages))), }; match (runtimes, package_managers, packages) { @@ -55,11 +55,12 @@ pub(super) fn format(toolchain: &Toolchain) -> Option { } } +#[derive(serde::Serialize)] +struct Runtimes<'a> { + runtimes: &'a [Node], +} + fn describe_runtimes(runtimes: &[Node]) -> Option { - #[derive(serde::Serialize)] - struct Runtimes<'a> { - runtimes: &'a [Node], - }; if runtimes.is_empty() { None } else { @@ -67,11 +68,12 @@ fn describe_runtimes(runtimes: &[Node]) -> Option { } } +#[derive(serde::Serialize)] +struct PackageManagers<'a> { + package_managers: &'a [PackageManager], +} + fn describe_package_managers(package_managers: &[PackageManager]) -> Option { - #[derive(serde::Serialize)] - struct PackageManagers<'a> { - package_managers: &'a [PackageManager], - }; if package_managers.is_empty() { None } else { @@ -79,11 +81,12 @@ fn describe_package_managers(package_managers: &[PackageManager]) -> Option { + packages: &'a [Package], +} + fn describe_packages(packages: &[Package]) -> Option { - #[derive(serde::Serialize)] - struct Packages<'a> { - packages: &'a [Package], - }; if packages.is_empty() { None } else { @@ -91,13 +94,13 @@ fn describe_packages(packages: &[Package]) -> Option { } } -fn describe_tool_set(name: &str, hosts: &[Package]) -> String { - #[derive(serde::Serialize)] - struct Tool<'a> { - name: &'a str, - host: &'a Package, - }; +#[derive(serde::Serialize)] +struct Tool<'a> { + name: &'a str, + host: &'a Package, +} +fn describe_tool_set(name: &str, hosts: &[Package]) -> String { hosts .into_iter() .map(|host| to_string_pretty(&Tool { name, host }).unwrap()) @@ -164,12 +167,8 @@ mod tests { fn single_project() { assert_eq!( describe_packages(&[Package::Project { - details: PackageDetails { - name: "typescript".into(), - version: TYPESCRIPT_VERSION.clone(), - }, + name: "typescript".into(), path: PROJECT_PATH.clone(), - node: NODE_VERSION.clone(), tools: vec!["tsc".into(), "tsserver".into()] }]) .expect("Should always return a `String` if given a non-empty set") @@ -178,11 +177,7 @@ mod tests { \"packages\": [ { \"Project\": { - \"details\": { - \"name\": \"typescript\", - \"version\": \"3.4.1\" - }, - \"node\": \"12.4.0\", + \"name\": \"typescript\", \"tools\": [ \"tsc\", \"tsserver\" @@ -200,12 +195,8 @@ mod tests { assert_eq!( describe_packages(&[ Package::Project { - details: PackageDetails { - name: "typescript".into(), - version: TYPESCRIPT_VERSION.clone(), - }, + name: "typescript".into(), path: PROJECT_PATH.clone(), - node: NODE_VERSION.clone(), tools: vec!["tsc".into(), "tsserver".into()] }, Package::Default { @@ -227,11 +218,7 @@ mod tests { \"packages\": [ { \"Project\": { - \"details\": { - \"name\": \"typescript\", - \"version\": \"3.4.1\" - }, - \"node\": \"12.4.0\", + \"name\": \"typescript\", \"tools\": [ \"tsc\", \"tsserver\" @@ -326,12 +313,8 @@ mod tests { tools: vec!["ember".into()] }, Package::Project { - details: PackageDetails { - name: "ember-cli".into(), - version: Version::from((3, 8, 1)), - }, + name: "ember-cli".into(), path: PROJECT_PATH.clone(), - node: NODE_VERSION.clone(), tools: vec!["ember".into()] }, Package::Default { @@ -388,11 +371,7 @@ mod tests { }, { \"Project\": { - \"details\": { - \"name\": \"ember-cli\", - \"version\": \"3.8.1\" - }, - \"node\": \"12.4.0\", + \"name\": \"ember-cli\", \"tools\": [ \"ember\" ], diff --git a/src/command/list/mod.rs b/src/command/list/mod.rs index 238ebd4cc..b405ffa91 100644 --- a/src/command/list/mod.rs +++ b/src/command/list/mod.rs @@ -220,12 +220,6 @@ pub(crate) struct List { )] format: Option, - /// Show a pretty version of the output (only for JSON format). - /// - /// - #[structopt(long = "pretty", short = "p")] - pretty: bool, - /// Show the currently-active tool(s). /// /// Equivalent to `volta list` when not specifying a specific tool.