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: expression for dependency source (defaults to `Registry`)
15///
16/// # Examples
17///
18/// ```ignore
19/// use deps_core::{impl_dependency, PackageName, VersionReq};
20///
21/// pub struct MyDependency {
22///     pub name: PackageName,
23///     pub name_range: Range,
24///     pub version_req: Option<VersionReq>,
25///     pub version_range: Option<Range>,
26/// }
27///
28/// impl_dependency!(MyDependency {
29///     name: name,
30///     name_range: name_range,
31///     version: version_req,
32///     version_range: version_range,
33/// });
34/// ```
35#[macro_export]
36macro_rules! impl_dependency {
37    ($type:ty {
38        name: $name:ident,
39        name_range: $name_range:ident,
40        version: $version:ident,
41        version_range: $version_range:ident $(,)?
42    }) => {
43        $crate::impl_dependency!($type {
44            name: $name,
45            name_range: $name_range,
46            version: $version,
47            version_range: $version_range,
48            source: $crate::parser::DependencySource::Registry,
49        });
50    };
51    ($type:ty {
52        name: $name:ident,
53        name_range: $name_range:ident,
54        version: $version:ident,
55        version_range: $version_range:ident,
56        source: $source:expr $(,)?
57    }) => {
58        impl $crate::ecosystem::Dependency for $type {
59            fn name(&self) -> &$crate::PackageName {
60                &self.$name
61            }
62
63            fn name_range(&self) -> ::tower_lsp_server::ls_types::Range {
64                self.$name_range
65            }
66
67            fn version_requirement(&self) -> Option<&$crate::VersionReq> {
68                self.$version.as_ref()
69            }
70
71            fn version_range(&self) -> Option<::tower_lsp_server::ls_types::Range> {
72                self.$version_range
73            }
74
75            fn source(&self) -> $crate::parser::DependencySource {
76                $source
77            }
78
79            fn as_any(&self) -> &dyn ::std::any::Any {
80                self
81            }
82        }
83    };
84}
85
86/// Implement `Version` trait for a struct.
87///
88/// # Arguments
89///
90/// * `$type` - The struct type name
91/// * `version` - Field name for version string (`ConcreteVersion`)
92/// * `status` - Expression evaluating to a closure `Fn(&$type) -> RemovalStatus`,
93///   restating at every declaration site whether the ecosystem's flag is a hard
94///   removal ([`RemovalStatus::from_yanked`](crate::RemovalStatus::from_yanked)) or an
95///   advisory one ([`RemovalStatus::from_advisory`](crate::RemovalStatus::from_advisory))
96///   — see [`RemovalStatus`](crate::RemovalStatus).
97/// * `published_at` - Optional: field name for publish timestamp
98///   (`Option<PublishTime>`), already parsed eagerly at construction
99/// * `prerelease` - Optional: expression evaluating to a closure
100///   `Fn(&$type) -> bool`, used when the ecosystem has its own reliable
101///   prerelease signal (e.g. a semver-parsed `pre` component) instead of
102///   falling back to the trait's default hyphen-substring heuristic.
103///   Omitting this arm silently installs the default heuristic, with no
104///   compile-time signal that it may be wrong for the ecosystem — only omit
105///   it when the default is provably correct for the registry's version
106///   format (as of #322, `deps-composer` is the sole deliberate holdout,
107///   since Packagist versions aren't strict semver).
108/// * `deprecation` - Optional, requires both `published_at` and `prerelease` to also be
109///   given: expression evaluating to a closure `Fn(&$type) -> Option<&Deprecation>`,
110///   for an ecosystem whose registry exposes a package-level deprecation payload
111///   (issue #205). Omitting this arm installs the trait default (`None`).
112///
113/// # Examples
114///
115/// ```ignore
116/// use deps_core::{ConcreteVersion, impl_version, RemovalStatus};
117///
118/// pub struct MyVersion {
119///     pub version: ConcreteVersion,
120///     pub deprecated: bool,
121/// }
122///
123/// impl_version!(MyVersion {
124///     version: version,
125///     status: |v: &MyVersion| RemovalStatus::from_advisory(v.deprecated),
126/// });
127/// ```
128///
129/// With a publish timestamp:
130///
131/// ```ignore
132/// use deps_core::{ConcreteVersion, impl_version, PublishTime, RemovalStatus};
133///
134/// pub struct MyVersion {
135///     pub version: ConcreteVersion,
136///     pub deprecated: bool,
137///     pub published_at: Option<PublishTime>,
138/// }
139///
140/// impl_version!(MyVersion {
141///     version: version,
142///     status: |v: &MyVersion| RemovalStatus::from_advisory(v.deprecated),
143///     published_at: published_at,
144/// });
145/// ```
146///
147/// With a structured prerelease signal:
148///
149/// ```ignore
150/// use deps_core::{ConcreteVersion, impl_version, RemovalStatus};
151///
152/// pub struct MyVersion {
153///     pub version: ConcreteVersion,
154///     pub deprecated: bool,
155/// }
156///
157/// impl_version!(MyVersion {
158///     version: version,
159///     status: |v: &MyVersion| RemovalStatus::from_advisory(v.deprecated),
160///     prerelease: |v: &MyVersion| v.version.as_str().contains("-pre"),
161/// });
162/// ```
163#[macro_export]
164macro_rules! impl_version {
165    ($type:ty {
166        version: $version:ident,
167        status: $status:expr $(,)?
168    }) => {
169        impl $crate::registry::Version for $type {
170            fn version_string(&self) -> &$crate::ConcreteVersion {
171                &self.$version
172            }
173
174            fn removal_status(&self) -> $crate::registry::RemovalStatus {
175                ($status)(self)
176            }
177
178            fn as_any(&self) -> &dyn ::std::any::Any {
179                self
180            }
181        }
182    };
183    ($type:ty {
184        version: $version:ident,
185        status: $status:expr,
186        published_at: $published_at:ident $(,)?
187    }) => {
188        impl $crate::registry::Version for $type {
189            fn version_string(&self) -> &$crate::ConcreteVersion {
190                &self.$version
191            }
192
193            fn removal_status(&self) -> $crate::registry::RemovalStatus {
194                ($status)(self)
195            }
196
197            fn published_at(&self) -> Option<$crate::freshness::PublishTime> {
198                self.$published_at
199            }
200
201            fn as_any(&self) -> &dyn ::std::any::Any {
202                self
203            }
204        }
205    };
206    ($type:ty {
207        version: $version:ident,
208        status: $status:expr,
209        prerelease: $prerelease:expr $(,)?
210    }) => {
211        impl $crate::registry::Version for $type {
212            fn version_string(&self) -> &$crate::ConcreteVersion {
213                &self.$version
214            }
215
216            fn removal_status(&self) -> $crate::registry::RemovalStatus {
217                ($status)(self)
218            }
219
220            fn is_prerelease(&self) -> bool {
221                ($prerelease)(self)
222            }
223
224            fn as_any(&self) -> &dyn ::std::any::Any {
225                self
226            }
227        }
228    };
229    ($type:ty {
230        version: $version:ident,
231        status: $status:expr,
232        published_at: $published_at:ident,
233        prerelease: $prerelease:expr $(,)?
234    }) => {
235        impl $crate::registry::Version for $type {
236            fn version_string(&self) -> &$crate::ConcreteVersion {
237                &self.$version
238            }
239
240            fn removal_status(&self) -> $crate::registry::RemovalStatus {
241                ($status)(self)
242            }
243
244            fn published_at(&self) -> Option<$crate::freshness::PublishTime> {
245                self.$published_at
246            }
247
248            fn is_prerelease(&self) -> bool {
249                ($prerelease)(self)
250            }
251
252            fn as_any(&self) -> &dyn ::std::any::Any {
253                self
254            }
255        }
256    };
257    ($type:ty {
258        version: $version:ident,
259        status: $status:expr,
260        published_at: $published_at:ident,
261        prerelease: $prerelease:expr,
262        deprecation: $deprecation:expr $(,)?
263    }) => {
264        impl $crate::registry::Version for $type {
265            fn version_string(&self) -> &$crate::ConcreteVersion {
266                &self.$version
267            }
268
269            fn removal_status(&self) -> $crate::registry::RemovalStatus {
270                ($status)(self)
271            }
272
273            fn published_at(&self) -> Option<$crate::freshness::PublishTime> {
274                self.$published_at
275            }
276
277            fn is_prerelease(&self) -> bool {
278                ($prerelease)(self)
279            }
280
281            fn deprecation(&self) -> Option<&$crate::registry::Deprecation> {
282                // Coerced through a plain `fn` pointer (rather than calling the closure
283                // expression directly) so it is forced to be `for<'a> Fn(&'a Self) ->
284                // Option<&'a Deprecation>` — a bare closure literal here infers a single
285                // concrete lifetime from its first call site instead of generalizing,
286                // which fails to typecheck against `self`'s elided lifetime.
287                let f: fn(&$type) -> Option<&$crate::registry::Deprecation> = $deprecation;
288                f(self)
289            }
290
291            fn as_any(&self) -> &dyn ::std::any::Any {
292                self
293            }
294        }
295    };
296}
297
298/// Implement `Metadata` trait for a struct.
299///
300/// # Arguments
301///
302/// * `$type` - The struct type name
303/// * `name` - Field name for package name (`PackageName`)
304/// * `description` - Field name for description (`Option<String>`)
305/// * `repository` - Field name for repository (`Option<String>`)
306/// * `documentation` - Field name for documentation URL (`Option<String>`)
307/// * `latest_version` - Field name for latest version (`ConcreteVersion`)
308///
309/// # Examples
310///
311/// ```ignore
312/// use deps_core::{ConcreteVersion, PackageName, impl_metadata};
313///
314/// pub struct MyPackage {
315///     pub name: PackageName,
316///     pub description: Option<String>,
317///     pub repository: Option<String>,
318///     pub homepage: Option<String>,
319///     pub latest_version: ConcreteVersion,
320/// }
321///
322/// impl_metadata!(MyPackage {
323///     name: name,
324///     description: description,
325///     repository: repository,
326///     documentation: homepage,
327///     latest_version: latest_version,
328/// });
329/// ```
330#[macro_export]
331macro_rules! impl_metadata {
332    ($type:ty {
333        name: $name:ident,
334        description: $description:ident,
335        repository: $repository:ident,
336        documentation: $documentation:ident,
337        latest_version: $latest_version:ident $(,)?
338    }) => {
339        impl $crate::registry::Metadata for $type {
340            fn name(&self) -> &$crate::PackageName {
341                &self.$name
342            }
343
344            fn description(&self) -> Option<&str> {
345                self.$description.as_deref()
346            }
347
348            fn repository(&self) -> Option<&str> {
349                self.$repository.as_deref()
350            }
351
352            fn documentation(&self) -> Option<&str> {
353                self.$documentation.as_deref()
354            }
355
356            fn latest_version(&self) -> &$crate::ConcreteVersion {
357                &self.$latest_version
358            }
359
360            fn as_any(&self) -> &dyn ::std::any::Any {
361                self
362            }
363        }
364    };
365}
366
367/// Implement `ParseResult` trait for a struct.
368///
369/// # Arguments
370///
371/// * `$type` - The struct type name
372/// * `$dep_type` - The dependency type that implements `Dependency`
373/// * `dependencies` - Field name for dependencies vec (`Vec<DepType>`)
374/// * `uri` - Field name for document URI (`Url`)
375/// * `workspace_root` - Optional: field name for workspace root (`Option<PathBuf>`)
376///
377/// # Examples
378///
379/// ```ignore
380/// use deps_core::impl_parse_result;
381///
382/// pub struct MyParseResult {
383///     pub dependencies: Vec<MyDependency>,
384///     pub uri: Uri,
385/// }
386///
387/// impl_parse_result!(MyParseResult, MyDependency {
388///     dependencies: dependencies,
389///     uri: uri,
390/// });
391///
392/// // With workspace root:
393/// impl_parse_result!(MyParseResult, MyDependency {
394///     dependencies: dependencies,
395///     uri: uri,
396///     workspace_root: workspace_root,
397/// });
398/// ```
399#[macro_export]
400macro_rules! impl_parse_result {
401    ($type:ty, $dep_type:ty {
402        dependencies: $dependencies:ident,
403        uri: $uri:ident $(,)?
404    }) => {
405        impl $crate::ecosystem::ParseResult for $type {
406            fn dependencies(&self) -> Vec<&dyn $crate::ecosystem::Dependency> {
407                self.$dependencies
408                    .iter()
409                    .map(|d| d as &dyn $crate::ecosystem::Dependency)
410                    .collect()
411            }
412
413            fn workspace_root(&self) -> Option<&::std::path::Path> {
414                None
415            }
416
417            fn uri(&self) -> &::tower_lsp_server::ls_types::Uri {
418                &self.$uri
419            }
420
421            fn as_any(&self) -> &dyn ::std::any::Any {
422                self
423            }
424        }
425    };
426    ($type:ty, $dep_type:ty {
427        dependencies: $dependencies:ident,
428        uri: $uri:ident,
429        workspace_root: $workspace_root:ident $(,)?
430    }) => {
431        impl $crate::ecosystem::ParseResult for $type {
432            fn dependencies(&self) -> Vec<&dyn $crate::ecosystem::Dependency> {
433                self.$dependencies
434                    .iter()
435                    .map(|d| d as &dyn $crate::ecosystem::Dependency)
436                    .collect()
437            }
438
439            fn workspace_root(&self) -> Option<&::std::path::Path> {
440                self.$workspace_root.as_deref()
441            }
442
443            fn uri(&self) -> &::tower_lsp_server::ls_types::Uri {
444                &self.$uri
445            }
446
447            fn as_any(&self) -> &dyn ::std::any::Any {
448                self
449            }
450        }
451    };
452}
453
454#[cfg(test)]
455mod tests {
456    use crate::ConcreteVersion;
457    use tower_lsp_server::ls_types::{Position, Range, Uri};
458
459    // Test structs
460    #[derive(Debug, Clone)]
461    struct TestDependency {
462        name: crate::PackageName,
463        name_range: Range,
464        version_req: Option<crate::VersionReq>,
465        version_range: Option<Range>,
466    }
467
468    #[derive(Debug, Clone)]
469    struct TestVersion {
470        version: ConcreteVersion,
471        yanked: bool,
472    }
473
474    #[derive(Debug, Clone)]
475    struct TestVersionWithPublishedAt {
476        version: ConcreteVersion,
477        yanked: bool,
478        published_at: Option<crate::freshness::PublishTime>,
479    }
480
481    #[derive(Debug, Clone)]
482    struct TestVersionWithPrerelease {
483        version: ConcreteVersion,
484        yanked: bool,
485    }
486
487    #[derive(Debug, Clone)]
488    struct TestVersionWithPublishedAtAndPrerelease {
489        version: ConcreteVersion,
490        yanked: bool,
491        published_at: Option<crate::freshness::PublishTime>,
492    }
493
494    #[derive(Debug, Clone)]
495    struct TestPackage {
496        name: crate::PackageName,
497        description: Option<String>,
498        repository: Option<String>,
499        homepage: Option<String>,
500        latest_version: ConcreteVersion,
501    }
502
503    #[derive(Debug)]
504    struct TestParseResult {
505        dependencies: Vec<TestDependency>,
506        uri: Uri,
507    }
508
509    // Apply macros
510    impl_dependency!(TestDependency {
511        name: name,
512        name_range: name_range,
513        version: version_req,
514        version_range: version_range,
515    });
516
517    impl_version!(TestVersion {
518        version: version,
519        status: |v: &TestVersion| crate::registry::RemovalStatus::from_yanked(v.yanked),
520    });
521
522    impl_version!(TestVersionWithPublishedAt {
523        version: version,
524        status: |v: &TestVersionWithPublishedAt| crate::registry::RemovalStatus::from_yanked(
525            v.yanked
526        ),
527        published_at: published_at,
528    });
529
530    impl_version!(TestVersionWithPrerelease {
531        version: version,
532        status: |v: &TestVersionWithPrerelease| crate::registry::RemovalStatus::from_yanked(
533            v.yanked
534        ),
535        prerelease: |v: &TestVersionWithPrerelease| v.version.as_str().contains(".pre"),
536    });
537
538    impl_version!(TestVersionWithPublishedAtAndPrerelease {
539        version: version,
540        status: |v: &TestVersionWithPublishedAtAndPrerelease| {
541            crate::registry::RemovalStatus::from_yanked(v.yanked)
542        },
543        published_at: published_at,
544        prerelease: |v: &TestVersionWithPublishedAtAndPrerelease| v
545            .version
546            .as_str()
547            .contains(".pre"),
548    });
549
550    impl_metadata!(TestPackage {
551        name: name,
552        description: description,
553        repository: repository,
554        documentation: homepage,
555        latest_version: latest_version,
556    });
557
558    impl_parse_result!(
559        TestParseResult,
560        TestDependency {
561            dependencies: dependencies,
562            uri: uri,
563        }
564    );
565
566    #[test]
567    fn test_impl_dependency_macro() {
568        use crate::ecosystem::Dependency;
569
570        let dep = TestDependency {
571            name: "test-pkg".into(),
572            name_range: Range::new(Position::new(0, 0), Position::new(0, 8)),
573            version_req: Some("1.0.0".into()),
574            version_range: Some(Range::new(Position::new(0, 10), Position::new(0, 15))),
575        };
576
577        assert_eq!(dep.name(), "test-pkg");
578        assert_eq!(
579            dep.version_requirement().map(crate::VersionReq::as_str),
580            Some("1.0.0")
581        );
582        assert!(dep.as_any().is::<TestDependency>());
583    }
584
585    #[test]
586    fn test_impl_version_macro() {
587        use crate::registry::Version;
588
589        let version = TestVersion {
590            version: "2.0.0".into(),
591            yanked: true,
592        };
593
594        assert_eq!(version.version_string().as_str(), "2.0.0");
595        assert!(version.removal_status().blocks_resolution());
596        assert!(version.as_any().is::<TestVersion>());
597        assert!(version.published_at().is_none());
598    }
599
600    #[test]
601    fn test_impl_version_macro_with_published_at() {
602        use crate::freshness::PublishTime;
603        use crate::registry::Version;
604
605        let version = TestVersionWithPublishedAt {
606            version: "3.0.0".into(),
607            yanked: false,
608            published_at: Some(PublishTime::from_unix_secs(1_000)),
609        };
610
611        assert_eq!(version.version_string().as_str(), "3.0.0");
612        assert!(!version.removal_status().blocks_resolution());
613        assert_eq!(
614            version.published_at(),
615            Some(PublishTime::from_unix_secs(1_000))
616        );
617        assert!(version.as_any().is::<TestVersionWithPublishedAt>());
618    }
619
620    #[test]
621    fn test_impl_version_macro_with_prerelease() {
622        use crate::registry::Version;
623
624        let stable = TestVersionWithPrerelease {
625            version: "1.0.0".into(),
626            yanked: false,
627        };
628        let prerelease = TestVersionWithPrerelease {
629            version: "1.0.0.pre1".into(),
630            yanked: false,
631        };
632
633        assert!(!stable.is_prerelease());
634        assert!(prerelease.is_prerelease());
635        assert!(stable.is_stable());
636        assert!(!prerelease.is_stable());
637    }
638
639    #[test]
640    fn test_impl_version_macro_with_published_at_and_prerelease() {
641        use crate::freshness::PublishTime;
642        use crate::registry::Version;
643
644        let version = TestVersionWithPublishedAtAndPrerelease {
645            version: "2.0.0.pre1".into(),
646            yanked: false,
647            published_at: Some(PublishTime::from_unix_secs(2_000)),
648        };
649
650        assert!(version.is_prerelease());
651        assert_eq!(
652            version.published_at(),
653            Some(PublishTime::from_unix_secs(2_000))
654        );
655    }
656
657    #[test]
658    fn test_impl_metadata_macro() {
659        use crate::registry::Metadata;
660
661        let pkg = TestPackage {
662            name: crate::PackageName::new("my-pkg"),
663            description: Some("A test package".into()),
664            repository: Some("user/repo".into()),
665            homepage: Some("https://example.com".into()),
666            latest_version: "3.0.0".into(),
667        };
668
669        assert_eq!(pkg.name(), "my-pkg");
670        assert_eq!(pkg.description(), Some("A test package"));
671        assert_eq!(pkg.documentation(), Some("https://example.com"));
672        assert!(pkg.as_any().is::<TestPackage>());
673    }
674
675    #[test]
676    fn test_impl_parse_result_macro() {
677        use crate::ecosystem::ParseResult;
678
679        let result = TestParseResult {
680            dependencies: vec![TestDependency {
681                name: "dep1".into(),
682                name_range: Range::default(),
683                version_req: None,
684                version_range: None,
685            }],
686            uri: crate::test_util::test_uri("/test"),
687        };
688
689        assert_eq!(result.dependencies().len(), 1);
690        assert!(result.workspace_root().is_none());
691        assert!(result.as_any().is::<TestParseResult>());
692    }
693}