wasmer_cli/commands/package/
search.rs

1//! Search for packages in the registry.
2
3use wasmer_backend_api::types::{DateTime, SearchPackageVersion};
4use wasmer_sdk::package::search::{
5    CountComparison, CountFilter, PackageOrderBy, PackagesFilter, SearchOptions, SearchOrderSort,
6    SearchPublishDate,
7};
8
9use crate::{
10    commands::AsyncCliCommand, config::WasmerEnv, opts::ListFormatOpts, utils::render::ListFormat,
11};
12
13const NO_PACKAGES_FOUND_MESSAGE: &str = "No packages found.";
14
15/// Field to order results by.
16#[derive(clap::ValueEnum, Clone, Copy, Debug)]
17enum OrderBy {
18    Alphabetically,
19    Size,
20    Downloads,
21    Published,
22    Created,
23    Likes,
24}
25
26impl From<OrderBy> for PackageOrderBy {
27    fn from(value: OrderBy) -> Self {
28        match value {
29            OrderBy::Alphabetically => PackageOrderBy::Alphabetically,
30            OrderBy::Size => PackageOrderBy::Size,
31            OrderBy::Downloads => PackageOrderBy::TotalDownloads,
32            OrderBy::Published => PackageOrderBy::PublishedDate,
33            OrderBy::Created => PackageOrderBy::CreatedDate,
34            OrderBy::Likes => PackageOrderBy::TotalLikes,
35        }
36    }
37}
38
39/// Sort direction.
40#[derive(clap::ValueEnum, Clone, Copy, Debug)]
41enum Sort {
42    Asc,
43    Desc,
44}
45
46impl From<Sort> for SearchOrderSort {
47    fn from(value: Sort) -> Self {
48        match value {
49            Sort::Asc => SearchOrderSort::Asc,
50            Sort::Desc => SearchOrderSort::Desc,
51        }
52    }
53}
54
55/// Relative window for the last-published date filter.
56#[derive(clap::ValueEnum, Clone, Copy, Debug)]
57#[allow(clippy::enum_variant_names)] // `last-day`/`last-week`/... are the desired CLI values
58enum PublishedWithin {
59    LastDay,
60    LastWeek,
61    LastMonth,
62    LastYear,
63}
64
65impl From<PublishedWithin> for SearchPublishDate {
66    fn from(value: PublishedWithin) -> Self {
67        match value {
68            PublishedWithin::LastDay => SearchPublishDate::LastDay,
69            PublishedWithin::LastWeek => SearchPublishDate::LastWeek,
70            PublishedWithin::LastMonth => SearchPublishDate::LastMonth,
71            PublishedWithin::LastYear => SearchPublishDate::LastYear,
72        }
73    }
74}
75
76/// A `count >= n` filter (the common case for download/like/size thresholds).
77fn at_least(count: i32) -> CountFilter {
78    CountFilter {
79        count: Some(count),
80        comparison: Some(CountComparison::GreaterThanOrEqual),
81    }
82}
83
84/// Validate an RFC3339 timestamp at parse time so a bad value is a clap error
85/// rather than a cryptic backend scalar rejection.
86fn parse_rfc3339(value: &str) -> Result<DateTime, String> {
87    time::OffsetDateTime::parse(value, &time::format_description::well_known::Rfc3339)
88        .map(|_| DateTime(value.to_string()))
89        .map_err(|e| {
90            format!("`{value}` is not an RFC3339 timestamp (e.g. 2024-01-01T00:00:00Z): {e}")
91        })
92}
93
94fn render_search_results(format: ListFormat, results: &[SearchPackageVersion]) -> String {
95    if results.is_empty() && matches!(format, ListFormat::Table | ListFormat::ItemTable) {
96        NO_PACKAGES_FOUND_MESSAGE.to_string()
97    } else {
98        format.render(results)
99    }
100}
101
102/// Search for packages in the registry.
103#[derive(clap::Parser, Debug)]
104pub struct PackageSearch {
105    #[clap(flatten)]
106    fmt: ListFormatOpts,
107
108    #[clap(flatten)]
109    env: WasmerEnv,
110
111    /// Only show packages owned by this user or namespace.
112    #[clap(long)]
113    owner: Option<String>,
114
115    /// Only show packages published by this user.
116    #[clap(long)]
117    published_by: Option<String>,
118
119    /// Filter by curated status. `--curated` for curated only, `--curated=false`
120    /// for non-curated.
121    #[clap(long, num_args = 0..=1, default_missing_value = "true", require_equals = true)]
122    curated: Option<bool>,
123
124    /// Filter by deployable status. `--deployable` for deployable only,
125    /// `--deployable=false` for non-deployable.
126    #[clap(long, num_args = 0..=1, default_missing_value = "true", require_equals = true)]
127    deployable: Option<bool>,
128
129    /// Only show packages whose latest version has bindings.
130    #[clap(long)]
131    has_bindings: bool,
132
133    /// Only show packages whose latest version has commands.
134    #[clap(long)]
135    has_commands: bool,
136
137    /// Only show standalone packages (implies --has-commands).
138    #[clap(long)]
139    standalone: bool,
140
141    /// Only show packages exposing this interface (repeatable).
142    #[clap(long = "interface", value_name = "NAME")]
143    interfaces: Vec<String>,
144
145    /// Only show packages whose latest version's license matches (substring).
146    #[clap(long)]
147    license: Option<String>,
148
149    /// Only show packages with at least this many downloads.
150    #[clap(long, value_parser = clap::value_parser!(i32).range(0..))]
151    min_downloads: Option<i32>,
152
153    /// Only show packages with at least this many likes.
154    #[clap(long, value_parser = clap::value_parser!(i32).range(0..))]
155    min_likes: Option<i32>,
156
157    /// Only show packages whose latest version is at least this many bytes.
158    #[clap(long, value_parser = clap::value_parser!(i32).range(0..))]
159    min_size: Option<i32>,
160
161    /// Only show packages created on or after this RFC3339 timestamp
162    /// (e.g. 2024-01-01T00:00:00Z).
163    #[clap(long, value_parser = parse_rfc3339)]
164    created_after: Option<DateTime>,
165
166    /// Only show packages created on or before this RFC3339 timestamp.
167    #[clap(long, value_parser = parse_rfc3339)]
168    created_before: Option<DateTime>,
169
170    /// Only show packages last published on or after this RFC3339 timestamp.
171    #[clap(long, value_parser = parse_rfc3339)]
172    published_after: Option<DateTime>,
173
174    /// Only show packages last published on or before this RFC3339 timestamp.
175    #[clap(long, value_parser = parse_rfc3339)]
176    published_before: Option<DateTime>,
177
178    /// Only show packages published within the given window.
179    #[clap(long, value_enum)]
180    published_within: Option<PublishedWithin>,
181
182    /// Field to order results by.
183    #[clap(long, value_enum, default_value = "published")]
184    order_by: OrderBy,
185
186    /// Sort direction.
187    #[clap(long, value_enum, default_value = "desc")]
188    sort: Sort,
189
190    /// Maximum number of results to display.
191    #[clap(long, default_value = "50")]
192    max: usize,
193
194    /// The search query. Leave empty to list all (matching) packages.
195    #[clap(default_value = "")]
196    query: String,
197}
198
199#[async_trait::async_trait]
200impl AsyncCliCommand for PackageSearch {
201    type Output = ();
202
203    async fn run_async(self) -> Result<(), anyhow::Error> {
204        let client = self.env.client_unauthennticated()?;
205
206        let with_interfaces =
207            (!self.interfaces.is_empty()).then(|| self.interfaces.into_iter().map(Some).collect());
208
209        let filter = PackagesFilter {
210            owner: self.owner,
211            published_by: self.published_by,
212            curated: self.curated,
213            deployable: self.deployable,
214            has_bindings: self.has_bindings.then_some(true),
215            has_commands: self.has_commands.then_some(true),
216            is_standalone: self.standalone.then_some(true),
217            with_interfaces,
218            license: self.license,
219            downloads: self.min_downloads.map(at_least),
220            likes: self.min_likes.map(at_least),
221            size: self.min_size.map(at_least),
222            created_after: self.created_after,
223            created_before: self.created_before,
224            last_published_after: self.published_after,
225            last_published_before: self.published_before,
226            publish_date: self.published_within.map(Into::into),
227            order_by: Some(self.order_by.into()),
228            sort_by: Some(self.sort.into()),
229            ..Default::default()
230        };
231
232        let results = wasmer_sdk::package::search::search_packages(
233            &client,
234            SearchOptions {
235                query: self.query,
236                filter,
237                limit: Some(self.max),
238            },
239        )
240        .await?;
241
242        println!(
243            "{}",
244            render_search_results(self.fmt.format, results.as_slice())
245        );
246
247        Ok(())
248    }
249}