schemadoc_diff_derive/
lib.rs

1extern crate proc_macro;
2
3use proc_macro::TokenStream;
4use proc_macro2::{Ident, Span};
5
6use quote::quote;
7use syn::{Data, Field, Fields, GenericArgument, Path, PathArguments, Type, TypePath};
8
9#[derive(Debug)]
10enum FieldType {
11    DiffResult,
12    BoxedDiffResult,
13    Other,
14}
15
16impl FieldType {
17    pub fn is_other(&self) -> bool {
18        matches!(self, FieldType::Other)
19    }
20}
21
22fn inner_type(path: &Path) -> Option<&Type> {
23    if path.leading_colon.is_some() {
24        return None;
25    }
26
27    if path.segments.len() != 1 {
28        return None;
29    }
30
31    let ab = match &path.segments[0].arguments {
32        PathArguments::AngleBracketed(ab) => ab,
33        _ => return None,
34    };
35
36    if ab.args.len() != 1 {
37        return None;
38    }
39
40    match &ab.args[0] {
41        GenericArgument::Type(t) => Some(t),
42        _ => None,
43    }
44}
45
46enum CoFieldType {
47    Other,
48    ContainerDiffResult,
49    PrimitiveDiffResult,
50}
51
52
53fn is_primitive_type(path: &Path) -> bool {
54    match path.get_ident() {
55        None => false,
56        Some(ident) => {
57            matches!(ident.to_string().as_ref(), "String" | "usize" | "bool" | "f32" | "Value")
58        }
59    }
60}
61
62
63fn co_get_field_type(field: &Field) -> CoFieldType {
64    match &field.ty {
65        Type::Path(ty) => {
66            let ident = &ty.path.segments[0].ident;
67
68            if ident == "Box" {
69                match inner_type(&ty.path) {
70                    Some(Type::Path(ty @ TypePath { path, .. })) => {
71                        let ident = &path.segments[0].ident;
72                        if ident == "DiffResult" {
73                            match inner_type(&ty.path) {
74                                Some(Type::Path(TypePath { path, .. })) => {
75                                    if is_primitive_type(path) {
76                                        CoFieldType::PrimitiveDiffResult
77                                    } else {
78                                        CoFieldType::Other
79                                    }
80                                }
81                                _ => unreachable!("DiffResult must have generic type."),
82                            }
83                        } else {
84                            CoFieldType::Other
85                        }
86                    }
87                    None => unreachable!("Box must have generic type."),
88                    _ => CoFieldType::Other
89                }
90            } else if ident == "DiffResult" {
91                match inner_type(&ty.path) {
92                    Some(Type::Path(TypePath { path, .. })) => {
93                        if is_primitive_type(path) {
94                            CoFieldType::PrimitiveDiffResult
95                        } else if matches!(path.segments[0].ident.to_string().as_ref(), "VecDiff" | "MapDiff") {
96                            CoFieldType::ContainerDiffResult
97                        } else {
98                            CoFieldType::Other
99                        }
100                    }
101                    _ => unreachable!("DiffResult must have generic type."),
102                }
103            } else {
104                CoFieldType::Other
105            }
106        }
107        _ => CoFieldType::Other
108    }
109}
110
111#[proc_macro_derive(DiffOwnChanges)]
112pub fn diff_own_changes_proc_macro(input: TokenStream) -> TokenStream {
113    let syn::DeriveInput {
114        ident,
115        data,
116        ..
117    } = syn::parse_macro_input!(input as syn::DeriveInput);
118
119    let data = match data {
120        Data::Struct(data) => data,
121        _ => panic!("Only structs are supported"),
122    };
123
124    let fields = match data.fields {
125        Fields::Named(fields) => fields.named,
126        _ => panic!("Only structs with names fields are supported")
127    };
128
129    let diff_result_fields: Vec<_> = fields
130        .iter()
131        .map(|field| (field, co_get_field_type(field)))
132        .filter(|(_, field_type)| !matches!(field_type, CoFieldType::Other))
133        .collect();
134
135    let field_idents: Vec<_> = diff_result_fields
136        .iter()
137        .map(|(field, field_type)| {
138            let field_ident = field.ident.as_ref().unwrap();
139            let field_name = field_ident.to_string();
140
141            if matches!(field_type, CoFieldType::PrimitiveDiffResult) {
142                quote! {
143                    if !self.#field_ident.is_same_or_none() {
144                        changes.push((#field_name.into(), (&self.#field_ident).into()))
145                    }
146                }
147            } else {
148                quote! {
149                     if !self.#field_ident.is_same_or_none() {
150                        changes.extend(self.#field_ident.get_own_changes())
151                    }
152                }
153            }
154        }).collect();
155
156    let expanded = quote! {
157        impl crate::diff_own_changes::DiffOwnChanges for #ident{
158             fn get_own_changes(&self) -> Vec<(::std::borrow::Cow<str>, crate::diff_result_type::DiffResultType)> {
159                let mut changes = Vec::new();
160
161                 #(#field_idents)*
162
163                changes
164             }
165        }
166    };
167
168    TokenStream::from(expanded)
169}
170
171fn get_field_type(field: &Field) -> FieldType {
172    match &field.ty {
173        Type::Path(ty) => {
174            let ident = &ty.path.segments[0].ident;
175
176            if ident == "Box" {
177                match inner_type(&ty.path) {
178                    Some(Type::Path(ty)) => {
179                        let ident = &ty.path.segments[0].ident;
180                        if ident == "DiffResult" {
181                            FieldType::BoxedDiffResult
182                        } else {
183                            FieldType::Other
184                        }
185                    }
186                    None => unreachable!("Box must have generic type."),
187                    _ => FieldType::Other
188                }
189            } else if ident == "DiffResult" {
190                FieldType::DiffResult
191            } else {
192                FieldType::Other
193            }
194        }
195        _ => FieldType::Other
196    }
197}
198
199
200#[proc_macro_derive(Empty)]
201pub fn is_empty_proc_macro(input: TokenStream) -> TokenStream {
202    let syn::DeriveInput {
203        ident,
204        data,
205        ..
206    } = syn::parse_macro_input!(input as syn::DeriveInput);
207
208    let data = match data {
209        Data::Struct(data) => data,
210        _ => panic!("Only structs are supported"),
211    };
212
213    let fields = match data.fields {
214        Fields::Named(fields) => fields.named,
215        _ => panic!("Only structs with names fields are supported")
216    };
217    let diff_result_fields: Vec<_> = fields
218        .iter()
219        .filter(|field| !get_field_type(field).is_other())
220        .collect();
221
222    let field_idents = diff_result_fields
223        .iter()
224        .enumerate()
225        .map(|(i, field)| {
226            let field_ident = field.ident.as_ref().unwrap();
227
228            let delim = if i < diff_result_fields.len() - 1 {
229                quote! { && }
230            } else {
231                quote! {}
232            };
233
234            quote! { self.#field_ident.is_same_or_none() #delim}
235        });
236
237    let expanded = quote! {
238        impl crate::core::Empty for #ident{
239             fn is_empty(&self) -> bool {
240                 #(#field_idents)*
241            }
242        }
243    };
244
245    TokenStream::from(expanded)
246}
247
248enum FieldTypeDiff {
249    Box,
250    Other,
251    DiffResult,
252}
253
254fn get_field_diff_type(field: &Field) -> FieldTypeDiff {
255    match &field.ty {
256        Type::Path(p) => {
257            let ident = &p.path.segments[0].ident;
258
259            if ident == "Box" {
260                FieldTypeDiff::Box
261            } else if ident == "DiffResult" {
262                FieldTypeDiff::DiffResult
263            } else {
264                FieldTypeDiff::Other
265            }
266        }
267        _ => FieldTypeDiff::Other
268    }
269}
270
271
272#[proc_macro_derive(Diff)]
273pub fn diff_proc_macro(input: TokenStream) -> TokenStream {
274    let syn::DeriveInput {
275        ident: diff_ident,
276        data,
277        ..
278    } = syn::parse_macro_input!(input as syn::DeriveInput);
279
280    let data = match data {
281        Data::Struct(data) => data,
282        _ => panic!("Only structs are supported"),
283    };
284
285    let fields = match data.fields {
286        Fields::Named(fields) => fields.named,
287        _ => panic!("Only structs with names fields are supported")
288    };
289
290    let fields: Vec<_> = fields
291        .iter()
292        .map(|field| (field, get_field_diff_type(field)))
293        .collect();
294
295    let removed_idents = fields
296        .iter()
297        .map(|(field, ty)| {
298            let field_ident = field.ident.as_ref().unwrap();
299            if matches!(ty, FieldTypeDiff::Box) {
300                quote! { #field_ident: Box::new(self.#field_ident.diff(None, &context.removing())), }
301            } else if matches!(ty, FieldTypeDiff::DiffResult) {
302                quote! { #field_ident: self.#field_ident.diff(None, &context.removing()), }
303            } else {
304                quote! { #field_ident: self.#field_ident.clone(), }
305            }
306        });
307
308    let updated_idents = fields
309        .iter()
310        .map(|(field, ty)| {
311            let field_ident = field.ident.as_ref().unwrap();
312
313            if matches!(ty, FieldTypeDiff::Box) {
314                quote! { #field_ident: Box::new(self.#field_ident.diff(Option::from(&*value.#field_ident), context)), }
315            } else if matches!(ty, FieldTypeDiff::DiffResult) {
316                quote! { #field_ident: self.#field_ident.diff(Option::from(&value.#field_ident), context), }
317            } else {
318                quote! { #field_ident: self.#field_ident.clone(), }
319            }
320        });
321
322    let diff_ident_name = diff_ident.to_string();
323    let ident = Ident::new(
324        &diff_ident_name.replace("Diff", ""),
325        Span::call_site(),
326    );
327
328    let expanded = quote! {
329        impl Diff<crate::schema::#ident, #diff_ident, crate::context::HttpSchemaDiffContext> for crate::schema::#ident {
330            fn diff(
331                &self,
332                new: Option<&crate::schema::#ident>,
333                context: &crate::context::HttpSchemaDiffContext,
334            ) -> DiffResult<#diff_ident> {
335                let diff = match new {
336                    None => DiffResult::Removed(#diff_ident {
337                        #(#removed_idents)*
338                    }),
339                    Some(value) => {
340                        let diff = #diff_ident {
341                            #(#updated_idents)*
342                        };
343
344                        if diff.is_empty() {
345                            DiffResult::Same(diff)
346                        } else {
347                            DiffResult::Updated(diff, None)
348                        }
349                    }
350                };
351                DiffResult::new(diff, context)
352            }
353        }
354    };
355
356    TokenStream::from(expanded)
357}
358
359
360#[cfg(test)]
361mod tests {}