codama_syn_helpers/extensions/
path.rs

1use super::ToTokensExtension;
2use codama_errors::CodamaResult;
3use syn::{Path, PathArguments, PathSegment};
4
5pub trait PathExtension {
6    fn get_self(&self) -> &Path;
7
8    /// Returns all segment idents as strings
9    fn idents(&self) -> Vec<String> {
10        let this = self.get_self();
11        this.segments
12            .iter()
13            .map(|segment| segment.ident.to_string())
14            .collect::<Vec<_>>()
15    }
16
17    /// Returns all segment idents joined by "::".
18    /// E.g. for `a::b<B>::c::Option<T>` it returns `a::b::c::Option`.
19    fn to_string(&self) -> String {
20        self.idents().join("::")
21    }
22
23    /// Returns all segment idents joined by "::" except the last one.
24    /// E.g. for `a::b<B>::c::Option<T>` it returns `a::b::c`.
25    fn prefix(&self) -> String {
26        let idents = self.idents();
27        idents[..idents.len() - 1].join("::")
28    }
29
30    /// Returns the last segment.
31    fn last(&self) -> &PathSegment {
32        self.get_self().segments.last().unwrap()
33    }
34
35    /// Returns the ident of the last segment as a string.
36    fn last_str(&self) -> String {
37        self.last().ident.to_string()
38    }
39
40    /// Returns true if the path is equal to the given path including or excluding the prefix.
41    fn is(&self, path: &str) -> bool {
42        let mut segments = path.split("::").collect::<Vec<_>>();
43        let last = segments.pop().unwrap();
44        let prefix = segments.join("::");
45        let this_prefix = self.prefix();
46        (this_prefix == prefix || this_prefix.is_empty()) && last == self.last_str()
47    }
48
49    /// Returns true if the path is equal to the given path including the prefix.
50    fn is_strict(&self, path: &str) -> bool {
51        let mut segments = path.split("::").collect::<Vec<_>>();
52        let last = segments.pop().unwrap();
53        let prefix = segments.join("::");
54        prefix == self.prefix() && last == self.last_str()
55    }
56
57    /// Returns the generic arguments of the last segment.
58    /// E.g. for `a::b::c::Option<'a, T, U>` it returns `GenericArguments(Some(['a, T, U]))`.
59    /// E.g. for `a::b::c::u32` it returns `GenericArguments(None)`.
60    fn generic_arguments(&self) -> Vec<&syn::GenericArgument> {
61        match &self.last().arguments {
62            PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments { args, .. }) => {
63                args.iter().collect()
64            }
65            _ => vec![],
66        }
67    }
68
69    /// Filters out all generic arguments that are not types.
70    /// E.g. for `Option<'a, T, U>` it returns `[T, U]`.
71    fn generic_types(&self) -> Vec<&syn::Type> {
72        self.generic_arguments()
73            .iter()
74            .filter_map(|arg| match arg {
75                syn::GenericArgument::Type(ty) => Some(ty),
76                _ => None,
77            })
78            .collect()
79    }
80
81    /// Returns the first generic type argument if there is one.
82    /// E.g. for `Vec<'a, T, U>` it returns `Ok(T)`.
83    fn first_generic_type(&self) -> CodamaResult<&syn::Type> {
84        let this = self.get_self();
85        self.generic_types()
86            .first()
87            .copied()
88            .ok_or_else(|| this.error("expected at least one generic type").into())
89    }
90
91    /// Returns the first generic type argument if there is exactly one.
92    /// E.g. for `Vec<'a, T>` it returns `Ok(T)`.
93    fn single_generic_type(&self) -> CodamaResult<&syn::Type> {
94        let this = self.get_self();
95        if self.generic_types().len() != 1 {
96            return Err(this
97                .error("expected a single generic type".to_string())
98                .into());
99        }
100        self.first_generic_type()
101    }
102}
103
104impl PathExtension for Path {
105    fn get_self(&self) -> &Path {
106        self
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn idents() {
116        let path: Path = syn::parse_quote! { std::option<Foo>::Option<String> };
117        assert_eq!(path.idents(), vec!["std", "option", "Option"]);
118    }
119
120    #[test]
121    fn to_string() {
122        let path: Path = syn::parse_quote! { std::option<Foo>::Option<String> };
123        assert_eq!(path.to_string(), "std::option::Option");
124    }
125
126    #[test]
127    fn prefix() {
128        let path: Path = syn::parse_quote! { std::option<Foo>::Option<String> };
129        assert_eq!(path.prefix(), "std::option");
130    }
131
132    #[test]
133    fn prefix_with_inner_generics() {
134        let path: Path = syn::parse_quote! { a<A>::b<B>::c::Final };
135        assert_eq!(path.prefix(), "a::b::c");
136    }
137
138    #[test]
139    fn prefix_empty() {
140        let path: Path = syn::parse_quote! { Foo };
141        assert_eq!(path.prefix(), "");
142    }
143
144    #[test]
145    fn is() {
146        let path: Path = syn::parse_quote! { prefix::Foo<'a, T> };
147        assert!(path.is("prefix::Foo"));
148        assert!(!path.is("Foo"));
149        assert!(!path.is("wrong::prefix::Foo"));
150        assert!(!path.is("Bar"));
151
152        let path: Path = syn::parse_quote! { Foo<T> };
153        assert!(path.is("Foo"));
154        assert!(path.is("prefix::Foo"));
155        assert!(!path.is("Bar"));
156    }
157
158    #[test]
159    fn is_strict() {
160        let path: Path = syn::parse_quote! { prefix::Foo<'a, T> };
161        assert!(path.is_strict("prefix::Foo"));
162        assert!(!path.is_strict("Foo"));
163        assert!(!path.is_strict("wrong::prefix::Foo"));
164        assert!(!path.is_strict("Bar"));
165
166        let path: Path = syn::parse_quote! { Foo<T> };
167        assert!(path.is_strict("Foo"));
168        assert!(!path.is_strict("prefix::Foo"));
169        assert!(!path.is_strict("Bar"));
170    }
171
172    #[test]
173    fn generic_arguments() {
174        let path: Path = syn::parse_quote! { prefix::Foo<'a, T, U> };
175        assert_eq!(path.generic_arguments().len(), 3);
176    }
177
178    #[test]
179    fn generic_types() {
180        let path: Path = syn::parse_quote! { prefix::Foo<'a, T, U> };
181        assert_eq!(path.generic_types().len(), 2);
182    }
183
184    #[test]
185    fn first_generic_type_ok() {
186        let path: Path = syn::parse_quote! { prefix::Foo<'a, T, U> };
187        assert!(matches!(path.first_generic_type(), Ok(syn::Type::Path(_))));
188    }
189
190    #[test]
191    fn first_generic_type_err() {
192        let path: Path = syn::parse_quote! { prefix::Foo<'a> };
193        assert!(path.first_generic_type().is_err());
194    }
195
196    #[test]
197    fn single_generic_type_ok() {
198        let path: Path = syn::parse_quote! { Foo<'a, String> };
199        assert!(matches!(path.single_generic_type(), Ok(syn::Type::Path(_))));
200    }
201
202    #[test]
203    fn single_generic_type_err() {
204        let path: Path = syn::parse_quote! { Foo<'a, String, u32> };
205        assert!(path.single_generic_type().is_err());
206
207        let path: Path = syn::parse_quote! { Foo<'a> };
208        assert!(path.single_generic_type().is_err());
209    }
210}