Skip to main content

preserves_schema_macros/
lib.rs

1use preserves::value::Map;
2use preserves_schema::compiler::*;
3use preserves_schema::compiler::types::Purpose;
4use preserves_schema::gen::schema::Schema;
5use proc_macro2::Span;
6use quote::ToTokens;
7use quote::quote;
8use std::fmt::Display;
9use syn::LitStr;
10use syn::Token;
11use syn::parenthesized;
12use syn::parse::Parser;
13use syn::punctuated::Punctuated;
14
15mod kw {
16    use syn::custom_keyword;
17    custom_keyword!(load);
18    custom_keyword!(cross_reference);
19    custom_keyword!(external_module);
20}
21
22#[derive(Debug)]
23enum Instruction {
24    Namespace(String),
25    Load(LitStr),
26    CrossReference {
27        namespace: String,
28        bundle_path: LitStr,
29    },
30    ExternalModule {
31        module_path: ModulePath,
32        namespace: String,
33    }
34}
35
36fn syn_path_string(p: syn::Path) -> String {
37    p.to_token_stream().to_string().replace(&[' ', '\t', '\n', '\r'], "")
38}
39
40fn syn_litstr_resolve(s: &syn::LitStr) -> String {
41    let s: String = s.value();
42    match s.chars().nth(0) {
43        Some('/') => s.into(),
44        Some('<') => match &s[1..].split_once('>') {
45            Some((envvar, remainder)) => match std::env::var(envvar) {
46                Ok(p) => p + "/" + remainder,
47                Err(_) => panic!("No such environment variable: {:?}", s),
48            }
49            None => panic!("Invalid relative path syntax: {:?}", s),
50        },
51        _ => panic!("Invalid path syntax: {:?}", s)
52    }
53}
54
55impl syn::parse::Parse for Instruction {
56    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
57        let lookahead = input.lookahead1();
58        if lookahead.peek(kw::load) {
59            let _: kw::load = input.parse()?;
60            let content;
61            let _ = parenthesized!(content in input);
62            let bundle_path: syn::LitStr = content.parse()?;
63            Ok(Instruction::Load(bundle_path))
64        } else if lookahead.peek(kw::cross_reference) {
65            let _: kw::cross_reference = input.parse()?;
66            let content;
67            let _ = parenthesized!(content in input);
68            let namespace: syn::Path = content.parse()?;
69            let _: Token![=] = content.parse()?;
70            let bundle_path: syn::LitStr = content.parse()?;
71            Ok(Instruction::CrossReference {
72                namespace: syn_path_string(namespace),
73                bundle_path,
74            })
75        } else if lookahead.peek(kw::external_module) {
76            let _: kw::external_module = input.parse()?;
77            let content;
78            let _ = parenthesized!(content in input);
79            let module_path = Punctuated::<syn::Ident, syn::Token![.]>::parse_separated_nonempty(&content)?;
80            let _: Token![=] = content.parse()?;
81            let namespace: syn::Path = content.parse()?;
82            Ok(Instruction::ExternalModule {
83                module_path: module_path.into_iter().map(|p| p.to_string()).collect(),
84                namespace: syn_path_string(namespace),
85            })
86        } else {
87            let ns: syn::Path = input.parse()?;
88            Ok(Instruction::Namespace(syn_path_string(ns)))
89        }
90    }
91}
92
93struct ModuleTree {
94    own_body: String,
95    children: Map<String, ModuleTree>,
96}
97
98impl Default for ModuleTree {
99    fn default() -> Self {
100        ModuleTree {
101            own_body: String::new(),
102            children: Map::default(),
103        }
104    }
105}
106
107impl ModuleTree {
108    fn build(outputs: Map<Option<ModulePath>, String>) -> Self {
109        let mut mt = ModuleTree::default();
110        for (p, c) in outputs.into_iter() {
111            match p {
112                None => mt.own_body = c,
113                Some(k) => {
114                    let mut r = &mut mt;
115                    for e in k { r = mt.children.entry(names::render_modname(&e)).or_default(); }
116                    r.own_body = c;
117                }
118            }
119        }
120        mt
121    }
122}
123
124impl Display for ModuleTree {
125    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126        write!(f, "{}", self.own_body)?;
127        for (label, mt) in self.children.iter() {
128            write!(f, "\npub mod {} {{ ", label)?;
129            mt.fmt(f)?;
130            write!(f, "}}")?;
131        }
132        Ok(())
133    }
134}
135
136#[proc_macro]
137pub fn compile_preserves_schemas(src: proc_macro::TokenStream) -> proc_macro::TokenStream {
138    let instructions = Punctuated::<Instruction, syn::Token![,]>::parse_terminated
139        .parse(src)
140        .expect("valid sequence of compile_preserves_schemas instructions");
141
142    let mut namespace = None::<String>;
143    let mut bundles_to_load = Vec::<LitStr>::new();
144    let mut bundles_to_xref = Vec::<LitStr>::new();
145    let mut external_modules = Vec::<ExternalModule>::new();
146
147    for i in instructions.into_iter() {
148        match i {
149            Instruction::Namespace(n) => {
150                if namespace.is_some() {
151                    panic!("Only one namespace is permitted")
152                }
153                namespace = Some(n)
154            }
155            Instruction::Load(p) => bundles_to_load.push(p),
156            Instruction::ExternalModule { module_path, namespace } => {
157                external_modules.push(ExternalModule::new(module_path, &namespace));
158            }
159            Instruction::CrossReference { namespace, bundle_path } => {
160                let mut bundle = Map::<ModulePath, Schema>::new();
161                let is_schema = load_schema_or_bundle(&mut bundle, &syn_litstr_resolve(&bundle_path).into())
162                    .expect("Invalid schema/bundle binary");
163                bundles_to_xref.push(bundle_path);
164                for (k, _v) in bundle.into_iter() {
165                    external_modules.push(if is_schema {
166                        ExternalModule::new(k, &namespace)
167                    } else {
168                        let ns = namespace.clone();
169                        let mut pieces = vec![ns.clone()];
170                        pieces.extend(
171                            k.iter().map(|p| names::render_modname(&p)).collect::<Vec<_>>());
172                        ExternalModule::new(k, &pieces.join("::"))
173                            .set_fallback_language_types(
174                                move |v| vec![format!("{}::Language<{}>", ns, v)].into_iter().collect())
175                    });
176                }
177            }
178        }
179    }
180
181    let namespace = namespace.expect("Missing namespace");
182
183    let mut dependency_paths = Vec::<syn::LitStr>::new();
184
185    let mut c = CompilerConfig::new(namespace.clone());
186    for b in bundles_to_load.into_iter() {
187        dependency_paths.push(syn::LitStr::new(&syn_litstr_resolve(&b), Span::call_site()));
188        load_schema_or_bundle_with_purpose(&mut c.bundle, &syn_litstr_resolve(&b).into(), Purpose::Codegen)
189            .expect(&b.value());
190    }
191    for b in bundles_to_xref.into_iter() {
192        dependency_paths.push(syn::LitStr::new(&syn_litstr_resolve(&b), Span::call_site()));
193        load_schema_or_bundle_with_purpose(&mut c.bundle, &syn_litstr_resolve(&b).into(), Purpose::Xref)
194            .expect(&b.value());
195    }
196    for m in external_modules.into_iter() {
197        c.add_external_module(m);
198    }
199
200    let mut outputs = Map::<Option<ModulePath>, String>::new();
201    let mut collector = CodeCollector {
202        emit_mod_declarations: false,
203        collect_module: CodeModuleCollector::Custom {
204            collect_output: &mut |p, c| {
205                outputs.insert(p.cloned(), c.to_owned());
206                Ok(())
207            },
208        },
209    };
210    compile(&c, &mut collector).expect("Compilation failed");
211
212    let top_module_source = format!(
213        "pub mod {} {{ {} }}",
214        names::render_modname(namespace.split("::").last().unwrap()),
215        ModuleTree::build(outputs));
216    let top_module: syn::Item = syn::parse_str(&top_module_source)
217        .expect("Invalid generated code");
218
219    quote!{
220        // TODO: this is ugly, but makes the code depend on the actual schema bundle:
221        // See https://doc.rust-lang.org/nightly/proc_macro/tracked_path/fn.path.html
222        // and https://github.com/rust-lang/rust/issues/99515
223        #( const _: &'static [u8] = include_bytes!(#dependency_paths); )*
224
225        #top_module
226    }.into()
227}