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/// Implement `ParseResult` trait for a struct.
477///
478/// # Arguments
479///
480/// * `$type` - The struct type name
481/// * `$dep_type` - The dependency type that implements `Dependency`
482/// * `dependencies` - Field name for dependencies vec (`Vec<DepType>`)
483/// * `uri` - Field name for document URI (`Url`)
484/// * `workspace_root` - Optional: field name for workspace root (`Option<PathBuf>`)
485/// * `dependency_truncation` - Optional: field name for the `Option<(usize, usize)>`
486///   [`ecosystem::ParseResult::dependency_truncation`](crate::ecosystem::ParseResult::dependency_truncation)
487///   override (#796) — omit when the parser has no [`crate::DependencyBudget`] wired in yet
488///   (the trait default `None` applies).
489/// * `blocked_registries` - Optional, matched last: field name for the `Vec<BlockedRegistryOccurrence>`
490///   [`ecosystem::ParseResult::blocked_registries`](crate::ecosystem::ParseResult::blocked_registries)
491///   override (#925, #969), read via `.clone()` — omit when the ecosystem enforces no
492///   configurable-registry reachability policy (the trait default empty `Vec` applies).
493///
494/// All optional fields are matched in the fixed order shown above — `workspace_root`, then
495/// `dependency_truncation`, then `blocked_registries` — any subset may be omitted, but a
496/// present field cannot appear out of order.
497///
498/// # Examples
499///
500/// ```ignore
501/// use deps_core::impl_parse_result;
502///
503/// pub struct MyParseResult {
504///     pub dependencies: Vec<MyDependency>,
505///     pub uri: Url,
506///     pub dependency_truncation: Option<(usize, usize)>,
507/// }
508///
509/// impl_parse_result!(MyParseResult, MyDependency {
510///     dependencies: dependencies,
511///     uri: uri,
512/// });
513///
514/// // With workspace root and/or dependency-truncation tracking:
515/// impl_parse_result!(MyParseResult, MyDependency {
516///     dependencies: dependencies,
517///     uri: uri,
518///     workspace_root: workspace_root,
519///     dependency_truncation: dependency_truncation,
520/// });
521///
522/// // With a policy-blocked-registry override too:
523/// impl_parse_result!(MyParseResult, MyDependency {
524///     dependencies: dependencies,
525///     uri: uri,
526///     dependency_truncation: dependency_truncation,
527///     blocked_registries: blocked_registries,
528/// });
529/// ```
530#[macro_export]
531macro_rules! impl_parse_result {
532    ($type:ty, $dep_type:ty {
533        dependencies: $dependencies:ident,
534        uri: $uri:ident $(,)?
535    }) => {
536        impl $crate::ecosystem::ParseResult for $type {
537            fn dependencies(&self) -> Vec<&dyn $crate::ecosystem::Dependency> {
538                self.$dependencies
539                    .iter()
540                    .map(|d| d as &dyn $crate::ecosystem::Dependency)
541                    .collect()
542            }
543
544            fn workspace_root(&self) -> Option<&::std::path::Path> {
545                None
546            }
547
548            fn uri(&self) -> &::url::Url {
549                &self.$uri
550            }
551
552            fn as_any(&self) -> &dyn ::std::any::Any {
553                self
554            }
555        }
556    };
557    ($type:ty, $dep_type:ty {
558        dependencies: $dependencies:ident,
559        uri: $uri:ident,
560        workspace_root: $workspace_root:ident $(,)?
561    }) => {
562        impl $crate::ecosystem::ParseResult for $type {
563            fn dependencies(&self) -> Vec<&dyn $crate::ecosystem::Dependency> {
564                self.$dependencies
565                    .iter()
566                    .map(|d| d as &dyn $crate::ecosystem::Dependency)
567                    .collect()
568            }
569
570            fn workspace_root(&self) -> Option<&::std::path::Path> {
571                self.$workspace_root.as_deref()
572            }
573
574            fn uri(&self) -> &::url::Url {
575                &self.$uri
576            }
577
578            fn as_any(&self) -> &dyn ::std::any::Any {
579                self
580            }
581        }
582    };
583    ($type:ty, $dep_type:ty {
584        dependencies: $dependencies:ident,
585        uri: $uri:ident,
586        dependency_truncation: $dependency_truncation:ident $(,)?
587    }) => {
588        impl $crate::ecosystem::ParseResult for $type {
589            fn dependencies(&self) -> Vec<&dyn $crate::ecosystem::Dependency> {
590                self.$dependencies
591                    .iter()
592                    .map(|d| d as &dyn $crate::ecosystem::Dependency)
593                    .collect()
594            }
595
596            fn workspace_root(&self) -> Option<&::std::path::Path> {
597                None
598            }
599
600            fn uri(&self) -> &::url::Url {
601                &self.$uri
602            }
603
604            fn as_any(&self) -> &dyn ::std::any::Any {
605                self
606            }
607
608            fn dependency_truncation(&self) -> Option<(usize, usize)> {
609                self.$dependency_truncation
610            }
611        }
612    };
613    ($type:ty, $dep_type:ty {
614        dependencies: $dependencies:ident,
615        uri: $uri:ident,
616        workspace_root: $workspace_root:ident,
617        dependency_truncation: $dependency_truncation:ident $(,)?
618    }) => {
619        impl $crate::ecosystem::ParseResult for $type {
620            fn dependencies(&self) -> Vec<&dyn $crate::ecosystem::Dependency> {
621                self.$dependencies
622                    .iter()
623                    .map(|d| d as &dyn $crate::ecosystem::Dependency)
624                    .collect()
625            }
626
627            fn workspace_root(&self) -> Option<&::std::path::Path> {
628                self.$workspace_root.as_deref()
629            }
630
631            fn uri(&self) -> &::url::Url {
632                &self.$uri
633            }
634
635            fn as_any(&self) -> &dyn ::std::any::Any {
636                self
637            }
638
639            fn dependency_truncation(&self) -> Option<(usize, usize)> {
640                self.$dependency_truncation
641            }
642        }
643    };
644    ($type:ty, $dep_type:ty {
645        dependencies: $dependencies:ident,
646        uri: $uri:ident,
647        blocked_registries: $blocked_registries:ident $(,)?
648    }) => {
649        impl $crate::ecosystem::ParseResult for $type {
650            fn dependencies(&self) -> Vec<&dyn $crate::ecosystem::Dependency> {
651                self.$dependencies
652                    .iter()
653                    .map(|d| d as &dyn $crate::ecosystem::Dependency)
654                    .collect()
655            }
656
657            fn workspace_root(&self) -> Option<&::std::path::Path> {
658                None
659            }
660
661            fn uri(&self) -> &::url::Url {
662                &self.$uri
663            }
664
665            fn blocked_registries(&self) -> Vec<$crate::ecosystem::BlockedRegistryOccurrence> {
666                self.$blocked_registries.clone()
667            }
668
669            fn as_any(&self) -> &dyn ::std::any::Any {
670                self
671            }
672        }
673    };
674    ($type:ty, $dep_type:ty {
675        dependencies: $dependencies:ident,
676        uri: $uri:ident,
677        workspace_root: $workspace_root:ident,
678        blocked_registries: $blocked_registries:ident $(,)?
679    }) => {
680        impl $crate::ecosystem::ParseResult for $type {
681            fn dependencies(&self) -> Vec<&dyn $crate::ecosystem::Dependency> {
682                self.$dependencies
683                    .iter()
684                    .map(|d| d as &dyn $crate::ecosystem::Dependency)
685                    .collect()
686            }
687
688            fn workspace_root(&self) -> Option<&::std::path::Path> {
689                self.$workspace_root.as_deref()
690            }
691
692            fn uri(&self) -> &::url::Url {
693                &self.$uri
694            }
695
696            fn blocked_registries(&self) -> Vec<$crate::ecosystem::BlockedRegistryOccurrence> {
697                self.$blocked_registries.clone()
698            }
699
700            fn as_any(&self) -> &dyn ::std::any::Any {
701                self
702            }
703        }
704    };
705    ($type:ty, $dep_type:ty {
706        dependencies: $dependencies:ident,
707        uri: $uri:ident,
708        dependency_truncation: $dependency_truncation:ident,
709        blocked_registries: $blocked_registries:ident $(,)?
710    }) => {
711        impl $crate::ecosystem::ParseResult for $type {
712            fn dependencies(&self) -> Vec<&dyn $crate::ecosystem::Dependency> {
713                self.$dependencies
714                    .iter()
715                    .map(|d| d as &dyn $crate::ecosystem::Dependency)
716                    .collect()
717            }
718
719            fn workspace_root(&self) -> Option<&::std::path::Path> {
720                None
721            }
722
723            fn uri(&self) -> &::url::Url {
724                &self.$uri
725            }
726
727            fn blocked_registries(&self) -> Vec<$crate::ecosystem::BlockedRegistryOccurrence> {
728                self.$blocked_registries.clone()
729            }
730
731            fn as_any(&self) -> &dyn ::std::any::Any {
732                self
733            }
734
735            fn dependency_truncation(&self) -> Option<(usize, usize)> {
736                self.$dependency_truncation
737            }
738        }
739    };
740    ($type:ty, $dep_type:ty {
741        dependencies: $dependencies:ident,
742        uri: $uri:ident,
743        workspace_root: $workspace_root:ident,
744        dependency_truncation: $dependency_truncation:ident,
745        blocked_registries: $blocked_registries:ident $(,)?
746    }) => {
747        impl $crate::ecosystem::ParseResult for $type {
748            fn dependencies(&self) -> Vec<&dyn $crate::ecosystem::Dependency> {
749                self.$dependencies
750                    .iter()
751                    .map(|d| d as &dyn $crate::ecosystem::Dependency)
752                    .collect()
753            }
754
755            fn workspace_root(&self) -> Option<&::std::path::Path> {
756                self.$workspace_root.as_deref()
757            }
758
759            fn uri(&self) -> &::url::Url {
760                &self.$uri
761            }
762
763            fn blocked_registries(&self) -> Vec<$crate::ecosystem::BlockedRegistryOccurrence> {
764                self.$blocked_registries.clone()
765            }
766
767            fn as_any(&self) -> &dyn ::std::any::Any {
768                self
769            }
770
771            fn dependency_truncation(&self) -> Option<(usize, usize)> {
772                self.$dependency_truncation
773            }
774        }
775    };
776}
777
778/// Generates one `Registry` trait-impl method that boxes an inherent method's result.
779///
780/// Turns a concrete `Vec<T>` into `Vec<Box<dyn Version>>` (#834 code review: this exact
781/// boilerplate was independently hand-written ~11 times for `get_versions` and ~7 times for
782/// `get_versions_with` across the workspace's ecosystem crates before this macro).
783///
784/// Requires an inherent method of the same name, taking `(&self, name: &str)` —
785/// `get_versions` — or `(&self, name: &str, freshness: FreshnessSettings)` —
786/// `get_versions_with` — and returning `Result<Vec<T>>` where `T: Version`. Use inside your
787/// own `impl deps_core::Registry for MyRegistry { ... }` block; every other trait method
788/// (`get_latest_matching`, `search`, ...) is unaffected and still hand-written, since their
789/// boxing shape is either `Option<Box<dyn Version>>` (a single value, not a `Vec`) or
790/// `Box<dyn Metadata>` (a different trait entirely).
791///
792/// # Examples
793///
794/// ```ignore
795/// use deps_core::impl_registry_versions_method;
796///
797/// impl deps_core::Registry for MyRegistry {
798///     impl_registry_versions_method!(get_versions);
799///     impl_registry_versions_method!(get_versions_with);
800///     // ... get_latest_matching, search, as_any still hand-written ...
801/// }
802/// ```
803#[macro_export]
804macro_rules! impl_registry_versions_method {
805    (get_versions) => {
806        fn get_versions<'a>(
807            &'a self,
808            name: &'a $crate::PackageName,
809        ) -> $crate::ecosystem::BoxFuture<
810            'a,
811            $crate::error::Result<::std::vec::Vec<::std::boxed::Box<dyn $crate::Version>>>,
812        > {
813            ::std::boxed::Box::pin(async move {
814                let versions = self.get_versions(name.as_str()).await?;
815                Ok(versions
816                    .into_iter()
817                    .map(|v| ::std::boxed::Box::new(v) as ::std::boxed::Box<dyn $crate::Version>)
818                    .collect())
819            })
820        }
821    };
822    (get_versions_with) => {
823        fn get_versions_with<'a>(
824            &'a self,
825            name: &'a $crate::PackageName,
826            freshness: $crate::FreshnessSettings,
827        ) -> $crate::ecosystem::BoxFuture<
828            'a,
829            $crate::error::Result<::std::vec::Vec<::std::boxed::Box<dyn $crate::Version>>>,
830        > {
831            ::std::boxed::Box::pin(async move {
832                let versions = self.get_versions_with(name.as_str(), freshness).await?;
833                Ok(versions
834                    .into_iter()
835                    .map(|v| ::std::boxed::Box::new(v) as ::std::boxed::Box<dyn $crate::Version>)
836                    .collect())
837            })
838        }
839    };
840}
841
842/// Generates a trivial inherent `get_versions_with` that ignores `freshness`.
843///
844/// Delegates to `Self::get_versions` (#834 code review: this exact body was independently
845/// hand-written 6 times across the workspace before this macro).
846///
847/// For ecosystems whose base version listing already carries everything
848/// `get_versions_with` would otherwise fetch separately — there is no second request for
849/// `freshness` to gate today, so the flag is accepted (matching every other ecosystem's
850/// `get_versions_with` signature) but otherwise unused. Not appropriate for an ecosystem
851/// that *does* have a separate freshness-enrichment fetch (e.g. `deps-maven`, `deps-nuget`,
852/// `deps-swift`) — those keep their own hand-written `get_versions_with`. If a registry
853/// generated by this macro ever gains its own publish-time enrichment, switch it to a
854/// hand-written `get_versions_with` too (issue #588 critic M10) — this macro's whole point
855/// is a pass-through, not a promise that `freshness` stays forever irrelevant.
856///
857/// # Examples
858///
859/// ```ignore
860/// use deps_core::impl_get_versions_with_passthrough;
861///
862/// impl MyRegistry {
863///     pub async fn get_versions(&self, name: &str) -> Result<Vec<MyVersion>> { todo!() }
864/// }
865///
866/// impl_get_versions_with_passthrough!(MyRegistry, MyVersion);
867/// ```
868#[macro_export]
869macro_rules! impl_get_versions_with_passthrough {
870    ($type:ty, $version:ty $(,)?) => {
871        impl $type {
872            /// Same as [`Self::get_versions`] — the publish date, if any, already arrives
873            /// with the base listing, so `freshness` has no extra fetch to gate. Accepted
874            /// (matching every other ecosystem's `get_versions_with` signature, #834
875            /// critic S2) but otherwise unused.
876            ///
877            /// # Errors
878            ///
879            /// Same as [`Self::get_versions`].
880            pub async fn get_versions_with(
881                &self,
882                name: &str,
883                _freshness: $crate::FreshnessSettings,
884            ) -> $crate::error::Result<::std::vec::Vec<$version>> {
885                self.get_versions(name).await
886            }
887        }
888    };
889}
890
891#[cfg(test)]
892mod tests {
893    use crate::ConcreteVersion;
894    use crate::position::{Position, Range};
895
896    #[derive(Debug, Clone)]
897    struct TestDependency {
898        name: crate::PackageName,
899        name_range: Range,
900        version_req: Option<crate::VersionReq>,
901        version_range: Option<Range>,
902    }
903
904    #[derive(Debug, Clone)]
905    struct TestDependencyWithSource {
906        name: crate::PackageName,
907        name_range: Range,
908        version_req: Option<crate::VersionReq>,
909        version_range: Option<Range>,
910        source: crate::parser::DependencySource,
911        version_literal: Option<String>,
912    }
913
914    #[derive(Debug, Clone)]
915    struct TestVersion {
916        version: ConcreteVersion,
917        yanked: bool,
918    }
919
920    #[derive(Debug, Clone)]
921    struct TestVersionWithPublishedAt {
922        version: ConcreteVersion,
923        yanked: bool,
924        published_at: Option<crate::freshness::PublishTime>,
925    }
926
927    #[derive(Debug, Clone)]
928    struct TestVersionWithPrerelease {
929        version: ConcreteVersion,
930        yanked: bool,
931    }
932
933    #[derive(Debug, Clone)]
934    struct TestVersionWithPublishedAtAndPrerelease {
935        version: ConcreteVersion,
936        yanked: bool,
937        published_at: Option<crate::freshness::PublishTime>,
938    }
939
940    #[derive(Debug, Clone)]
941    struct TestPackage {
942        name: crate::PackageName,
943        description: Option<String>,
944        repository: Option<String>,
945        homepage: Option<String>,
946        latest_version: ConcreteVersion,
947    }
948
949    #[derive(Debug)]
950    struct TestParseResult {
951        dependencies: Vec<TestDependency>,
952        uri: url::Url,
953    }
954
955    impl_dependency!(TestDependency {
956        name: name,
957        name_range: name_range,
958        version: version_req,
959        version_range: version_range,
960    });
961
962    impl_dependency!(TestDependencyWithSource {
963        name: name,
964        name_range: name_range,
965        version: version_req,
966        version_range: version_range,
967        source: source,
968        version_literal: version_literal,
969    });
970
971    impl_version!(TestVersion {
972        version: version,
973        status: |v: &TestVersion| crate::registry::RemovalStatus::from_yanked(v.yanked),
974    });
975
976    impl_version!(TestVersionWithPublishedAt {
977        version: version,
978        status: |v: &TestVersionWithPublishedAt| crate::registry::RemovalStatus::from_yanked(
979            v.yanked
980        ),
981        published_at: published_at,
982    });
983
984    impl_version!(TestVersionWithPrerelease {
985        version: version,
986        status: |v: &TestVersionWithPrerelease| crate::registry::RemovalStatus::from_yanked(
987            v.yanked
988        ),
989        prerelease: |v: &TestVersionWithPrerelease| v.version.as_str().contains(".pre"),
990    });
991
992    impl_version!(TestVersionWithPublishedAtAndPrerelease {
993        version: version,
994        status: |v: &TestVersionWithPublishedAtAndPrerelease| {
995            crate::registry::RemovalStatus::from_yanked(v.yanked)
996        },
997        published_at: published_at,
998        prerelease: |v: &TestVersionWithPublishedAtAndPrerelease| v
999            .version
1000            .as_str()
1001            .contains(".pre"),
1002    });
1003
1004    impl_metadata!(TestPackage {
1005        name: name,
1006        description: description,
1007        repository: repository,
1008        documentation: homepage,
1009        latest_version: latest_version,
1010    });
1011
1012    impl_parse_result!(
1013        TestParseResult,
1014        TestDependency {
1015            dependencies: dependencies,
1016            uri: uri,
1017        }
1018    );
1019
1020    #[test]
1021    fn test_impl_dependency_macro() {
1022        use crate::ecosystem::Dependency;
1023
1024        let dep = TestDependency {
1025            name: "test-pkg".into(),
1026            name_range: Range::new(Position::new(0, 0), Position::new(0, 8)),
1027            version_req: Some("1.0.0".into()),
1028            version_range: Some(Range::new(Position::new(0, 10), Position::new(0, 15))),
1029        };
1030
1031        assert_eq!(dep.name(), "test-pkg");
1032        assert_eq!(
1033            dep.version_requirement().map(crate::VersionReq::as_str),
1034            Some("1.0.0")
1035        );
1036        assert!(dep.as_any().is::<TestDependency>());
1037        assert!(matches!(
1038            dep.source(),
1039            crate::parser::DependencySource::Registry
1040        ));
1041    }
1042
1043    #[test]
1044    fn test_impl_dependency_macro_with_source_and_version_literal() {
1045        use crate::ecosystem::Dependency;
1046
1047        let dep = TestDependencyWithSource {
1048            name: "test-pkg".into(),
1049            name_range: Range::new(Position::new(0, 0), Position::new(0, 8)),
1050            version_req: Some("1.0.0".into()),
1051            version_range: Some(Range::new(Position::new(0, 10), Position::new(0, 15))),
1052            source: crate::parser::DependencySource::Git {
1053                url: "https://example.com/repo.git".into(),
1054                rev: None,
1055            },
1056            version_literal: Some("v1.0.0".into()),
1057        };
1058
1059        assert!(matches!(
1060            dep.source(),
1061            crate::parser::DependencySource::Git { .. }
1062        ));
1063        assert_eq!(dep.version_literal(), Some("v1.0.0"));
1064    }
1065
1066    #[test]
1067    fn test_impl_version_macro() {
1068        use crate::registry::Version;
1069
1070        let version = TestVersion {
1071            version: "2.0.0".into(),
1072            yanked: true,
1073        };
1074
1075        assert_eq!(version.version_string().as_str(), "2.0.0");
1076        assert!(version.removal_status().blocks_resolution());
1077        assert!(version.as_any().is::<TestVersion>());
1078        assert!(version.published_at().is_none());
1079    }
1080
1081    #[test]
1082    fn test_impl_version_macro_with_published_at() {
1083        use crate::freshness::PublishTime;
1084        use crate::registry::Version;
1085
1086        let version = TestVersionWithPublishedAt {
1087            version: "3.0.0".into(),
1088            yanked: false,
1089            published_at: Some(PublishTime::from_unix_secs(1_000)),
1090        };
1091
1092        assert_eq!(version.version_string().as_str(), "3.0.0");
1093        assert!(!version.removal_status().blocks_resolution());
1094        assert_eq!(
1095            version.published_at(),
1096            Some(PublishTime::from_unix_secs(1_000))
1097        );
1098        assert!(version.as_any().is::<TestVersionWithPublishedAt>());
1099    }
1100
1101    #[test]
1102    fn test_impl_version_macro_with_prerelease() {
1103        use crate::registry::Version;
1104
1105        let stable = TestVersionWithPrerelease {
1106            version: "1.0.0".into(),
1107            yanked: false,
1108        };
1109        let prerelease = TestVersionWithPrerelease {
1110            version: "1.0.0.pre1".into(),
1111            yanked: false,
1112        };
1113
1114        assert!(!stable.is_prerelease());
1115        assert!(prerelease.is_prerelease());
1116        assert!(stable.is_stable());
1117        assert!(!prerelease.is_stable());
1118    }
1119
1120    #[test]
1121    fn test_impl_version_macro_with_published_at_and_prerelease() {
1122        use crate::freshness::PublishTime;
1123        use crate::registry::Version;
1124
1125        let version = TestVersionWithPublishedAtAndPrerelease {
1126            version: "2.0.0.pre1".into(),
1127            yanked: false,
1128            published_at: Some(PublishTime::from_unix_secs(2_000)),
1129        };
1130
1131        assert!(version.is_prerelease());
1132        assert_eq!(
1133            version.published_at(),
1134            Some(PublishTime::from_unix_secs(2_000))
1135        );
1136    }
1137
1138    #[test]
1139    fn test_impl_metadata_macro() {
1140        use crate::registry::Metadata;
1141
1142        let pkg = TestPackage {
1143            name: crate::PackageName::new("my-pkg"),
1144            description: Some("A test package".into()),
1145            repository: Some("user/repo".into()),
1146            homepage: Some("https://example.com".into()),
1147            latest_version: "3.0.0".into(),
1148        };
1149
1150        assert_eq!(pkg.name(), "my-pkg");
1151        assert_eq!(pkg.description(), Some("A test package"));
1152        assert_eq!(pkg.documentation(), Some("https://example.com"));
1153        assert!(pkg.as_any().is::<TestPackage>());
1154    }
1155
1156    #[test]
1157    fn test_impl_parse_result_macro() {
1158        use crate::ecosystem::ParseResult;
1159
1160        let result = TestParseResult {
1161            dependencies: vec![TestDependency {
1162                name: "dep1".into(),
1163                name_range: Range::default(),
1164                version_req: None,
1165                version_range: None,
1166            }],
1167            uri: crate::test_util::test_uri("/test"),
1168        };
1169
1170        assert_eq!(result.dependencies().len(), 1);
1171        assert!(result.workspace_root().is_none());
1172        assert!(result.as_any().is::<TestParseResult>());
1173    }
1174}