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 `Dependency` and `DependencyInfo` traits for a struct.
6///
7/// # Arguments
8///
9/// * `$type` - The struct type name
10/// * `name` - Field name for the dependency name (`String`)
11/// * `name_range` - Field name for the name range (`Range`)
12/// * `version` - Field name for version requirement (`Option<String>`)
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;
20///
21/// pub struct MyDependency {
22///     pub name: String,
23///     pub name_range: Range,
24///     pub version_req: Option<String>,
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::parser::DependencyInfo for $type {
59            fn name(&self) -> &str {
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<&str> {
68                self.$version.as_deref()
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
80        impl $crate::ecosystem::Dependency for $type {
81            fn name(&self) -> &str {
82                &self.$name
83            }
84
85            fn name_range(&self) -> ::tower_lsp_server::ls_types::Range {
86                self.$name_range
87            }
88
89            fn version_requirement(&self) -> Option<&str> {
90                self.$version.as_deref()
91            }
92
93            fn version_range(&self) -> Option<::tower_lsp_server::ls_types::Range> {
94                self.$version_range
95            }
96
97            fn source(&self) -> $crate::parser::DependencySource {
98                $source
99            }
100
101            fn as_any(&self) -> &dyn ::std::any::Any {
102                self
103            }
104        }
105    };
106}
107
108/// Implement `Version` trait for a struct.
109///
110/// # Arguments
111///
112/// * `$type` - The struct type name
113/// * `version` - Field name for version string (`String`)
114/// * `yanked` - Field name for yanked/deprecated status (`bool`)
115///
116/// # Examples
117///
118/// ```ignore
119/// use deps_core::impl_version;
120///
121/// pub struct MyVersion {
122///     pub version: String,
123///     pub deprecated: bool,
124/// }
125///
126/// impl_version!(MyVersion {
127///     version: version,
128///     yanked: deprecated,
129/// });
130/// ```
131#[macro_export]
132macro_rules! impl_version {
133    ($type:ty {
134        version: $version:ident,
135        yanked: $yanked:ident $(,)?
136    }) => {
137        impl $crate::registry::Version for $type {
138            fn version_string(&self) -> &str {
139                &self.$version
140            }
141
142            fn is_yanked(&self) -> bool {
143                self.$yanked
144            }
145
146            fn as_any(&self) -> &dyn ::std::any::Any {
147                self
148            }
149        }
150    };
151}
152
153/// Implement `Metadata` trait for a struct.
154///
155/// # Arguments
156///
157/// * `$type` - The struct type name
158/// * `name` - Field name for package name (`String`)
159/// * `description` - Field name for description (`Option<String>`)
160/// * `repository` - Field name for repository (`Option<String>`)
161/// * `documentation` - Field name for documentation URL (`Option<String>`)
162/// * `latest_version` - Field name for latest version (`String`)
163///
164/// # Examples
165///
166/// ```ignore
167/// use deps_core::impl_metadata;
168///
169/// pub struct MyPackage {
170///     pub name: String,
171///     pub description: Option<String>,
172///     pub repository: Option<String>,
173///     pub homepage: Option<String>,
174///     pub latest_version: String,
175/// }
176///
177/// impl_metadata!(MyPackage {
178///     name: name,
179///     description: description,
180///     repository: repository,
181///     documentation: homepage,
182///     latest_version: latest_version,
183/// });
184/// ```
185#[macro_export]
186macro_rules! impl_metadata {
187    ($type:ty {
188        name: $name:ident,
189        description: $description:ident,
190        repository: $repository:ident,
191        documentation: $documentation:ident,
192        latest_version: $latest_version:ident $(,)?
193    }) => {
194        impl $crate::registry::Metadata for $type {
195            fn name(&self) -> &str {
196                &self.$name
197            }
198
199            fn description(&self) -> Option<&str> {
200                self.$description.as_deref()
201            }
202
203            fn repository(&self) -> Option<&str> {
204                self.$repository.as_deref()
205            }
206
207            fn documentation(&self) -> Option<&str> {
208                self.$documentation.as_deref()
209            }
210
211            fn latest_version(&self) -> &str {
212                &self.$latest_version
213            }
214
215            fn as_any(&self) -> &dyn ::std::any::Any {
216                self
217            }
218        }
219    };
220}
221
222/// Implement `ParseResult` trait for a struct.
223///
224/// # Arguments
225///
226/// * `$type` - The struct type name
227/// * `$dep_type` - The dependency type that implements `Dependency`
228/// * `dependencies` - Field name for dependencies vec (`Vec<DepType>`)
229/// * `uri` - Field name for document URI (`Url`)
230/// * `workspace_root` - Optional: field name for workspace root (`Option<PathBuf>`)
231///
232/// # Examples
233///
234/// ```ignore
235/// use deps_core::impl_parse_result;
236///
237/// pub struct MyParseResult {
238///     pub dependencies: Vec<MyDependency>,
239///     pub uri: Uri,
240/// }
241///
242/// impl_parse_result!(MyParseResult, MyDependency {
243///     dependencies: dependencies,
244///     uri: uri,
245/// });
246///
247/// // With workspace root:
248/// impl_parse_result!(MyParseResult, MyDependency {
249///     dependencies: dependencies,
250///     uri: uri,
251///     workspace_root: workspace_root,
252/// });
253/// ```
254#[macro_export]
255macro_rules! impl_parse_result {
256    ($type:ty, $dep_type:ty {
257        dependencies: $dependencies:ident,
258        uri: $uri:ident $(,)?
259    }) => {
260        impl $crate::ecosystem::ParseResult for $type {
261            fn dependencies(&self) -> Vec<&dyn $crate::ecosystem::Dependency> {
262                self.$dependencies
263                    .iter()
264                    .map(|d| d as &dyn $crate::ecosystem::Dependency)
265                    .collect()
266            }
267
268            fn workspace_root(&self) -> Option<&::std::path::Path> {
269                None
270            }
271
272            fn uri(&self) -> &::tower_lsp_server::ls_types::Uri {
273                &self.$uri
274            }
275
276            fn as_any(&self) -> &dyn ::std::any::Any {
277                self
278            }
279        }
280    };
281    ($type:ty, $dep_type:ty {
282        dependencies: $dependencies:ident,
283        uri: $uri:ident,
284        workspace_root: $workspace_root:ident $(,)?
285    }) => {
286        impl $crate::ecosystem::ParseResult for $type {
287            fn dependencies(&self) -> Vec<&dyn $crate::ecosystem::Dependency> {
288                self.$dependencies
289                    .iter()
290                    .map(|d| d as &dyn $crate::ecosystem::Dependency)
291                    .collect()
292            }
293
294            fn workspace_root(&self) -> Option<&::std::path::Path> {
295                self.$workspace_root.as_deref()
296            }
297
298            fn uri(&self) -> &::tower_lsp_server::ls_types::Uri {
299                &self.$uri
300            }
301
302            fn as_any(&self) -> &dyn ::std::any::Any {
303                self
304            }
305        }
306    };
307}
308
309/// Delegate a method call to all enum variants.
310///
311/// This macro generates a match expression that delegates to the same
312/// method on each enum variant, eliminating boilerplate.
313///
314/// # Examples
315///
316/// ```ignore
317/// impl UnifiedDependency {
318///     pub fn name(&self) -> &str {
319///         delegate_to_variants!(self, name)
320///     }
321/// }
322/// ```
323#[macro_export]
324macro_rules! delegate_to_variants {
325    ($self:ident, $method:ident $(, $arg:expr)*) => {
326        match $self {
327            Self::Cargo(dep) => dep.$method($($arg),*),
328            Self::Npm(dep) => dep.$method($($arg),*),
329            Self::Pypi(dep) => dep.$method($($arg),*),
330        }
331    };
332}
333
334#[cfg(test)]
335mod tests {
336    use tower_lsp_server::ls_types::{Position, Range, Uri};
337
338    // Test structs
339    #[derive(Debug, Clone)]
340    struct TestDependency {
341        name: String,
342        name_range: Range,
343        version_req: Option<String>,
344        version_range: Option<Range>,
345    }
346
347    #[derive(Debug, Clone)]
348    struct TestVersion {
349        version: String,
350        yanked: bool,
351    }
352
353    #[derive(Debug, Clone)]
354    struct TestPackage {
355        name: String,
356        description: Option<String>,
357        repository: Option<String>,
358        homepage: Option<String>,
359        latest_version: String,
360    }
361
362    #[derive(Debug)]
363    struct TestParseResult {
364        dependencies: Vec<TestDependency>,
365        uri: Uri,
366    }
367
368    // Apply macros
369    impl_dependency!(TestDependency {
370        name: name,
371        name_range: name_range,
372        version: version_req,
373        version_range: version_range,
374    });
375
376    impl_version!(TestVersion {
377        version: version,
378        yanked: yanked,
379    });
380
381    impl_metadata!(TestPackage {
382        name: name,
383        description: description,
384        repository: repository,
385        documentation: homepage,
386        latest_version: latest_version,
387    });
388
389    impl_parse_result!(
390        TestParseResult,
391        TestDependency {
392            dependencies: dependencies,
393            uri: uri,
394        }
395    );
396
397    #[test]
398    fn test_impl_dependency_macro() {
399        use crate::ecosystem::Dependency;
400
401        let dep = TestDependency {
402            name: "test-pkg".into(),
403            name_range: Range::new(Position::new(0, 0), Position::new(0, 8)),
404            version_req: Some("1.0.0".into()),
405            version_range: Some(Range::new(Position::new(0, 10), Position::new(0, 15))),
406        };
407
408        assert_eq!(dep.name(), "test-pkg");
409        assert_eq!(dep.version_requirement(), Some("1.0.0"));
410        assert!(dep.as_any().is::<TestDependency>());
411    }
412
413    #[test]
414    fn test_impl_version_macro() {
415        use crate::registry::Version;
416
417        let version = TestVersion {
418            version: "2.0.0".into(),
419            yanked: true,
420        };
421
422        assert_eq!(version.version_string(), "2.0.0");
423        assert!(version.is_yanked());
424        assert!(version.as_any().is::<TestVersion>());
425    }
426
427    #[test]
428    fn test_impl_metadata_macro() {
429        use crate::registry::Metadata;
430
431        let pkg = TestPackage {
432            name: "my-pkg".into(),
433            description: Some("A test package".into()),
434            repository: Some("user/repo".into()),
435            homepage: Some("https://example.com".into()),
436            latest_version: "3.0.0".into(),
437        };
438
439        assert_eq!(pkg.name(), "my-pkg");
440        assert_eq!(pkg.description(), Some("A test package"));
441        assert_eq!(pkg.documentation(), Some("https://example.com"));
442        assert!(pkg.as_any().is::<TestPackage>());
443    }
444
445    #[test]
446    fn test_impl_parse_result_macro() {
447        use crate::ecosystem::ParseResult;
448
449        let result = TestParseResult {
450            dependencies: vec![TestDependency {
451                name: "dep1".into(),
452                name_range: Range::default(),
453                version_req: None,
454                version_range: None,
455            }],
456            uri: Uri::from_file_path("/test").unwrap(),
457        };
458
459        assert_eq!(result.dependencies().len(), 1);
460        assert!(result.workspace_root().is_none());
461        assert!(result.as_any().is::<TestParseResult>());
462    }
463}