automapper_proc/
lib.rs

1#![allow(dead_code)]
2#![allow(unused_imports)]
3
4use std::{collections::HashSet, ops::Deref, path::PathBuf, sync::Arc};
5
6use anyhow::Context;
7use models::context::MacroCtx;
8use proc_macro::{Span, TokenStream};
9use quote::{format_ident, quote, ToTokens};
10use rodc_util::StructRustType;
11use serde_json::Value;
12use struct_to_struct_mapping::TypeToTypeMapping;
13use syn::{
14    braced, parenthesized, parse::Parse, parse_macro_input, punctuated::Punctuated, token,
15    DeriveInput, Meta, Token,
16};
17use walkdir::WalkDir;
18
19mod models;
20mod rodc_util;
21mod struct_to_struct_mapping;
22
23#[derive(Debug)]
24struct TraitImpl {
25    struct_token: Token![fn],
26    iden: syn::Ident,
27    paren_token: token::Paren,
28    mapping: Request,
29    semi_token: Token![;],
30}
31
32#[derive(Debug, Clone)]
33struct Request {
34    source_type: syn::Path,
35    _coma: syn::Token![->],
36    dest_type: syn::Path,
37}
38
39/// See crate level doc for automapper for more information.
40#[proc_macro]
41pub fn impl_map_fn(input: TokenStream) -> TokenStream {
42    let def = parse_macro_input!(input as TraitImpl);
43    def.into_token_stream().into()
44}
45
46impl Parse for TraitImpl {
47    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
48        let content;
49        let this = Self {
50            struct_token: input.parse()?,
51            iden: input.parse()?,
52            paren_token: parenthesized!(content in input),
53            mapping: content.parse()?,
54            semi_token: input.parse()?,
55        };
56
57        Ok(this)
58    }
59}
60
61impl Parse for Request {
62    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
63        Ok(Self {
64            source_type: input.parse()?,
65            _coma: input.parse()?,
66            dest_type: input.parse()?,
67        })
68    }
69}
70
71impl ToTokens for TraitImpl {
72    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
73        let cargo_toml_path = caller_crate_cargo_toml();
74        let rustdoc_path = cargo_toml_path.parent().unwrap().join("rustdoc.json");
75
76        if !rustdoc_path.exists() {
77            eprintln!(
78                "rustdoc.json does not exist at {:?}, run the cli generate this.",
79                rustdoc_path
80            );
81            tokens.extend(quote! {
82                panic!("rustdoc.json does not exist at {:?}, run the cli generate this.", rustdoc_path);
83            });
84            return;
85        };
86
87        let rdocs = {
88            let content =
89                std::fs::read_to_string(rustdoc_path).expect("read rustdoc.json file from disk");
90            serde_json::from_str::<rustdoc_types::Crate>(&content)
91                .expect("parse rustdoc.json as json")
92        };
93
94        let ctx = MacroCtx::new(rdocs);
95
96        let mapping = TypeToTypeMapping::new(
97            self.mapping.source_type.clone(),
98            vec!["value".to_string()], // the name of the input variable in the mapping function
99            self.mapping.dest_type.clone(),
100            ctx,
101        )
102        .expect("create struct to struct mapping");
103
104        let fn_name = self.iden.clone();
105        let value_ty = mapping.source.path();
106        let dest_ty = mapping.dest.path();
107
108        let t = quote! {
109            fn #fn_name(value: #value_ty) -> #dest_ty {
110                #mapping
111            }
112        };
113
114        #[cfg(debug_assertions)]
115        //std::fs::write("crates/usage/src/output.rs", t.to_string()).expect("write to output.rs");
116        tokens.extend(t);
117    }
118}
119
120/// Returns the root path of the crate that calls this function.
121/// This is a cursed method
122fn caller_crate_cargo_toml() -> PathBuf {
123    let crate_name =
124        std::env::var("CARGO_PKG_NAME").expect("failed to read ENV var `CARGO_PKG_NAME`!");
125    let current_dir = std::env::current_dir().expect("failed to unwrap env::current_dir()!");
126    let search_entry = format!("name=\"{crate_name}\"");
127    for entry in WalkDir::new(&current_dir)
128        .into_iter()
129        .filter_entry(|e| !e.file_name().eq_ignore_ascii_case("target"))
130    {
131        let Ok(entry) = entry else { continue };
132        if !entry.file_type().is_file() {
133            continue;
134        }
135        let Some(file_name) = entry.path().file_name() else {
136            continue;
137        };
138        if !file_name.eq_ignore_ascii_case("Cargo.toml") {
139            continue;
140        }
141        let Ok(cargo_toml) = std::fs::read_to_string(entry.path()) else {
142            continue;
143        };
144        if cargo_toml
145            .chars()
146            .filter(|&c| !c.is_whitespace())
147            .collect::<String>()
148            .contains(search_entry.as_str())
149        {
150            return entry.path().to_path_buf();
151        }
152    }
153    current_dir
154}