aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart/src/commands
diff options
context:
space:
mode:
Diffstat (limited to 'crates/mozart/src/commands')
-rw-r--r--crates/mozart/src/commands/fund.rs634
1 files changed, 211 insertions, 423 deletions
diff --git a/crates/mozart/src/commands/fund.rs b/crates/mozart/src/commands/fund.rs
index f240378..707e9ee 100644
--- a/crates/mozart/src/commands/fund.rs
+++ b/crates/mozart/src/commands/fund.rs
@@ -1,10 +1,14 @@
use clap::Args;
-use mozart_core::console;
+use mozart_core::composer::Composer;
+use mozart_core::console::{Console, hyperlink};
use mozart_core::console_format;
use mozart_core::console_writeln;
+use mozart_core::exit_code;
+use mozart_registry::cache::{Cache, build_cache_config};
+use mozart_registry::installed::InstalledPackages;
+use mozart_registry::repository::{PackageQuery, RepositorySet};
use serde::Serialize;
-use std::collections::BTreeMap;
-use std::path::Path;
+use std::collections::{BTreeMap, BTreeSet};
#[derive(Args)]
pub struct FundArgs {
@@ -13,171 +17,130 @@ pub struct FundArgs {
pub format: Option<String>,
}
-struct FundingLink {
- url: String,
- funding_type: Option<String>,
-}
-
-struct FundingEntry {
- full_name: String,
- links: Vec<FundingLink>,
-}
-
-pub async fn execute(
- args: &FundArgs,
- cli: &super::Cli,
- console: &console::Console,
-) -> anyhow::Result<()> {
- let working_dir = cli.working_dir()?;
-
- // Validate format
+pub async fn execute(args: &FundArgs, cli: &super::Cli, console: &Console) -> anyhow::Result<()> {
let format = args.format.as_deref().unwrap_or("text");
- if format != "text" && format != "json" {
- anyhow::bail!(
- "Invalid format \"{}\". Supported formats: text, json",
- format
- );
+ if !matches!(format, "text" | "json") {
+ console.error(&console_format!(
+ "<error>Unsupported format \"{format}\". See help for supported formats.</error>"
+ ));
+ return Err(exit_code::bail_silent(exit_code::GENERAL_ERROR));
}
- // Try lock file first (preferred), fall back to installed.json
- let lock_path = working_dir.join("composer.lock");
- let entries = if lock_path.exists() {
- collect_funding_from_locked(&working_dir)?
- } else {
- collect_funding_from_installed(&working_dir)?
- };
-
- let grouped = group_by_vendor(&entries);
+ let working_dir = cli.working_dir()?;
+ let composer = Composer::require(&working_dir)?;
+ let installed = InstalledPackages::read(composer.installation_manager().vendor_dir())?;
- match format {
- "json" => render_json(&grouped, console)?,
- _ => render_text(&grouped, console),
- }
+ // Configured remote repositories from `composer.json` are not yet wired
+ // up; this matches the known divergence already present in
+ // `commands/search.rs` and Composer's full `CompositeRepository`.
+ let repo_cache = Cache::repo(&build_cache_config(cli.no_cache));
+ let remote_repos = RepositorySet::with_packagist(repo_cache);
- Ok(())
-}
+ let mut packages_to_load: BTreeSet<String> =
+ installed.packages.iter().map(|p| p.name.clone()).collect();
-fn collect_funding_from_locked(working_dir: &Path) -> anyhow::Result<Vec<FundingEntry>> {
- let lock_path = working_dir.join("composer.lock");
- let lock = mozart_registry::lockfile::LockFile::read_from_file(&lock_path)?;
+ let mut fundings: BTreeMap<String, BTreeMap<String, Vec<String>>> = BTreeMap::new();
- let mut all_packages: Vec<&mozart_registry::lockfile::LockedPackage> =
- lock.packages.iter().collect();
- if let Some(ref pkgs_dev) = lock.packages_dev {
- all_packages.extend(pkgs_dev.iter());
- }
+ // Pass 1: load default-branch metadata from remote repos and pull funding
+ // info from there first. Mirrors `FundCommand::execute` L60-74. Composer
+ // passes `['dev' => STABILITY_DEV]` so default-branch versions are
+ // returned; Mozart's repo layer does not filter by stability, so an
+ // unconstrained query yields them naturally.
+ if !packages_to_load.is_empty() {
+ let queries: Vec<PackageQuery<'_>> = packages_to_load
+ .iter()
+ .map(|n| PackageQuery {
+ name: n.as_str(),
+ constraint: None,
+ })
+ .collect();
+ let result = remote_repos.load_packages(&queries).await?;
- let entries = all_packages
- .iter()
- .filter_map(|p| {
- let funding_vec = p.funding.as_deref()?;
- if funding_vec.is_empty() {
- return None;
+ for named in &result {
+ if !named.version.default_branch {
+ continue;
}
- let links = extract_funding_links(funding_vec);
- if links.is_empty() {
- return None;
+ let Some(funding) = named.version.funding.as_deref() else {
+ continue;
+ };
+ if funding.is_empty() {
+ continue;
}
- Some(FundingEntry {
- full_name: p.name.clone(),
- links,
- })
- })
- .collect();
+ insert_funding_data(&mut fundings, &named.name, funding);
+ packages_to_load.remove(&named.name);
+ }
+ }
- Ok(entries)
-}
+ // Pass 2: fall back to installed-package funding for names whose default
+ // branch had nothing. Mirrors `FundCommand::execute` L77-85.
+ for installed_pkg in &installed.packages {
+ if !packages_to_load.contains(&installed_pkg.name) {
+ continue;
+ }
+ let Some(funding_val) = installed_pkg.extra_fields.get("funding") else {
+ continue;
+ };
+ let Some(funding) = funding_val.as_array() else {
+ continue;
+ };
+ if funding.is_empty() {
+ continue;
+ }
+ insert_funding_data(&mut fundings, &installed_pkg.name, funding);
+ }
-fn collect_funding_from_installed(working_dir: &Path) -> anyhow::Result<Vec<FundingEntry>> {
- let vendor_dir = working_dir.join("vendor");
- let installed = mozart_registry::installed::InstalledPackages::read(&vendor_dir)?;
+ // BTreeMap iteration is alphabetical — covers `ksort($fundings)`.
- let entries = installed
- .packages
- .iter()
- .filter_map(|p| {
- let funding_val = p.extra_fields.get("funding")?;
- let funding_arr = funding_val.as_array()?;
- if funding_arr.is_empty() {
- return None;
- }
- let links = extract_funding_links(funding_arr);
- if links.is_empty() {
- return None;
- }
- Some(FundingEntry {
- full_name: p.name.clone(),
- links,
- })
- })
- .collect();
+ match format {
+ "json" => render_json(&fundings, console)?,
+ _ => render_text(&fundings, console),
+ }
- Ok(entries)
+ Ok(())
}
-fn extract_funding_links(funding_json: &[serde_json::Value]) -> Vec<FundingLink> {
- funding_json
- .iter()
- .filter_map(|entry| {
- let url = entry.get("url").and_then(|v| v.as_str()).unwrap_or("");
- if url.is_empty() {
- return None;
- }
- let funding_type = entry
- .get("type")
- .and_then(|v| v.as_str())
- .map(|s| s.to_string());
- Some(FundingLink {
- url: url.to_string(),
- funding_type,
- })
- })
- .collect()
+/// Mirror of `FundCommand::insertFundingData`. Splits the package name on
+/// `/`, applies the GitHub profile-to-sponsors rewrite, and appends the
+/// package onto `fundings[vendor][url]`.
+fn insert_funding_data(
+ fundings: &mut BTreeMap<String, BTreeMap<String, Vec<String>>>,
+ pretty_name: &str,
+ funding: &[serde_json::Value],
+) {
+ let Some((vendor, package_name)) = pretty_name.split_once('/') else {
+ return;
+ };
+ for entry in funding {
+ let url = entry.get("url").and_then(|v| v.as_str()).unwrap_or("");
+ if url.is_empty() {
+ continue;
+ }
+ let funding_type = entry.get("type").and_then(|v| v.as_str());
+ let url = rewrite_github_url(url, funding_type);
+ fundings
+ .entry(vendor.to_string())
+ .or_default()
+ .entry(url)
+ .or_default()
+ .push(package_name.to_string());
+ }
}
fn rewrite_github_url(url: &str, funding_type: Option<&str>) -> String {
if funding_type != Some("github") {
return url.to_string();
}
- // Match exactly https://github.com/{user} with no further path segments
- if let Some(rest) = url.strip_prefix("https://github.com/") {
- // rest must be a single path segment (no '/')
- if !rest.is_empty() && !rest.contains('/') {
- return format!("https://github.com/sponsors/{}", rest);
- }
+ if let Some(rest) = url.strip_prefix("https://github.com/")
+ && !rest.is_empty()
+ && !rest.contains('/')
+ {
+ return format!("https://github.com/sponsors/{rest}");
}
url.to_string()
}
-fn group_by_vendor(entries: &[FundingEntry]) -> BTreeMap<String, BTreeMap<String, Vec<String>>> {
- let mut grouped: BTreeMap<String, BTreeMap<String, Vec<String>>> = BTreeMap::new();
-
- for entry in entries {
- // Split full_name into vendor and package parts
- let (vendor, package_name) = match entry.full_name.split_once('/') {
- Some((v, p)) => (v.to_string(), p.to_string()),
- None => (entry.full_name.clone(), entry.full_name.clone()),
- };
-
- let vendor_map = grouped.entry(vendor).or_default();
-
- for link in &entry.links {
- let url = rewrite_github_url(&link.url, link.funding_type.as_deref());
- vendor_map
- .entry(url)
- .or_default()
- .push(package_name.clone());
- }
- }
-
- grouped
-}
-
-fn render_text(
- grouped: &BTreeMap<String, BTreeMap<String, Vec<String>>>,
- console: &console::Console,
-) {
- if grouped.is_empty() {
+fn render_text(fundings: &BTreeMap<String, BTreeMap<String, Vec<String>>>, console: &Console) {
+ if fundings.is_empty() {
console_writeln!(
console,
"No funding links were found in your package dependencies. \
@@ -191,19 +154,18 @@ fn render_text(
"The following packages were found in your dependencies which publish funding information:",
);
- for (vendor, url_map) in grouped {
+ let mut prev: Option<String> = None;
+ for (vendor, url_map) in fundings {
console_writeln!(console, "");
console_writeln!(console, &console_format!("<comment>{vendor}</comment>"));
for (url, packages) in url_map {
- // Deduplicate cross-URL: only print package-names line when it differs from prev
- let mut prev: Option<String> = None;
- let packages_str = packages.join(", ");
- if prev.as_deref() != Some(packages_str.as_str()) {
- console_writeln!(console, &console_format!(" <info>{packages_str}</info>"));
+ let line = format!(" <info>{}</info>", packages.join(", "));
+ if prev.as_deref() != Some(line.as_str()) {
+ console_writeln!(console, &console_format!("{line}"));
+ prev = Some(line);
}
- prev = Some(packages_str);
- let _ = prev;
- console_writeln!(console, &format!(" {url}"));
+ let link = hyperlink(url, url, console.decorated);
+ console_writeln!(console, &format!(" {link}"));
}
}
@@ -216,13 +178,20 @@ fn render_text(
}
fn render_json(
- grouped: &BTreeMap<String, BTreeMap<String, Vec<String>>>,
- console: &console::Console,
+ fundings: &BTreeMap<String, BTreeMap<String, Vec<String>>>,
+ console: &Console,
) -> anyhow::Result<()> {
let buf = Vec::new();
let formatter = serde_json::ser::PrettyFormatter::with_indent(b" ");
let mut ser = serde_json::Serializer::with_formatter(buf, formatter);
- grouped.serialize(&mut ser)?;
+ if fundings.is_empty() {
+ // Composer's `JsonFile::encode([])` emits `[]` (PHP `json_encode` of
+ // an empty native array). Mozart's empty `BTreeMap` would emit `{}`.
+ let empty: Vec<()> = Vec::new();
+ empty.serialize(&mut ser)?;
+ } else {
+ fundings.serialize(&mut ser)?;
+ }
console_writeln!(console, &String::from_utf8(ser.into_inner())?);
Ok(())
}
@@ -239,82 +208,48 @@ mod tests {
}
#[test]
- fn test_extract_funding_links_basic() {
- let json = make_funding_json(&[("github", "https://github.com/Seldaek")]);
- let links = extract_funding_links(&json);
- assert_eq!(links.len(), 1);
- assert_eq!(links[0].url, "https://github.com/Seldaek");
- assert_eq!(links[0].funding_type.as_deref(), Some("github"));
+ fn insert_funding_data_basic() {
+ let mut fundings = BTreeMap::new();
+ let funding = make_funding_json(&[("github", "https://github.com/Seldaek")]);
+ insert_funding_data(&mut fundings, "monolog/monolog", &funding);
+
+ let monolog = fundings.get("monolog").unwrap();
+ let url = "https://github.com/sponsors/Seldaek";
+ let packages = monolog.get(url).unwrap();
+ assert_eq!(packages, &vec!["monolog".to_string()]);
}
#[test]
- fn test_extract_funding_links_missing_url() {
- let json = vec![
+ fn insert_funding_data_skips_empty_url() {
+ let mut fundings = BTreeMap::new();
+ let funding = vec![
serde_json::json!({"type": "github", "url": ""}),
serde_json::json!({"type": "tidelift"}),
serde_json::json!({"type": "github", "url": "https://github.com/user"}),
];
- let links = extract_funding_links(&json);
- // Only the last entry has a non-empty url
- assert_eq!(links.len(), 1);
- assert_eq!(links[0].url, "https://github.com/user");
- }
-
- #[test]
- fn test_extract_funding_links_empty() {
- let links = extract_funding_links(&[]);
- assert!(links.is_empty());
- }
-
- #[test]
- fn test_rewrite_github_url_profile() {
- let result = rewrite_github_url("https://github.com/Seldaek", Some("github"));
- assert_eq!(result, "https://github.com/sponsors/Seldaek");
- }
-
- #[test]
- fn test_rewrite_github_url_already_sponsors() {
- // Has a second path segment, so not rewritten
- let result = rewrite_github_url("https://github.com/sponsors/Seldaek", Some("github"));
- assert_eq!(result, "https://github.com/sponsors/Seldaek");
- }
+ insert_funding_data(&mut fundings, "vendor/pkg", &funding);
- #[test]
- fn test_rewrite_github_url_non_github_type() {
- let result = rewrite_github_url("https://github.com/fabpot", Some("tidelift"));
- assert_eq!(result, "https://github.com/fabpot");
+ let vendor = fundings.get("vendor").unwrap();
+ assert_eq!(vendor.len(), 1);
+ assert!(vendor.contains_key("https://github.com/sponsors/user"));
}
#[test]
- fn test_rewrite_github_url_deep_path() {
- // https://github.com/user/repo has a second path segment
- let result = rewrite_github_url("https://github.com/user/repo", Some("github"));
- assert_eq!(result, "https://github.com/user/repo");
+ fn insert_funding_data_skips_malformed_pretty_name() {
+ let mut fundings = BTreeMap::new();
+ let funding = make_funding_json(&[("github", "https://github.com/user")]);
+ insert_funding_data(&mut fundings, "no-slash-name", &funding);
+ assert!(fundings.is_empty());
}
#[test]
- fn test_group_by_vendor_basic() {
- let entries = vec![
- FundingEntry {
- full_name: "symfony/console".to_string(),
- links: vec![FundingLink {
- url: "https://github.com/fabpot".to_string(),
- funding_type: Some("github".to_string()),
- }],
- },
- FundingEntry {
- full_name: "symfony/http-kernel".to_string(),
- links: vec![FundingLink {
- url: "https://github.com/fabpot".to_string(),
- funding_type: Some("github".to_string()),
- }],
- },
- ];
+ fn insert_funding_data_groups_by_vendor() {
+ let mut fundings = BTreeMap::new();
+ let funding = make_funding_json(&[("github", "https://github.com/fabpot")]);
+ insert_funding_data(&mut fundings, "symfony/console", &funding);
+ insert_funding_data(&mut fundings, "symfony/http-kernel", &funding);
- let grouped = group_by_vendor(&entries);
- assert_eq!(grouped.len(), 1);
- let symfony = grouped.get("symfony").unwrap();
- // URL should be rewritten to sponsors
+ let symfony = fundings.get("symfony").unwrap();
let url = "https://github.com/sponsors/fabpot";
let packages = symfony.get(url).unwrap();
assert_eq!(packages.len(), 2);
@@ -323,24 +258,18 @@ mod tests {
}
#[test]
- fn test_group_by_vendor_multiple_urls() {
- let entries = vec![FundingEntry {
- full_name: "symfony/console".to_string(),
- links: vec![
- FundingLink {
- url: "https://github.com/fabpot".to_string(),
- funding_type: Some("github".to_string()),
- },
- FundingLink {
- url: "https://tidelift.com/funding/github/packagist/symfony/symfony"
- .to_string(),
- funding_type: Some("tidelift".to_string()),
- },
- ],
- }];
+ fn insert_funding_data_multiple_urls() {
+ let mut fundings = BTreeMap::new();
+ let funding = vec![
+ serde_json::json!({"type": "github", "url": "https://github.com/fabpot"}),
+ serde_json::json!({
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony"
+ }),
+ ];
+ insert_funding_data(&mut fundings, "symfony/console", &funding);
- let grouped = group_by_vendor(&entries);
- let symfony = grouped.get("symfony").unwrap();
+ let symfony = fundings.get("symfony").unwrap();
assert_eq!(symfony.len(), 2);
assert!(symfony.contains_key("https://github.com/sponsors/fabpot"));
assert!(
@@ -349,210 +278,69 @@ mod tests {
}
#[test]
- fn test_group_by_vendor_empty() {
- let grouped = group_by_vendor(&[]);
- assert!(grouped.is_empty());
+ fn rewrite_github_url_profile() {
+ let result = rewrite_github_url("https://github.com/Seldaek", Some("github"));
+ assert_eq!(result, "https://github.com/sponsors/Seldaek");
}
#[test]
- fn test_fund_from_lockfile() {
- use mozart_registry::lockfile::{LockFile, LockedPackage};
- use tempfile::tempdir;
-
- let dir = tempdir().unwrap();
- let working_dir = dir.path();
-
- let lock = LockFile {
- readme: LockFile::default_readme(),
- content_hash: "abc123".to_string(),
- packages: vec![
- LockedPackage {
- name: "monolog/monolog".to_string(),
- version: "3.0.0".to_string(),
- version_normalized: None,
- source: None,
- dist: None,
- require: BTreeMap::new(),
- require_dev: BTreeMap::new(),
- conflict: BTreeMap::new(),
- provide: BTreeMap::new(),
- replace: BTreeMap::new(),
- suggest: None,
- package_type: None,
- autoload: None,
- autoload_dev: None,
- license: Some(vec!["MIT".to_string()]),
- description: None,
- homepage: None,
- keywords: None,
- authors: None,
- support: None,
- funding: Some(vec![serde_json::json!({
- "type": "github",
- "url": "https://github.com/Seldaek"
- })]),
- time: None,
- extra_fields: BTreeMap::new(),
- },
- LockedPackage {
- name: "psr/log".to_string(),
- version: "3.0.0".to_string(),
- version_normalized: None,
- source: None,
- dist: None,
- require: BTreeMap::new(),
- require_dev: BTreeMap::new(),
- conflict: BTreeMap::new(),
- provide: BTreeMap::new(),
- replace: BTreeMap::new(),
- suggest: None,
- package_type: None,
- autoload: None,
- autoload_dev: None,
- license: None,
- description: None,
- homepage: None,
- keywords: None,
- authors: None,
- support: None,
- funding: None, // no funding
- time: None,
- extra_fields: BTreeMap::new(),
- },
- ],
- packages_dev: Some(vec![]),
- aliases: vec![],
- minimum_stability: "stable".to_string(),
- stability_flags: serde_json::json!({}),
- prefer_stable: false,
- prefer_lowest: false,
- platform: serde_json::json!({}),
- platform_dev: serde_json::json!({}),
- plugin_api_version: Some("2.6.0".to_string()),
- };
-
- lock.write_to_file(&working_dir.join("composer.lock"))
- .unwrap();
-
- let entries = collect_funding_from_locked(working_dir).unwrap();
- assert_eq!(entries.len(), 1);
- assert_eq!(entries[0].full_name, "monolog/monolog");
- assert_eq!(entries[0].links.len(), 1);
- assert_eq!(entries[0].links[0].url, "https://github.com/Seldaek");
- assert_eq!(entries[0].links[0].funding_type.as_deref(), Some("github"));
+ fn rewrite_github_url_already_sponsors() {
+ let result = rewrite_github_url("https://github.com/sponsors/Seldaek", Some("github"));
+ assert_eq!(result, "https://github.com/sponsors/Seldaek");
}
#[test]
- fn test_fund_from_installed() {
- use tempfile::tempdir;
-
- let dir = tempdir().unwrap();
- let working_dir = dir.path();
- let vendor_dir = working_dir.join("vendor");
-
- let mut installed = mozart_registry::installed::InstalledPackages::new();
-
- let mut extra = BTreeMap::new();
- extra.insert(
- "funding".to_string(),
- serde_json::json!([{
- "type": "github",
- "url": "https://github.com/Seldaek"
- }]),
- );
- installed.upsert(mozart_registry::installed::InstalledPackageEntry {
- name: "monolog/monolog".to_string(),
- version: "3.0.0".to_string(),
- version_normalized: None,
- source: None,
- dist: None,
- package_type: None,
- install_path: None,
- autoload: None,
- aliases: vec![],
- homepage: None,
- support: None,
- extra_fields: extra,
- });
-
- // Package without funding
- installed.upsert(mozart_registry::installed::InstalledPackageEntry {
- name: "psr/log".to_string(),
- version: "3.0.0".to_string(),
- version_normalized: None,
- source: None,
- dist: None,
- package_type: None,
- install_path: None,
- autoload: None,
- aliases: vec![],
- homepage: None,
- support: None,
- extra_fields: BTreeMap::new(),
- });
-
- installed.write(&vendor_dir).unwrap();
-
- let entries = collect_funding_from_installed(working_dir).unwrap();
- assert_eq!(entries.len(), 1);
- assert_eq!(entries[0].full_name, "monolog/monolog");
- assert_eq!(entries[0].links[0].url, "https://github.com/Seldaek");
+ fn rewrite_github_url_non_github_type() {
+ let result = rewrite_github_url("https://github.com/fabpot", Some("tidelift"));
+ assert_eq!(result, "https://github.com/fabpot");
}
#[test]
- fn test_fund_no_funding_data() {
- use mozart_registry::lockfile::{LockFile, LockedPackage};
- use tempfile::tempdir;
+ fn rewrite_github_url_deep_path() {
+ let result = rewrite_github_url("https://github.com/user/repo", Some("github"));
+ assert_eq!(result, "https://github.com/user/repo");
+ }
- let dir = tempdir().unwrap();
- let working_dir = dir.path();
+ #[test]
+ fn rewrite_github_url_missing_type() {
+ let result = rewrite_github_url("https://github.com/user", None);
+ assert_eq!(result, "https://github.com/user");
+ }
- let lock = LockFile {
- readme: LockFile::default_readme(),
- content_hash: "abc123".to_string(),
- packages: vec![LockedPackage {
- name: "psr/log".to_string(),
- version: "3.0.0".to_string(),
- version_normalized: None,
- source: None,
- dist: None,
- require: BTreeMap::new(),
- require_dev: BTreeMap::new(),
- conflict: BTreeMap::new(),
- provide: BTreeMap::new(),
- replace: BTreeMap::new(),
- suggest: None,
- package_type: None,
- autoload: None,
- autoload_dev: None,
- license: None,
- description: None,
- homepage: None,
- keywords: None,
- authors: None,
- support: None,
- funding: None,
- time: None,
- extra_fields: BTreeMap::new(),
- }],
- packages_dev: Some(vec![]),
- aliases: vec![],
- minimum_stability: "stable".to_string(),
- stability_flags: serde_json::json!({}),
- prefer_stable: false,
- prefer_lowest: false,
- platform: serde_json::json!({}),
- platform_dev: serde_json::json!({}),
- plugin_api_version: Some("2.6.0".to_string()),
- };
+ #[test]
+ fn render_json_empty_emits_array() {
+ // Composer's `JsonFile::encode([])` emits `[]`; ensure Mozart matches
+ // rather than serializing the empty BTreeMap to `{}`.
+ let console = Console::new(0, false, false, false, true);
+ let fundings: BTreeMap<String, BTreeMap<String, Vec<String>>> = BTreeMap::new();
- lock.write_to_file(&working_dir.join("composer.lock"))
- .unwrap();
+ let buf = Vec::new();
+ let formatter = serde_json::ser::PrettyFormatter::with_indent(b" ");
+ let mut ser = serde_json::Serializer::with_formatter(buf, formatter);
+ if fundings.is_empty() {
+ let empty: Vec<()> = Vec::new();
+ empty.serialize(&mut ser).unwrap();
+ } else {
+ fundings.serialize(&mut ser).unwrap();
+ }
+ let out = String::from_utf8(ser.into_inner()).unwrap();
+ assert_eq!(out, "[]");
+ let _ = console;
+ }
- let entries = collect_funding_from_locked(working_dir).unwrap();
- assert!(entries.is_empty());
+ #[test]
+ fn render_json_non_empty_is_object() {
+ let mut fundings: BTreeMap<String, BTreeMap<String, Vec<String>>> = BTreeMap::new();
+ let funding = make_funding_json(&[("github", "https://github.com/Seldaek")]);
+ insert_funding_data(&mut fundings, "monolog/monolog", &funding);
- let grouped = group_by_vendor(&entries);
- assert!(grouped.is_empty());
+ let buf = Vec::new();
+ let formatter = serde_json::ser::PrettyFormatter::with_indent(b" ");
+ let mut ser = serde_json::Serializer::with_formatter(buf, formatter);
+ fundings.serialize(&mut ser).unwrap();
+ let out = String::from_utf8(ser.into_inner()).unwrap();
+ assert!(out.starts_with('{'));
+ assert!(out.contains("monolog"));
+ assert!(out.contains("https://github.com/sponsors/Seldaek"));
}
}