Skip to main content

deps_core/
macros.rs

1//! Macro utilities for reducing boilerplate in ecosystem implementations.
2//!
3//! Provides macros for implementing common traits with minimal code duplication.
4
5/// Implement the `Dependency` trait for a struct.
6///
7/// # Arguments
8///
9/// * `$type` - The struct type name
10/// * `name` - Field name for the dependency name (`PackageName`)
11/// * `name_range` - Field name for the name range (`Range`)
12/// * `version` - Field name for version requirement (`Option<VersionReq>`)
13/// * `version_range` - Field name for version range (`Option<Range>`)
14/// * `source` - Optional: field name for dependency source (`DependencySource`), cloned
15///   (defaults to the constant `DependencySource::Registry` when omitted). A *field name*
16///   rather than an arbitrary expression: a `$source:expr` fragment can express the same
17///   thing as a closure (`impl_version!`'s `status`/`prerelease` arguments use exactly that
18///   pattern), but a closure returning a *borrow* out of `self` (as `source()`'s caller-side
19///   equivalent would need for, e.g., a non-cloning accessor) hits a real lifetime footgun —
20///   the closure's return type can't name a lifetime tied to its `&self` parameter without
21///   HRTB ceremony the call sites here shouldn't have to write. A bare field identifier,
22///   substituted into the generated method's own `self.$field`, sidesteps that entirely and
23///   keeps every call site a one-line field name — the same reasoning that makes `name`,
24///   `name_range`, `version` and `version_range` above plain idents rather than expressions.
25/// * `version_literal` - Optional, requires `source` to also be given (fields are matched in
26///   this exact order — `version_literal` alone, or before `source`, matches no arm): field
27///   name for [`Dependency::version_literal`](crate::ecosystem::Dependency::version_literal)
28///   (`Option<String>`), read via `.as_deref()` (defaults to the trait's `None` when omitted)
29///
30/// # Examples
31///
32/// ```ignore
33/// use deps_core::{impl_dependency, PackageName, VersionReq};
34///
35/// pub struct MyDependency {
36///     pub name: PackageName,
37///     pub name_range: Range,
38///     pub version_req: Option<VersionReq>,
39///     pub version_range: Option<Range>,
40/// }
41///
42/// impl_dependency!(MyDependency {
43///     name: name,
44///     name_range: name_range,
45///     version: version_req,
46///     version_range: version_range,
47/// });
48/// ```
49#[macro_export]
50macro_rules! impl_dependency {
51    ($type:ty {
52        name: $name:ident,
53        name_range: $name_range:ident,
54        version: $version:ident,
55        version_range: $version_range:ident $(,)?
56    }) => {
57        impl $crate::ecosystem::Dependency for $type {
58            fn name(&self) -> &$crate::PackageName {
59                &self.$name
60            }
61
62            fn name_range(&self) -> $crate::position::Range {
63                self.$name_range
64            }
65
66            fn version_requirement(&self) -> Option<&$crate::VersionReq> {
67                self.$version.as_ref()
68            }
69
70            fn version_range(&self) -> Option<$crate::position::Range> {
71                self.$version_range
72            }
73
74            fn source(&self) -> $crate::parser::DependencySource {
75                $crate::parser::DependencySource::Registry
76            }
77
78            fn as_any(&self) -> &dyn ::std::any::Any {
79                self
80            }
81        }
82    };
83    ($type:ty {
84        name: $name:ident,
85        name_range: $name_range:ident,
86        version: $version:ident,
87        version_range: $version_range:ident,
88        source: $source:ident $(,)?
89    }) => {
90        impl $crate::ecosystem::Dependency for $type {
91            fn name(&self) -> &$crate::PackageName {
92                &self.$name
93            }
94
95            fn name_range(&self) -> $crate::position::Range {
96                self.$name_range
97            }
98
99            fn version_requirement(&self) -> Option<&$crate::VersionReq> {
100                self.$version.as_ref()
101            }
102
103            fn version_range(&self) -> Option<$crate::position::Range> {
104                self.$version_range
105            }
106
107            fn source(&self) -> $crate::parser::DependencySource {
108                self.$source.clone()
109            }
110
111            fn as_any(&self) -> &dyn ::std::any::Any {
112                self
113            }
114        }
115    };
116    ($type:ty {
117        name: $name:ident,
118        name_range: $name_range:ident,
119        version: $version:ident,
120        version_range: $version_range:ident,
121        source: $source:ident,
122        version_literal: $version_literal:ident $(,)?
123    }) => {
124        impl $crate::ecosystem::Dependency for $type {
125            fn name(&self) -> &$crate::PackageName {
126                &self.$name
127            }
128
129            fn name_range(&self) -> $crate::position::Range {
130                self.$name_range
131            }
132
133            fn version_requirement(&self) -> Option<&$crate::VersionReq> {
134                self.$version.as_ref()
135            }
136
137            fn version_range(&self) -> Option<$crate::position::Range> {
138                self.$version_range
139            }
140
141            fn source(&self) -> $crate::parser::DependencySource {
142                self.$source.clone()
143            }
144
145            fn version_literal(&self) -> Option<&str> {
146                self.$version_literal.as_deref()
147            }
148
149            fn as_any(&self) -> &dyn ::std::any::Any {
150                self
151            }
152        }
153    };
154}
155
156/// Implement `Version` trait for a struct.
157///
158/// # Arguments
159///
160/// * `$type` - The struct type name
161/// * `version` - Field name for version string (`ConcreteVersion`)
162/// * `status` - Expression evaluating to a closure `Fn(&$type) -> RemovalStatus`,
163///   restating at every declaration site whether the ecosystem's flag is a hard
164///   removal ([`RemovalStatus::from_yanked`](crate::RemovalStatus::from_yanked)) or an
165///   advisory one ([`RemovalStatus::from_advisory`](crate::RemovalStatus::from_advisory))
166///   — see [`RemovalStatus`](crate::RemovalStatus).
167/// * `published_at` - Optional: field name for publish timestamp
168///   (`Option<PublishTime>`), already parsed eagerly at construction
169/// * `prerelease` - Optional: expression evaluating to a closure
170///   `Fn(&$type) -> bool`, used when the ecosystem has its own reliable
171///   prerelease signal (e.g. a semver-parsed `pre` component) instead of
172///   falling back to the trait's default hyphen-substring heuristic.
173///   Omitting this arm silently installs the default heuristic, with no
174///   compile-time signal that it may be wrong for the ecosystem — only omit
175///   it when the default is provably correct for the registry's version
176///   format (as of #322, `deps-composer` is the sole deliberate holdout,
177///   since Packagist versions aren't strict semver).
178/// * `deprecation` - Optional, requires both `published_at` and `prerelease` to also be
179///   given: expression evaluating to a closure `Fn(&$type) -> Option<&Deprecation>`,
180///   for an ecosystem whose registry exposes a package-level deprecation payload
181///   (issue #205). Omitting this arm installs the trait default (`None`).
182///
183/// # Examples
184///
185/// ```ignore
186/// use deps_core::{ConcreteVersion, impl_version, RemovalStatus};
187///
188/// pub struct MyVersion {
189///     pub version: ConcreteVersion,
190///     pub deprecated: bool,
191/// }
192///
193/// impl_version!(MyVersion {
194///     version: version,
195///     status: |v: &MyVersion| RemovalStatus::from_advisory(v.deprecated),
196/// });
197/// ```
198///
199/// With a publish timestamp:
200///
201/// ```ignore
202/// use deps_core::{ConcreteVersion, impl_version, PublishTime, RemovalStatus};
203///
204/// pub struct MyVersion {
205///     pub version: ConcreteVersion,
206///     pub deprecated: bool,
207///     pub published_at: Option<PublishTime>,
208/// }
209///
210/// impl_version!(MyVersion {
211///     version: version,
212///     status: |v: &MyVersion| RemovalStatus::from_advisory(v.deprecated),
213///     published_at: published_at,
214/// });
215/// ```
216///
217/// With a structured prerelease signal:
218///
219/// ```ignore
220/// use deps_core::{ConcreteVersion, impl_version, RemovalStatus};
221///
222/// pub struct MyVersion {
223///     pub version: ConcreteVersion,
224///     pub deprecated: bool,
225/// }
226///
227/// impl_version!(MyVersion {
228///     version: version,
229///     status: |v: &MyVersion| RemovalStatus::from_advisory(v.deprecated),
230///     prerelease: |v: &MyVersion| v.version.as_str().contains("-pre"),
231/// });
232/// ```
233#[macro_export]
234macro_rules! impl_version {
235    ($type:ty {
236        version: $version:ident,
237        status: $status:expr $(,)?
238    }) => {
239        impl $crate::registry::Version for $type {
240            fn version_string(&self) -> &$crate::ConcreteVersion {
241                &self.$version
242            }
243
244            fn removal_status(&self) -> $crate::registry::RemovalStatus {
245                ($status)(self)
246            }
247
248            fn as_any(&self) -> &dyn ::std::any::Any {
249                self
250            }
251        }
252    };
253    ($type:ty {
254        version: $version:ident,
255        status: $status:expr,
256        published_at: $published_at:ident $(,)?
257    }) => {
258        impl $crate::registry::Version for $type {
259            fn version_string(&self) -> &$crate::ConcreteVersion {
260                &self.$version
261            }
262
263            fn removal_status(&self) -> $crate::registry::RemovalStatus {
264                ($status)(self)
265            }
266
267            fn published_at(&self) -> Option<$crate::freshness::PublishTime> {
268                self.$published_at
269            }
270
271            fn as_any(&self) -> &dyn ::std::any::Any {
272                self
273            }
274        }
275    };
276    ($type:ty {
277        version: $version:ident,
278        status: $status:expr,
279        prerelease: $prerelease:expr $(,)?
280    }) => {
281        impl $crate::registry::Version for $type {
282            fn version_string(&self) -> &$crate::ConcreteVersion {
283                &self.$version
284            }
285
286            fn removal_status(&self) -> $crate::registry::RemovalStatus {
287                ($status)(self)
288            }
289
290            fn is_prerelease(&self) -> bool {
291                ($prerelease)(self)
292            }
293
294            fn as_any(&self) -> &dyn ::std::any::Any {
295                self
296            }
297        }
298    };
299    ($type:ty {
300        version: $version:ident,
301        status: $status:expr,
302        published_at: $published_at:ident,
303        prerelease: $prerelease:expr $(,)?
304    }) => {
305        impl $crate::registry::Version for $type {
306            fn version_string(&self) -> &$crate::ConcreteVersion {
307                &self.$version
308            }
309
310            fn removal_status(&self) -> $crate::registry::RemovalStatus {
311                ($status)(self)
312            }
313
314            fn published_at(&self) -> Option<$crate::freshness::PublishTime> {
315                self.$published_at
316            }
317
318            fn is_prerelease(&self) -> bool {
319                ($prerelease)(self)
320            }
321
322            fn as_any(&self) -> &dyn ::std::any::Any {
323                self
324            }
325        }
326    };
327    ($type:ty {
328        version: $version:ident,
329        status: $status:expr,
330        published_at: $published_at:ident,
331        prerelease: $prerelease:expr,
332        deprecation: $deprecation:expr $(,)?
333    }) => {
334        impl $crate::registry::Version for $type {
335            fn version_string(&self) -> &$crate::ConcreteVersion {
336                &self.$version
337            }
338
339            fn removal_status(&self) -> $crate::registry::RemovalStatus {
340                ($status)(self)
341            }
342
343            fn published_at(&self) -> Option<$crate::freshness::PublishTime> {
344                self.$published_at
345            }
346
347            fn is_prerelease(&self) -> bool {
348                ($prerelease)(self)
349            }
350
351            fn deprecation(&self) -> Option<&$crate::registry::Deprecation> {
352                // Coerced through a plain `fn` pointer (rather than calling the closure
353                // expression directly) so it is forced to be `for<'a> Fn(&'a Self) ->
354                // Option<&'a Deprecation>` — a bare closure literal here infers a single
355                // concrete lifetime from its first call site instead of generalizing,
356                // which fails to typecheck against `self`'s elided lifetime.
357                let f: fn(&$type) -> Option<&$crate::registry::Deprecation> = $deprecation;
358                f(self)
359            }
360
361            fn as_any(&self) -> &dyn ::std::any::Any {
362                self
363            }
364        }
365    };
366    ($type:ty {
367        version: $version:ident,
368        status: $status:expr,
369        published_at: $published_at:ident,
370        prerelease: $prerelease:expr,
371        deprecation: $deprecation:expr,
372        license: $license:ident $(,)?
373    }) => {
374        impl $crate::registry::Version for $type {
375            fn version_string(&self) -> &$crate::ConcreteVersion {
376                &self.$version
377            }
378
379            fn removal_status(&self) -> $crate::registry::RemovalStatus {
380                ($status)(self)
381            }
382
383            fn published_at(&self) -> Option<$crate::freshness::PublishTime> {
384                self.$published_at
385            }
386
387            fn is_prerelease(&self) -> bool {
388                ($prerelease)(self)
389            }
390
391            fn deprecation(&self) -> Option<&$crate::registry::Deprecation> {
392                let f: fn(&$type) -> Option<&$crate::registry::Deprecation> = $deprecation;
393                f(self)
394            }
395
396            fn license(&self) -> &[String] {
397                &self.$license
398            }
399
400            fn as_any(&self) -> &dyn ::std::any::Any {
401                self
402            }
403        }
404    };
405}
406
407/// Implement `Metadata` trait for a struct.
408///
409/// # Arguments
410///
411/// * `$type` - The struct type name
412/// * `name` - Field name for package name (`PackageName`)
413/// * `description` - Field name for description (`Option<String>`)
414/// * `repository` - Field name for repository (`Option<String>`)
415/// * `documentation` - Field name for documentation URL (`Option<String>`)
416/// * `latest_version` - Field name for latest version (`ConcreteVersion`)
417///
418/// # Examples
419///
420/// ```ignore
421/// use deps_core::{ConcreteVersion, PackageName, impl_metadata};
422///
423/// pub struct MyPackage {
424///     pub name: PackageName,
425///     pub description: Option<String>,
426///     pub repository: Option<String>,
427///     pub homepage: Option<String>,
428///     pub latest_version: ConcreteVersion,
429/// }
430///
431/// impl_metadata!(MyPackage {
432///     name: name,
433///     description: description,
434///     repository: repository,
435///     documentation: homepage,
436///     latest_version: latest_version,
437/// });
438/// ```
439#[macro_export]
440macro_rules! impl_metadata {
441    ($type:ty {
442        name: $name:ident,
443        description: $description:ident,
444        repository: $repository:ident,
445        documentation: $documentation:ident,
446        latest_version: $latest_version:ident $(,)?
447    }) => {
448        impl $crate::registry::Metadata for $type {
449            fn name(&self) -> &$crate::PackageName {
450                &self.$name
451            }
452
453            fn description(&self) -> Option<&str> {
454                self.$description.as_deref()
455            }
456
457            fn repository(&self) -> Option<&str> {
458                self.$repository.as_deref()
459            }
460
461            fn documentation(&self) -> Option<&str> {
462                self.$documentation.as_deref()
463            }
464
465            fn latest_version(&self) -> &$crate::ConcreteVersion {
466                &self.$latest_version
467            }
468
469            fn as_any(&self) -> &dyn ::std::any::Any {
470                self
471            }
472        }
473    };
474}
475
476/// Emits [`impl_parse_result!`]'s `workspace_root` method body — not part of the public
477/// macro API (call [`impl_parse_result!`] instead), but must be `#[macro_export]`ed like any
478/// other macro invoked from another crate's expansion.
479#[doc(hidden)]
480#[macro_export]
481macro_rules! __impl_parse_result_workspace_root {
482    () => {
483        fn workspace_root(&self) -> Option<&::std::path::Path> {
484            None
485        }
486    };
487    ($field:ident) => {
488        fn workspace_root(&self) -> Option<&::std::path::Path> {
489            self.$field.as_deref()
490        }
491    };
492}
493
494/// Emits [`impl_parse_result!`]'s `dependency_truncation` method body, or nothing when the
495/// field is omitted (the trait default applies) — not part of the public macro API.
496#[doc(hidden)]
497#[macro_export]
498macro_rules! __impl_parse_result_dependency_truncation {
499    () => {};
500    ($field:ident) => {
501        fn dependency_truncation(&self) -> Option<(usize, usize)> {
502            self.$field
503        }
504    };
505}
506
507/// Emits [`impl_parse_result!`]'s `blocked_registries` method body, or nothing when the
508/// field is omitted (the trait default applies) — not part of the public macro API.
509#[doc(hidden)]
510#[macro_export]
511macro_rules! __impl_parse_result_blocked_registries {
512    () => {};
513    ($field:ident) => {
514        fn blocked_registries(&self) -> Vec<$crate::ecosystem::BlockedRegistryOccurrence> {
515            self.$field.clone()
516        }
517    };
518}
519
520/// Emits [`impl_parse_result!`]'s `rejected_registries` method body, or nothing when the
521/// field is omitted (the trait default applies) — not part of the public macro API.
522#[doc(hidden)]
523#[macro_export]
524macro_rules! __impl_parse_result_rejected_registries {
525    () => {};
526    ($field:ident) => {
527        fn rejected_registries(&self) -> Vec<$crate::ecosystem::RejectedRegistryOccurrence> {
528            self.$field.clone()
529        }
530    };
531}
532
533/// Implement `ParseResult` trait for a struct.
534///
535/// # Arguments
536///
537/// * `$type` - The struct type name
538/// * `$dep_type` - The dependency type that implements `Dependency`
539/// * `dependencies` - Field name for dependencies vec (`Vec<DepType>`)
540/// * `uri` - Field name for document URI (`Url`)
541/// * `workspace_root` - Optional: field name for workspace root (`Option<PathBuf>`)
542/// * `dependency_truncation` - Optional: field name for the `Option<(usize, usize)>`
543///   [`ecosystem::ParseResult::dependency_truncation`](crate::ecosystem::ParseResult::dependency_truncation)
544///   override (#796) — omit when the parser has no [`crate::DependencyBudget`] wired in yet
545///   (the trait default `None` applies).
546/// * `blocked_registries` - Optional: field name for the `Vec<BlockedRegistryOccurrence>`
547///   [`ecosystem::ParseResult::blocked_registries`](crate::ecosystem::ParseResult::blocked_registries)
548///   override (#925, #969), read via `.clone()` — omit when the ecosystem enforces no
549///   configurable-registry reachability policy (the trait default empty `Vec` applies).
550/// * `rejected_registries` - Optional: field name for the `Vec<RejectedRegistryOccurrence>`
551///   [`ecosystem::ParseResult::rejected_registries`](crate::ecosystem::ParseResult::rejected_registries)
552///   override (#1438), read via `.clone()` — omit when the ecosystem produces no
553///   `InvalidEntry`-based rejections beyond the blocked-host case (the trait default empty
554///   `Vec` applies).
555///
556/// All optional fields are given in the fixed order shown above — `workspace_root`, then
557/// `dependency_truncation`, then `blocked_registries`, then `rejected_registries` — any
558/// subset may be omitted, but a present field cannot appear out of order.
559///
560/// # Examples
561///
562/// ```ignore
563/// use deps_core::impl_parse_result;
564///
565/// pub struct MyParseResult {
566///     pub dependencies: Vec<MyDependency>,
567///     pub uri: Url,
568///     pub dependency_truncation: Option<(usize, usize)>,
569/// }
570///
571/// impl_parse_result!(MyParseResult, MyDependency {
572///     dependencies: dependencies,
573///     uri: uri,
574/// });
575///
576/// // With workspace root and/or dependency-truncation tracking:
577/// impl_parse_result!(MyParseResult, MyDependency {
578///     dependencies: dependencies,
579///     uri: uri,
580///     workspace_root: workspace_root,
581///     dependency_truncation: dependency_truncation,
582/// });
583///
584/// // With a policy-blocked-registry override too:
585/// impl_parse_result!(MyParseResult, MyDependency {
586///     dependencies: dependencies,
587///     uri: uri,
588///     dependency_truncation: dependency_truncation,
589///     blocked_registries: blocked_registries,
590/// });
591/// ```
592#[macro_export]
593macro_rules! impl_parse_result {
594    ($type:ty, $dep_type:ty {
595        dependencies: $dependencies:ident,
596        uri: $uri:ident
597        $(, workspace_root: $workspace_root:ident)?
598        $(, dependency_truncation: $dependency_truncation:ident)?
599        $(, blocked_registries: $blocked_registries:ident)?
600        $(, rejected_registries: $rejected_registries:ident)?
601        $(,)?
602    }) => {
603        impl $crate::ecosystem::ParseResult for $type {
604            fn dependencies(&self) -> Vec<&dyn $crate::ecosystem::Dependency> {
605                self.$dependencies
606                    .iter()
607                    .map(|d| d as &dyn $crate::ecosystem::Dependency)
608                    .collect()
609            }
610
611            $crate::__impl_parse_result_workspace_root!($($workspace_root)?);
612
613            fn uri(&self) -> &::url::Url {
614                &self.$uri
615            }
616
617            $crate::__impl_parse_result_dependency_truncation!($($dependency_truncation)?);
618
619            $crate::__impl_parse_result_blocked_registries!($($blocked_registries)?);
620
621            $crate::__impl_parse_result_rejected_registries!($($rejected_registries)?);
622
623            fn as_any(&self) -> &dyn ::std::any::Any {
624                self
625            }
626        }
627    };
628}
629
630/// Generates one `Registry` trait-impl method that boxes an inherent method's result.
631///
632/// Turns a concrete `Vec<T>` into `Vec<Box<dyn Version>>` (#834 code review: this exact
633/// boilerplate was independently hand-written ~11 times for `get_versions` and ~7 times for
634/// `get_versions_with` across the workspace's ecosystem crates before this macro).
635///
636/// Requires an inherent method of the same name, taking `(&self, name: &str)` —
637/// `get_versions` — or `(&self, name: &str, freshness: FreshnessSettings)` —
638/// `get_versions_with` — and returning `Result<Vec<T>>` where `T: Version`. Use inside your
639/// own `impl deps_core::Registry for MyRegistry { ... }` block; every other trait method
640/// (`get_latest_matching`, `search`, ...) is unaffected and still hand-written, since their
641/// boxing shape is either `Option<Box<dyn Version>>` (a single value, not a `Vec`) or
642/// `Box<dyn Metadata>` (a different trait entirely).
643///
644/// # Examples
645///
646/// ```ignore
647/// use deps_core::impl_registry_versions_method;
648///
649/// impl deps_core::Registry for MyRegistry {
650///     impl_registry_versions_method!(get_versions);
651///     impl_registry_versions_method!(get_versions_with);
652///     // ... get_latest_matching, search, as_any still hand-written ...
653/// }
654/// ```
655#[macro_export]
656macro_rules! impl_registry_versions_method {
657    (get_versions) => {
658        fn get_versions<'a>(
659            &'a self,
660            name: &'a $crate::PackageName,
661        ) -> $crate::ecosystem::BoxFuture<
662            'a,
663            $crate::error::Result<::std::vec::Vec<::std::boxed::Box<dyn $crate::Version>>>,
664        > {
665            ::std::boxed::Box::pin(async move {
666                let versions = self.get_versions(name.as_str()).await?;
667                Ok(versions
668                    .into_iter()
669                    .map(|v| ::std::boxed::Box::new(v) as ::std::boxed::Box<dyn $crate::Version>)
670                    .collect())
671            })
672        }
673    };
674    (get_versions_with) => {
675        fn get_versions_with<'a>(
676            &'a self,
677            name: &'a $crate::PackageName,
678            freshness: $crate::FreshnessSettings,
679        ) -> $crate::ecosystem::BoxFuture<
680            'a,
681            $crate::error::Result<::std::vec::Vec<::std::boxed::Box<dyn $crate::Version>>>,
682        > {
683            ::std::boxed::Box::pin(async move {
684                let versions = self.get_versions_with(name.as_str(), freshness).await?;
685                Ok(versions
686                    .into_iter()
687                    .map(|v| ::std::boxed::Box::new(v) as ::std::boxed::Box<dyn $crate::Version>)
688                    .collect())
689            })
690        }
691    };
692}
693
694/// Generates a trivial inherent `get_versions_with` that ignores `freshness`.
695///
696/// Delegates to `Self::get_versions` (#834 code review: this exact body was independently
697/// hand-written 6 times across the workspace before this macro).
698///
699/// For ecosystems whose base version listing already carries everything
700/// `get_versions_with` would otherwise fetch separately — there is no second request for
701/// `freshness` to gate today, so the flag is accepted (matching every other ecosystem's
702/// `get_versions_with` signature) but otherwise unused. Not appropriate for an ecosystem
703/// that *does* have a separate freshness-enrichment fetch (e.g. `deps-maven`, `deps-nuget`,
704/// `deps-swift`) — those keep their own hand-written `get_versions_with`. If a registry
705/// generated by this macro ever gains its own publish-time enrichment, switch it to a
706/// hand-written `get_versions_with` too (issue #588 critic M10) — this macro's whole point
707/// is a pass-through, not a promise that `freshness` stays forever irrelevant.
708///
709/// # Examples
710///
711/// ```ignore
712/// use deps_core::impl_get_versions_with_passthrough;
713///
714/// impl MyRegistry {
715///     pub async fn get_versions(&self, name: &str) -> Result<Vec<MyVersion>> { todo!() }
716/// }
717///
718/// impl_get_versions_with_passthrough!(MyRegistry, MyVersion);
719/// ```
720#[macro_export]
721macro_rules! impl_get_versions_with_passthrough {
722    ($type:ty, $version:ty $(,)?) => {
723        impl $type {
724            /// Same as [`Self::get_versions`] — the publish date, if any, already arrives
725            /// with the base listing, so `freshness` has no extra fetch to gate. Accepted
726            /// (matching every other ecosystem's `get_versions_with` signature, #834
727            /// critic S2) but otherwise unused.
728            ///
729            /// # Errors
730            ///
731            /// Same as [`Self::get_versions`].
732            pub async fn get_versions_with(
733                &self,
734                name: &str,
735                _freshness: $crate::FreshnessSettings,
736            ) -> $crate::error::Result<::std::vec::Vec<$version>> {
737                self.get_versions(name).await
738            }
739        }
740    };
741}
742
743#[cfg(test)]
744mod tests {
745    use crate::ConcreteVersion;
746    use crate::position::{Position, Range};
747
748    #[derive(Debug, Clone)]
749    struct TestDependency {
750        name: crate::PackageName,
751        name_range: Range,
752        version_req: Option<crate::VersionReq>,
753        version_range: Option<Range>,
754    }
755
756    #[derive(Debug, Clone)]
757    struct TestDependencyWithSource {
758        name: crate::PackageName,
759        name_range: Range,
760        version_req: Option<crate::VersionReq>,
761        version_range: Option<Range>,
762        source: crate::parser::DependencySource,
763        version_literal: Option<String>,
764    }
765
766    #[derive(Debug, Clone)]
767    struct TestVersion {
768        version: ConcreteVersion,
769        yanked: bool,
770    }
771
772    #[derive(Debug, Clone)]
773    struct TestVersionWithPublishedAt {
774        version: ConcreteVersion,
775        yanked: bool,
776        published_at: Option<crate::freshness::PublishTime>,
777    }
778
779    #[derive(Debug, Clone)]
780    struct TestVersionWithPrerelease {
781        version: ConcreteVersion,
782        yanked: bool,
783    }
784
785    #[derive(Debug, Clone)]
786    struct TestVersionWithPublishedAtAndPrerelease {
787        version: ConcreteVersion,
788        yanked: bool,
789        published_at: Option<crate::freshness::PublishTime>,
790    }
791
792    #[derive(Debug, Clone)]
793    struct TestPackage {
794        name: crate::PackageName,
795        description: Option<String>,
796        repository: Option<String>,
797        homepage: Option<String>,
798        latest_version: ConcreteVersion,
799    }
800
801    #[derive(Debug)]
802    struct TestParseResult {
803        dependencies: Vec<TestDependency>,
804        uri: url::Url,
805    }
806
807    #[derive(Debug)]
808    struct TestParseResultAllOptionals {
809        dependencies: Vec<TestDependency>,
810        uri: url::Url,
811        workspace_root: Option<std::path::PathBuf>,
812        dependency_truncation: Option<(usize, usize)>,
813        blocked_registries: Vec<crate::ecosystem::BlockedRegistryOccurrence>,
814    }
815
816    #[derive(Debug)]
817    struct TestParseResultTruncationAndBlockedRegistries {
818        dependencies: Vec<TestDependency>,
819        uri: url::Url,
820        dependency_truncation: Option<(usize, usize)>,
821        blocked_registries: Vec<crate::ecosystem::BlockedRegistryOccurrence>,
822    }
823
824    impl_dependency!(TestDependency {
825        name: name,
826        name_range: name_range,
827        version: version_req,
828        version_range: version_range,
829    });
830
831    impl_dependency!(TestDependencyWithSource {
832        name: name,
833        name_range: name_range,
834        version: version_req,
835        version_range: version_range,
836        source: source,
837        version_literal: version_literal,
838    });
839
840    impl_version!(TestVersion {
841        version: version,
842        status: |v: &TestVersion| crate::registry::RemovalStatus::from_yanked(v.yanked),
843    });
844
845    impl_version!(TestVersionWithPublishedAt {
846        version: version,
847        status: |v: &TestVersionWithPublishedAt| crate::registry::RemovalStatus::from_yanked(
848            v.yanked
849        ),
850        published_at: published_at,
851    });
852
853    impl_version!(TestVersionWithPrerelease {
854        version: version,
855        status: |v: &TestVersionWithPrerelease| crate::registry::RemovalStatus::from_yanked(
856            v.yanked
857        ),
858        prerelease: |v: &TestVersionWithPrerelease| v.version.as_str().contains(".pre"),
859    });
860
861    impl_version!(TestVersionWithPublishedAtAndPrerelease {
862        version: version,
863        status: |v: &TestVersionWithPublishedAtAndPrerelease| {
864            crate::registry::RemovalStatus::from_yanked(v.yanked)
865        },
866        published_at: published_at,
867        prerelease: |v: &TestVersionWithPublishedAtAndPrerelease| v
868            .version
869            .as_str()
870            .contains(".pre"),
871    });
872
873    impl_metadata!(TestPackage {
874        name: name,
875        description: description,
876        repository: repository,
877        documentation: homepage,
878        latest_version: latest_version,
879    });
880
881    impl_parse_result!(
882        TestParseResult,
883        TestDependency {
884            dependencies: dependencies,
885            uri: uri,
886        }
887    );
888
889    impl_parse_result!(
890        TestParseResultAllOptionals,
891        TestDependency {
892            dependencies: dependencies,
893            uri: uri,
894            workspace_root: workspace_root,
895            dependency_truncation: dependency_truncation,
896            blocked_registries: blocked_registries,
897        }
898    );
899
900    impl_parse_result!(
901        TestParseResultTruncationAndBlockedRegistries,
902        TestDependency {
903            dependencies: dependencies,
904            uri: uri,
905            dependency_truncation: dependency_truncation,
906            blocked_registries: blocked_registries,
907        }
908    );
909
910    #[test]
911    fn test_impl_dependency_macro() {
912        use crate::ecosystem::Dependency;
913
914        let dep = TestDependency {
915            name: "test-pkg".into(),
916            name_range: Range::new(Position::new(0, 0), Position::new(0, 8)),
917            version_req: Some("1.0.0".into()),
918            version_range: Some(Range::new(Position::new(0, 10), Position::new(0, 15))),
919        };
920
921        assert_eq!(dep.name(), "test-pkg");
922        assert_eq!(
923            dep.version_requirement().map(crate::VersionReq::as_str),
924            Some("1.0.0")
925        );
926        assert!(dep.as_any().is::<TestDependency>());
927        assert!(matches!(
928            dep.source(),
929            crate::parser::DependencySource::Registry
930        ));
931    }
932
933    #[test]
934    fn test_impl_dependency_macro_with_source_and_version_literal() {
935        use crate::ecosystem::Dependency;
936
937        let dep = TestDependencyWithSource {
938            name: "test-pkg".into(),
939            name_range: Range::new(Position::new(0, 0), Position::new(0, 8)),
940            version_req: Some("1.0.0".into()),
941            version_range: Some(Range::new(Position::new(0, 10), Position::new(0, 15))),
942            source: crate::parser::DependencySource::Git {
943                url: "https://example.com/repo.git".into(),
944                rev: None,
945            },
946            version_literal: Some("v1.0.0".into()),
947        };
948
949        assert!(matches!(
950            dep.source(),
951            crate::parser::DependencySource::Git { .. }
952        ));
953        assert_eq!(dep.version_literal(), Some("v1.0.0"));
954    }
955
956    #[test]
957    fn test_impl_version_macro() {
958        use crate::registry::Version;
959
960        let version = TestVersion {
961            version: "2.0.0".into(),
962            yanked: true,
963        };
964
965        assert_eq!(version.version_string().as_str(), "2.0.0");
966        assert!(version.removal_status().blocks_resolution());
967        assert!(version.as_any().is::<TestVersion>());
968        assert!(version.published_at().is_none());
969    }
970
971    #[test]
972    fn test_impl_version_macro_with_published_at() {
973        use crate::freshness::PublishTime;
974        use crate::registry::Version;
975
976        let version = TestVersionWithPublishedAt {
977            version: "3.0.0".into(),
978            yanked: false,
979            published_at: Some(PublishTime::from_unix_secs(1_000)),
980        };
981
982        assert_eq!(version.version_string().as_str(), "3.0.0");
983        assert!(!version.removal_status().blocks_resolution());
984        assert_eq!(
985            version.published_at(),
986            Some(PublishTime::from_unix_secs(1_000))
987        );
988        assert!(version.as_any().is::<TestVersionWithPublishedAt>());
989    }
990
991    #[test]
992    fn test_impl_version_macro_with_prerelease() {
993        use crate::registry::Version;
994
995        let stable = TestVersionWithPrerelease {
996            version: "1.0.0".into(),
997            yanked: false,
998        };
999        let prerelease = TestVersionWithPrerelease {
1000            version: "1.0.0.pre1".into(),
1001            yanked: false,
1002        };
1003
1004        assert!(!stable.is_prerelease());
1005        assert!(prerelease.is_prerelease());
1006        assert!(stable.is_stable());
1007        assert!(!prerelease.is_stable());
1008    }
1009
1010    #[test]
1011    fn test_impl_version_macro_with_published_at_and_prerelease() {
1012        use crate::freshness::PublishTime;
1013        use crate::registry::Version;
1014
1015        let version = TestVersionWithPublishedAtAndPrerelease {
1016            version: "2.0.0.pre1".into(),
1017            yanked: false,
1018            published_at: Some(PublishTime::from_unix_secs(2_000)),
1019        };
1020
1021        assert!(version.is_prerelease());
1022        assert_eq!(
1023            version.published_at(),
1024            Some(PublishTime::from_unix_secs(2_000))
1025        );
1026    }
1027
1028    #[test]
1029    fn test_impl_metadata_macro() {
1030        use crate::registry::Metadata;
1031
1032        let pkg = TestPackage {
1033            name: crate::PackageName::new("my-pkg"),
1034            description: Some("A test package".into()),
1035            repository: Some("user/repo".into()),
1036            homepage: Some("https://example.com".into()),
1037            latest_version: "3.0.0".into(),
1038        };
1039
1040        assert_eq!(pkg.name(), "my-pkg");
1041        assert_eq!(pkg.description(), Some("A test package"));
1042        assert_eq!(pkg.documentation(), Some("https://example.com"));
1043        assert!(pkg.as_any().is::<TestPackage>());
1044    }
1045
1046    #[test]
1047    fn test_impl_parse_result_macro() {
1048        use crate::ecosystem::ParseResult;
1049
1050        let result = TestParseResult {
1051            dependencies: vec![TestDependency {
1052                name: "dep1".into(),
1053                name_range: Range::default(),
1054                version_req: None,
1055                version_range: None,
1056            }],
1057            uri: crate::test_util::test_uri("/test"),
1058        };
1059
1060        assert_eq!(result.dependencies().len(), 1);
1061        assert!(result.workspace_root().is_none());
1062        assert!(result.as_any().is::<TestParseResult>());
1063    }
1064
1065    #[test]
1066    fn test_impl_parse_result_macro_all_optionals() {
1067        use crate::ecosystem::{BlockedRegistryOccurrence, ParseResult};
1068        use crate::net_policy::HostClass;
1069
1070        let result = TestParseResultAllOptionals {
1071            dependencies: vec![TestDependency {
1072                name: "dep1".into(),
1073                name_range: Range::default(),
1074                version_req: None,
1075                version_range: None,
1076            }],
1077            uri: crate::test_util::test_uri("/test"),
1078            workspace_root: Some(std::path::PathBuf::from("/workspace")),
1079            dependency_truncation: Some((5, 10)),
1080            blocked_registries: vec![BlockedRegistryOccurrence {
1081                range: Range::default(),
1082                class: HostClass::Loopback,
1083                raw_value: "registry".into(),
1084                declaration_key: "key".into(),
1085            }],
1086        };
1087
1088        assert_eq!(result.dependencies().len(), 1);
1089        assert_eq!(
1090            result.workspace_root(),
1091            Some(std::path::Path::new("/workspace"))
1092        );
1093        assert_eq!(result.dependency_truncation(), Some((5, 10)));
1094        assert_eq!(result.blocked_registries().len(), 1);
1095        assert!(result.as_any().is::<TestParseResultAllOptionals>());
1096    }
1097
1098    #[test]
1099    fn test_impl_parse_result_macro_truncation_and_blocked_registries() {
1100        use crate::ecosystem::{BlockedRegistryOccurrence, ParseResult};
1101        use crate::net_policy::HostClass;
1102
1103        let result = TestParseResultTruncationAndBlockedRegistries {
1104            dependencies: vec![],
1105            uri: crate::test_util::test_uri("/test"),
1106            dependency_truncation: Some((3, 7)),
1107            blocked_registries: vec![BlockedRegistryOccurrence {
1108                range: Range::default(),
1109                class: HostClass::Loopback,
1110                raw_value: "registry".into(),
1111                declaration_key: "key".into(),
1112            }],
1113        };
1114
1115        assert!(result.workspace_root().is_none());
1116        assert_eq!(result.dependency_truncation(), Some((3, 7)));
1117        assert_eq!(result.blocked_registries().len(), 1);
1118        assert!(
1119            result
1120                .as_any()
1121                .is::<TestParseResultTruncationAndBlockedRegistries>()
1122        );
1123    }
1124}