scaffolding_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::{quote, ToTokens};
3use syn::parse::{Parse, ParseStream, Parser, Result};
4use syn::Expr::Struct;
5use syn::FieldValue;
6use syn::Member;
7use syn::{parse_macro_input, parse_quote, punctuated::Punctuated, ItemStruct, LitStr, Token};
8
9static ADDRESS: &str = "addresses";
10static EMAIL: &str = "email_addresses";
11static METADATA: &str = "metadata";
12static PHONE: &str = "phone_numbers";
13static NOTES: &str = "notes";
14static TAGS: &str = "tags";
15static CORE_ATTRS: [&str; 6] = [
16    "id",
17    "created_dtm",
18    "modified_dtm",
19    "inactive_dtm",
20    "expired_dtm",
21    "activity",
22];
23
24/// Modifying a struct
25///
26/// Dynammically adds the following public attributes to the struct
27/// + id: String
28/// + created_dtm: i64
29/// + modified_dtm: i64
30/// + inactive_dtm: i64
31/// + expired_dtm: i64
32/// + activity: Vec<ActivityItem>
33///
34/// Optionally
35/// + addresses: BTreeMap<String, Address>
36/// + metadata: BTreeMap<String, String>
37/// + notes: BTreeMap<String, Note>
38/// + phone_numbers: BTreeMap<String, PhoneNumber>
39/// + tags: Vec<String>
40///
41#[proc_macro_attribute]
42pub fn scaffolding_struct(args: TokenStream, input: TokenStream) -> TokenStream {
43    let mut item_struct: ItemStruct = parse_macro_input!(input as ItemStruct);
44    let attrs = parse_macro_input!(args as Args)
45        .vars
46        .iter()
47        .map(|a| a.value())
48        .collect::<Vec<_>>();
49
50    if let syn::Fields::Named(ref mut fields) = item_struct.fields {
51        // The unique identifier of the object
52        fields.named.push(
53            syn::Field::parse_named
54                .parse2(quote! { pub id: String })
55                .unwrap(),
56        );
57        // The timestamp when the object was created
58        fields.named.push(
59            syn::Field::parse_named
60                .parse2(quote! { pub created_dtm: i64 })
61                .unwrap(),
62        );
63        // The timestamp when the object was last modified
64        fields.named.push(
65            syn::Field::parse_named
66                .parse2(quote! { pub modified_dtm: i64 })
67                .unwrap(),
68        );
69        // The timestamp when the object is no longer active
70        fields.named.push(
71            syn::Field::parse_named
72                .parse2(quote! { pub inactive_dtm: i64 })
73                .unwrap(),
74        );
75        // The timestamp when the object is expired
76        fields.named.push(
77            syn::Field::parse_named
78                .parse2(quote! { pub expired_dtm: i64 })
79                .unwrap(),
80        );
81
82        // The list of activity performed on the object
83        fields.named.push(
84            syn::Field::parse_named
85                .parse2(quote! { pub activity: Vec<ActivityItem> })
86                .unwrap(),
87        );
88
89        // optional attributes
90        match attrs.contains(&ADDRESS.to_string()) {
91            true => {
92                // The address handler
93                fields.named.push(
94                    syn::Field::parse_named
95                        .parse2(quote! { pub addresses: BTreeMap<String, Address> })
96                        .unwrap(),
97                );
98            }
99            false => {}
100        }
101
102        match attrs.contains(&EMAIL.to_string()) {
103            true => {
104                // The phonenumber handler
105                fields.named.push(
106                    syn::Field::parse_named
107                        .parse2(quote! { pub email_addresses: BTreeMap<String, EmailAddress> })
108                        .unwrap(),
109                );
110            }
111            false => {}
112        }
113
114        // optional attributes
115        match attrs.contains(&METADATA.to_string()) {
116            true => {
117                // The metadata handler
118                fields.named.push(
119                    syn::Field::parse_named
120                        .parse2(quote! { pub metadata: BTreeMap<String, String> })
121                        .unwrap(),
122                );
123            }
124            false => {}
125        }
126
127        // optional attributes
128        match attrs.contains(&NOTES.to_string()) {
129            true => {
130                // The notes handler
131                fields.named.push(
132                    syn::Field::parse_named
133                        .parse2(quote! { pub notes: BTreeMap<String, Note> })
134                        .unwrap(),
135                );
136            }
137            false => {}
138        }
139
140        match attrs.contains(&PHONE.to_string()) {
141            true => {
142                // The phonenumber handler
143                fields.named.push(
144                    syn::Field::parse_named
145                        .parse2(quote! { pub phone_numbers: BTreeMap<String, PhoneNumber> })
146                        .unwrap(),
147                );
148            }
149            false => {}
150        }
151
152        // optional attributes
153        match attrs.contains(&TAGS.to_string()) {
154            true => {
155                // The tags handler
156                fields.named.push(
157                    syn::Field::parse_named
158                        .parse2(quote! { pub tags: Vec<String> })
159                        .unwrap(),
160                );
161            }
162            false => {}
163        }
164    }
165
166    return quote! {
167        #item_struct
168    }
169    .into();
170}
171
172#[derive(Debug)]
173struct Args {
174    pub vars: Vec<LitStr>,
175}
176
177impl Parse for Args {
178    fn parse(input: ParseStream) -> Result<Self> {
179        let vars = Punctuated::<syn::LitStr, Token![,]>::parse_terminated(input)?;
180        Ok(Args {
181            vars: vars.into_iter().collect::<Vec<LitStr>>(),
182        })
183    }
184}
185
186///
187/// Implementing the Traits
188///
189#[proc_macro_derive(Scaffolding)]
190pub fn scaffolding_derive(input: TokenStream) -> TokenStream {
191    // Construct a representation of Rust code as a syntax tree
192    // that we can manipulate
193    let ast: syn::DeriveInput = syn::parse(input).unwrap();
194
195    // Build the trait implementation
196    impl_scaffolding(&ast)
197}
198
199fn impl_scaffolding(ast: &syn::DeriveInput) -> TokenStream {
200    let name = &ast.ident;
201    let gen = quote! {
202        impl Scaffolding for #name {
203            fn get_activity(&self, name: String) -> Vec<ActivityItem>{
204                self.activity.iter().filter(|a| a.action == name).cloned().collect()
205            }
206
207            fn log_activity(&mut self, name: String, descr: String) {
208                self.activity.push(ActivityItem::new(name, descr));
209            }
210        }
211    };
212    gen.into()
213}
214
215// Addresses Trait
216#[proc_macro_derive(ScaffoldingAddresses)]
217pub fn scaffolding_addresses_derive(input: TokenStream) -> TokenStream {
218    let ast: syn::DeriveInput = syn::parse(input).unwrap();
219
220    impl_scaffolding_addresses(&ast)
221}
222
223fn impl_scaffolding_addresses(ast: &syn::DeriveInput) -> TokenStream {
224    let name = &ast.ident;
225    let gen = quote! {
226        impl ScaffoldingAddresses for #name {
227            fn get_address(&self, id: String) -> Option<&Address> {
228                self.addresses.get(&id)
229            }
230
231            fn insert_address(
232                &mut self,
233                category: String,
234                line_1: String,
235                line_2: String,
236                line_3: String,
237                line_4: String,
238                country_code: String,
239            ) -> String {
240                let address = Address::new(category, line_1, line_2, line_3, line_4, country_code);
241                let id = address.id.clone();
242                self.addresses.insert(id.clone(), address);
243                id
244            }
245
246            fn modify_address(&mut self, id: String, category: String, line_1: String, line_2: String, line_3: String, line_4: String, country_code: String) {
247                self.addresses
248                .entry(id)
249                .and_modify(|addr|
250                    addr.update(category, line_1, line_2, line_3, line_4, country_code)
251                );
252            }
253
254            fn search_addresses_by_category(&self, category: String) -> Vec<Address> {
255                self.addresses
256                    .iter()
257                    .filter(|(k,v)| v.category == category)
258                    .map(|(k,v)| v.clone())
259                    .collect()
260            }
261
262            fn remove_address(&mut self, id: String) {
263                self.addresses.remove(&id);
264            }
265        }
266    };
267    gen.into()
268}
269
270// EmailAddresses Trait
271#[proc_macro_derive(ScaffoldingEmailAddresses)]
272pub fn scaffolding_emailaddresses_derive(input: TokenStream) -> TokenStream {
273    let ast: syn::DeriveInput = syn::parse(input).unwrap();
274
275    impl_scaffolding_emailaddresses(&ast)
276}
277
278fn impl_scaffolding_emailaddresses(ast: &syn::DeriveInput) -> TokenStream {
279    let name = &ast.ident;
280    let gen = quote! {
281        impl ScaffoldingEmailAddresses for #name {
282            fn get_email_address(&self, id: String) -> Option<&EmailAddress> {
283                self.email_addresses.get(&id)
284            }
285
286            fn insert_email_address(
287                &mut self,
288                category: String,
289                address: String,
290            ) -> String {
291                let email = EmailAddress::new(category, address);
292                let id = email.id.clone();
293                self.email_addresses.insert(id.clone(), email);
294                id
295            }
296
297            fn search_email_addresses_by_category(&self, category: String) -> Vec<EmailAddress> {
298                self.email_addresses
299                    .iter()
300                    .filter(|(k,v)| v.category == category)
301                    .map(|(k,v)| v.clone())
302                    .collect()
303            }
304
305            fn remove_email_address(&mut self, id: String) {
306                self.email_addresses.remove(&id);
307            }
308        }
309    };
310    gen.into()
311}
312
313// Notes Trait
314#[proc_macro_derive(ScaffoldingNotes)]
315pub fn scaffolding_notes_derive(input: TokenStream) -> TokenStream {
316    let ast: syn::DeriveInput = syn::parse(input).unwrap();
317
318    impl_scaffolding_notes(&ast)
319}
320
321fn impl_scaffolding_notes(ast: &syn::DeriveInput) -> TokenStream {
322    let name = &ast.ident;
323    let gen = quote! {
324        impl ScaffoldingNotes for #name {
325            fn get_note(&self, id: String) -> Option<&Note> {
326                self.notes.get(&id)
327            }
328
329            fn insert_note(&mut self, auth: String, cont: Vec<u8>, acc: Option<String>) -> String {
330                let note = Note::new(auth, cont, acc);
331                let id = note.id.clone();
332                self.notes.insert(id.clone(), note);
333                id
334            }
335
336            fn modify_note(&mut self, id: String, auth: String, cont: Vec<u8>, acc: Option<String>) {
337                self.notes
338                    .entry(id)
339                    .and_modify(|note|
340                        note.update(auth, cont, acc)
341                    );
342            }
343
344            fn search_notes(&mut self, search: String) -> Vec<Note> {
345                let mut results: Vec<Note> = Vec::new();
346
347                for (key, note) in self.notes.iter() {
348                    let mut cont = String::from_utf8(note.content.clone())
349                    .map_err(|non_utf8| String::from_utf8_lossy(non_utf8.as_bytes()).into_owned())
350                    .unwrap();
351
352                    match cont.contains(&search) {
353                        true => {
354                            results.push(note.clone())
355                        },
356                        false => {},
357                    }
358                }
359
360                results
361            }
362
363            fn remove_note(&mut self, id: String) {
364                self.notes.remove(&id);
365            }
366        }
367    };
368    gen.into()
369}
370
371// PhoneNumber Trait
372#[proc_macro_derive(ScaffoldingPhoneNumbers)]
373pub fn scaffolding_phonenumbers_derive(input: TokenStream) -> TokenStream {
374    let ast: syn::DeriveInput = syn::parse(input).unwrap();
375
376    impl_scaffolding_phonenumbers(&ast)
377}
378
379fn impl_scaffolding_phonenumbers(ast: &syn::DeriveInput) -> TokenStream {
380    let name = &ast.ident;
381    let gen = quote! {
382        impl ScaffoldingPhoneNumbers for #name {
383            fn get_phone_number(&self, id: String) -> Option<&PhoneNumber> {
384                self.phone_numbers.get(&id)
385            }
386
387            fn insert_phone_number(
388                &mut self,
389                category: String,
390                number: String,
391                country_code: String,
392            ) -> String {
393                let phone = PhoneNumber::new(category, number, country_code);
394                let id = phone.id.clone();
395                self.phone_numbers.insert(id.clone(), phone);
396                id
397            }
398
399            fn search_phone_numbers_by_category(&self, category: String) -> Vec<PhoneNumber> {
400                self.phone_numbers
401                    .iter()
402                    .filter(|(k,v)| v.category == category)
403                    .map(|(k,v)| v.clone())
404                    .collect()
405            }
406
407            fn remove_phone_number(&mut self, id: String) {
408                self.phone_numbers.remove(&id);
409            }
410        }
411    };
412    gen.into()
413}
414
415// Tagging Trait
416#[proc_macro_derive(ScaffoldingTags)]
417pub fn scaffolding_tags_derive(input: TokenStream) -> TokenStream {
418    let ast: syn::DeriveInput = syn::parse(input).unwrap();
419
420    impl_scaffolding_tags(&ast)
421}
422
423fn impl_scaffolding_tags(ast: &syn::DeriveInput) -> TokenStream {
424    let name = &ast.ident;
425    let gen = quote! {
426        impl ScaffoldingTags for #name {
427            fn add_tag(&mut self, tag: String) {
428                // don't add duplicates
429                match self.has_tag(tag.clone()) {
430                    false => {
431                        self.tags.push(tag);
432                    },
433                    true => {
434                        println!("Ignoring tag {}. Tag already exists!", tag);
435                    },
436                }
437            }
438            fn has_tag(&self, tag: String) -> bool {
439                let results = self.tags.iter().filter(|t| **t == tag).cloned().collect::<String>();
440                match results.len() {
441                    0 => false,
442                    _ => true,
443                }
444            }
445            fn remove_tag(&mut self, tag: String) {
446                let pos = self.tags.iter().position(|t| **t == tag).unwrap();
447                self.tags.remove(pos);
448            }
449        }
450    };
451    gen.into()
452}
453
454///
455/// Modifies the following functions
456/// + new - Adds the core attributes to the new struct using the defined or default values
457///
458#[proc_macro_attribute]
459pub fn scaffolding_fn(args: TokenStream, input: TokenStream) -> TokenStream {
460    let mut item: syn::Item = syn::parse(input).unwrap();
461    let fn_item = match &mut item {
462        syn::Item::Fn(fn_item) => fn_item,
463        _ => panic!("expected fn"),
464    };
465    let attrs = parse_macro_input!(args as Args)
466        .vars
467        .iter()
468        .map(|a| a.value())
469        .collect::<Vec<_>>();
470
471    // get the name of the method
472    let name = &fn_item.sig.ident.to_string();
473
474    match name.as_ref() {
475        "new" => {
476            print!("Modifying function {} ...", name);
477            // find the line that sets the id attribute
478            for s in 0..fn_item.block.stmts.len() {
479                match &mut fn_item.block.stmts[s] {
480                    syn::Stmt::Expr(expr, None) => match expr {
481                        Struct(expr_struct) => {
482                            // println!("Found a Struct!");
483                            let mut modify_attr_list = vec![
484                                "id",
485                                "created_dtm",
486                                "modified_dtm",
487                                "inactive_dtm",
488                                "expired_dtm",
489                                "activity",
490                            ];
491
492                            match attrs.contains(&ADDRESS.to_string()) {
493                                true => {
494                                    modify_attr_list.push(&ADDRESS);
495                                }
496                                _ => {}
497                            }
498
499                            match attrs.contains(&EMAIL.to_string()) {
500                                true => {
501                                    modify_attr_list.push(&EMAIL);
502                                }
503                                _ => {}
504                            }
505
506                            match attrs.contains(&METADATA.to_string()) {
507                                true => {
508                                    modify_attr_list.push(&METADATA);
509                                }
510                                _ => {}
511                            }
512
513                            match attrs.contains(&NOTES.to_string()) {
514                                true => {
515                                    modify_attr_list.push(&NOTES);
516                                }
517                                _ => {}
518                            }
519
520                            match attrs.contains(&PHONE.to_string()) {
521                                true => {
522                                    modify_attr_list.push(&PHONE);
523                                }
524                                _ => {}
525                            }
526
527                            match attrs.contains(&TAGS.to_string()) {
528                                true => {
529                                    modify_attr_list.push(&TAGS);
530                                }
531                                _ => {}
532                            }
533
534                            // first determine if the attributes already exist
535                            for f in 0..expr_struct.fields.len() {
536                                match &expr_struct.fields[f].member {
537                                    Member::Named(mbr) => {
538                                        match CORE_ATTRS.contains(&mbr.to_string().as_str()) {
539                                            true => {
540                                                // core attribute already set, so don't need to add it
541                                                // println!("Ignoring attribute {}", mbr.to_string());
542                                                modify_attr_list
543                                                    .retain_mut(|a| *a != mbr.to_string().as_str());
544                                            }
545                                            false => {}
546                                        }
547                                    }
548                                    _ => {}
549                                }
550                            }
551
552                            // then, add the missing attributes
553                            for attr in modify_attr_list.iter() {
554                                // println!("Adding attribute {}", attr);
555                                match *attr {
556                                    "id" => {
557                                        let line: FieldValue = parse_quote! {id: defaults::id()};
558                                        expr_struct.fields.insert(0, line);
559                                    }
560                                    "created_dtm" => {
561                                        let line: FieldValue =
562                                            parse_quote! {created_dtm: defaults::now()};
563                                        expr_struct.fields.insert(0, line);
564                                    }
565                                    "modified_dtm" => {
566                                        let line: FieldValue =
567                                            parse_quote! {modified_dtm: defaults::now()};
568                                        expr_struct.fields.insert(0, line);
569                                    }
570                                    "inactive_dtm" => {
571                                        let line: FieldValue = parse_quote! {inactive_dtm: defaults::add_days(defaults::now(), 90)};
572                                        expr_struct.fields.insert(0, line);
573                                    }
574                                    "expired_dtm" => {
575                                        let line: FieldValue = parse_quote! {expired_dtm: defaults::add_years(defaults::now(), 3)};
576                                        expr_struct.fields.insert(0, line);
577                                    }
578                                    "activity" => {
579                                        let line: FieldValue = parse_quote! {activity: Vec::new()};
580                                        expr_struct.fields.insert(0, line);
581                                    }
582                                    "metadata" => {
583                                        let line: FieldValue =
584                                            parse_quote! {metadata: BTreeMap::new()};
585                                        expr_struct.fields.insert(0, line);
586                                    }
587                                    "notes" => {
588                                        let line: FieldValue =
589                                            parse_quote! {notes: BTreeMap::new()};
590                                        expr_struct.fields.insert(0, line);
591                                    }
592                                    "tags" => {
593                                        let line: FieldValue = parse_quote! {tags: Vec::new()};
594                                        expr_struct.fields.insert(0, line);
595                                    }
596                                    "addresses" => {
597                                        let line: FieldValue =
598                                            parse_quote! {addresses: BTreeMap::new()};
599                                        expr_struct.fields.insert(0, line);
600                                    }
601                                    "email_addresses" => {
602                                        let line: FieldValue =
603                                            parse_quote! {email_addresses: BTreeMap::new()};
604                                        expr_struct.fields.insert(0, line);
605                                    }
606                                    "phone_numbers" => {
607                                        let line: FieldValue =
608                                            parse_quote! {phone_numbers: BTreeMap::new()};
609                                        expr_struct.fields.insert(0, line);
610                                    }
611                                    _ => {}
612                                }
613                            }
614                        }
615                        _ => {
616                            // println!("Not an Struct!");
617                        }
618                    },
619                    _ => {
620                        // println!("Not an Expr!");
621                    }
622                }
623            }
624        }
625        _ => {
626            print!(
627                "Function {} is unsupported. Nothing to add to function ",
628                name
629            );
630        }
631    }
632
633    item.into_token_stream().into()
634}