aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart/src/commands/search.rs
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-02-21 15:19:18 +0900
committernsfisis <nsfisis@gmail.com>2026-02-21 15:19:18 +0900
commitc04744719bd16d9414a9f9a358691d03a993670c (patch)
tree2354da304c97094bfbe23bb38ceba08d60ea19a2 /crates/mozart/src/commands/search.rs
parentc07e2073e0484924f80f3bb68ea95ce127b42df6 (diff)
downloadphp-mozart-c04744719bd16d9414a9f9a358691d03a993670c.tar.gz
php-mozart-c04744719bd16d9414a9f9a358691d03a993670c.tar.zst
php-mozart-c04744719bd16d9414a9f9a358691d03a993670c.zip
feat(require,remove): add interactive search and dependency-aware partial updates
Implement Phase 5.5 of the require/remove commands: - Interactive package search when no packages specified on CLI (require) - --with-dependencies/--with-all-dependencies partial update for require - --with-all-dependencies/--no-update-with-dependencies for remove - --minimal-changes support for remove - Extract search API types and logic from search.rs into packagist.rs for reuse by both search and require commands Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart/src/commands/search.rs')
-rw-r--r--crates/mozart/src/commands/search.rs91
1 files changed, 6 insertions, 85 deletions
diff --git a/crates/mozart/src/commands/search.rs b/crates/mozart/src/commands/search.rs
index 5b35a20..e14c4ff 100644
--- a/crates/mozart/src/commands/search.rs
+++ b/crates/mozart/src/commands/search.rs
@@ -1,5 +1,5 @@
+use crate::packagist::SearchResult;
use clap::Args;
-use serde::{Deserialize, Serialize};
#[derive(Args)]
pub struct SearchArgs {
@@ -24,26 +24,6 @@ pub struct SearchArgs {
pub format: Option<String>,
}
-/// Maximum number of pages to fetch from the Packagist search API.
-const MAX_PAGES: usize = 20;
-
-#[derive(Debug, Deserialize)]
-struct SearchResponse {
- results: Vec<SearchResult>,
- total: u64,
- next: Option<String>,
-}
-
-#[derive(Debug, Deserialize, Serialize, Clone)]
-struct SearchResult {
- name: String,
- description: String,
- url: String,
- repository: Option<String>,
- downloads: u64,
- favers: u64,
-}
-
/// Format a large count as a human-readable string (e.g. 1500 -> "1.5K", 2500000 -> "2.5M").
fn format_count(n: u64) -> String {
if n >= 1_000_000 {
@@ -79,73 +59,10 @@ fn passes_only_vendor(result: &SearchResult, query: &str) -> bool {
vendor.eq_ignore_ascii_case(query)
}
-/// Percent-encode a string for use in a URL query parameter value.
-/// Encodes spaces as `%20` and other reserved/non-ASCII characters.
-fn url_encode(s: &str) -> String {
- let mut encoded = String::with_capacity(s.len());
- for byte in s.bytes() {
- match byte {
- b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
- encoded.push(byte as char);
- }
- b' ' => encoded.push_str("%20"),
- other => {
- encoded.push_str(&format!("%{other:02X}"));
- }
- }
- }
- encoded
-}
-
pub fn execute(args: &SearchArgs, _cli: &super::Cli) -> anyhow::Result<()> {
let query = args.tokens.join(" ");
- let client = reqwest::blocking::Client::builder()
- .user_agent("mozart/0.1.0")
- .build()?;
-
- let mut all_results: Vec<SearchResult> = Vec::new();
- let mut page = 1usize;
- let mut next_url: Option<String> = None;
- let mut total: u64 = 0;
-
- loop {
- let response: SearchResponse = if let Some(ref url) = next_url {
- // Packagist gives us the full next URL; just fetch it
- let resp = client.get(url).send()?;
- if !resp.status().is_success() {
- anyhow::bail!("Packagist search request failed (HTTP {})", resp.status());
- }
- resp.json()?
- } else {
- // Build the first request URL with query parameters encoded manually
- let encoded_query = url_encode(&query);
- let mut url = format!("https://packagist.org/search.json?q={encoded_query}");
- if let Some(ref t) = args.r#type {
- url.push_str("&type=");
- url.push_str(&url_encode(t));
- }
-
- let resp = client.get(&url).send()?;
-
- if !resp.status().is_success() {
- anyhow::bail!("Packagist search request failed (HTTP {})", resp.status());
- }
- resp.json()?
- };
-
- if page == 1 {
- total = response.total;
- }
-
- all_results.extend(response.results);
- next_url = response.next;
- page += 1;
-
- if next_url.is_none() || page > MAX_PAGES {
- break;
- }
- }
+ let (all_results, total) = crate::packagist::search_packages(&query, args.r#type.as_deref())?;
// Apply client-side filters
let mut results: Vec<&SearchResult> = all_results.iter().collect();
@@ -242,6 +159,8 @@ mod tests {
#[test]
fn test_parse_search_response() {
+ use crate::packagist::SearchResponse;
+
let json = r#"{
"results": [
{
@@ -286,6 +205,8 @@ mod tests {
#[test]
fn test_parse_search_response_with_next() {
+ use crate::packagist::SearchResponse;
+
let json = r#"{
"results": [],
"total": 100,