diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-08 03:12:05 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-08 03:12:05 +0900 |
| commit | e6f35b567564665c6cb741a06e4c4afcdc5ab317 (patch) | |
| tree | a1ae0e55afab95598ff0f6bec5b6a2e9f4f86cc0 /crates/mozart-registry/src/repository/packagist_repo.rs | |
| parent | b3fcdf3b5cf0d6b43109c4bcb3dfcbb6576abce5 (diff) | |
| download | php-mozart-e6f35b567564665c6cb741a06e4c4afcdc5ab317.tar.gz php-mozart-e6f35b567564665c6cb741a06e4c4afcdc5ab317.tar.zst php-mozart-e6f35b567564665c6cb741a06e4c4afcdc5ab317.zip | |
fix(search): align with Composer's RepositoryInterface::search
Replace the HTTP-only post-filtered implementation with a Repository::search
trait dispatch that mirrors ComposerRepository::search semantics for all
three modes (FULLTEXT/NAME/VENDOR). --only-name now does an OR-of-tokens
regex match against the full Packagist list.json index instead of a
substring match against a fulltext page, so e.g. \`mozart search --only-name
mono log\` matches \`monolog/monolog\` like Composer does. Other parity
fixes: regex::escape on non-fulltext queries, format check before mutex
check, 4-space JSON indent, OSC 8 terminal hyperlink emission when a
result has a url, <warning>\! Abandoned \!</warning> styling on abandoned
rows, and the Mozart-only "No packages found" warning is dropped to match
Composer's silent empty-result behavior.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart-registry/src/repository/packagist_repo.rs')
| -rw-r--r-- | crates/mozart-registry/src/repository/packagist_repo.rs | 66 |
1 files changed, 65 insertions, 1 deletions
diff --git a/crates/mozart-registry/src/repository/packagist_repo.rs b/crates/mozart-registry/src/repository/packagist_repo.rs index 6f9b687..fa656b7 100644 --- a/crates/mozart-registry/src/repository/packagist_repo.rs +++ b/crates/mozart-registry/src/repository/packagist_repo.rs @@ -5,9 +5,10 @@ //! direct call. Construction takes ownership of the [`Cache`] handle so //! callers no longer thread it through `ResolveRequest` / `LockFileGenerationRequest`. -use super::{LoadResult, NamedPackagistVersion, PackageQuery, Repository}; +use super::{LoadResult, NamedPackagistVersion, PackageQuery, Repository, SearchMode}; use crate::cache::Cache; use crate::packagist; +use crate::packagist::SearchResult; pub struct PackagistRepository { id: String, @@ -54,4 +55,67 @@ impl Repository for PackagistRepository { } Ok(result) } + + async fn search( + &self, + query: &str, + mode: SearchMode, + package_type: Option<&str>, + ) -> anyhow::Result<Vec<SearchResult>> { + match mode { + SearchMode::Fulltext => { + let (results, _total) = packagist::search_packages(query, package_type).await?; + Ok(results) + } + SearchMode::Name => { + let pattern = build_name_regex(query)?; + let names = packagist::fetch_package_names(package_type, &self.cache).await?; + Ok(names + .into_iter() + .filter(|name| pattern.is_match(name)) + .map(empty_search_result) + .collect()) + } + SearchMode::Vendor => { + let pattern = build_name_regex(query)?; + let vendors = packagist::fetch_vendor_names(&self.cache).await?; + Ok(vendors + .into_iter() + .filter(|name| pattern.is_match(name)) + .map(empty_search_result) + .collect()) + } + } + } +} + +/// Build the case-insensitive `(?:t1|t2|...)` regex from whitespace-split +/// tokens, mirroring Composer's `'{(?:'.implode('|', $matches).')}i'`. +/// +/// Tokens are joined as-is — callers are expected to have already escaped +/// regex metacharacters (`SearchCommand` calls `preg_quote`; Mozart calls +/// `regex::escape` before reaching this point). +fn build_name_regex(query: &str) -> anyhow::Result<regex::Regex> { + let tokens: Vec<&str> = query.split_whitespace().collect(); + let body = if tokens.is_empty() { + String::new() + } else { + tokens.join("|") + }; + Ok(regex::Regex::new(&format!("(?i)(?:{body})"))?) +} + +/// Build a [`SearchResult`] with only `name` populated, mirroring the shape +/// Composer returns for `SEARCH_NAME` / `SEARCH_VENDOR` modes +/// (`['name' => $name]`, all other fields `null`). +fn empty_search_result(name: String) -> SearchResult { + SearchResult { + name, + description: String::new(), + url: String::new(), + repository: None, + downloads: 0, + favers: 0, + abandoned: None, + } } |
