zng_env_proc_macros/
lib.rs

1#![doc(html_favicon_url = "https://zng-ui.github.io/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://zng-ui.github.io/res/zng-logo.png")]
3//!
4//! Proc-macros for `zng-env`.
5//!
6//! # Crate
7//!
8#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
9#![warn(unused_extern_crates)]
10#![warn(missing_docs)]
11
12use std::path::PathBuf;
13
14use proc_macro::TokenStream;
15use semver::Version;
16
17#[macro_use]
18extern crate quote;
19
20#[doc(hidden)]
21#[proc_macro]
22pub fn init_parse(crate_: TokenStream) -> TokenStream {
23    let crate_ = proc_macro2::TokenStream::from(crate_);
24
25    let manifest = match std::env::var("CARGO_MANIFEST_DIR") {
26        Ok(m) => PathBuf::from(m).join("Cargo.toml"),
27        Err(e) => {
28            let msg = format!("missing CARGO_MANIFEST_DIR, {e}");
29            return quote! {
30                compile_error!(#msg)
31            }
32            .into();
33        }
34    };
35    let manifest_str = match std::fs::read_to_string(&manifest) {
36        Ok(s) => s,
37        Err(e) => {
38            let msg = format!("cannot read `{}`, {e}", manifest.display());
39            return quote! {
40                compile_error!(#msg)
41            }
42            .into();
43        }
44    };
45
46    let m: Manifest = match toml::from_str(&manifest_str) {
47        Ok(m) => m,
48        Err(e) => {
49            let msg = format!("cannot parse Cargo.toml manifest, {e}");
50            return quote! {
51                compile_error!(#msg)
52            }
53            .into();
54        }
55    };
56
57    let pkg_name = m.package.name;
58    let pkg_authors = m.package.authors.unwrap_or_default();
59    let (major, minor, patch, pre, build) = {
60        let p = m.package.version;
61        (p.major, p.minor, p.patch, p.pre.to_string(), p.build.to_string())
62    };
63    let description = m.package.description.unwrap_or_default();
64    let homepage = m.package.homepage.unwrap_or_default();
65    let license = m.package.license.unwrap_or_default();
66    let mut app = "";
67    let mut org = "";
68    let mut app_id = String::new();
69    let mut has_about = false;
70    let mut meta_keys = vec![];
71    let mut meta_values = vec![];
72    if let Some(zng) = m.package.metadata.as_ref().and_then(|m| m.zng.as_ref())
73        && !zng.about.is_empty()
74    {
75        let s = |key: &str| match zng.about.get(key) {
76            Some(toml::Value::String(s)) => s.as_str(),
77            _ => "",
78        };
79        has_about = true;
80        app = s("app");
81        org = s("org");
82        app_id = clean_id(s("app_id"));
83        for (k, v) in &zng.about {
84            if let toml::Value::String(v) = v
85                && !["app", "org", "app_id"].contains(&k.as_str())
86            {
87                meta_keys.push(k);
88                meta_values.push(v);
89            }
90        }
91    }
92    if app.is_empty() {
93        app = &pkg_name;
94    }
95    if org.is_empty() {
96        org = pkg_authors.first().map(|s| s.as_str()).unwrap_or_default();
97    }
98    if app_id.is_empty() {
99        let qualifier = meta_keys
100            .iter()
101            .position(|k| k.as_str() == "qualifier")
102            .map(|i| meta_values[i].as_str())
103            .unwrap_or_default();
104        app_id = clean_id(&format!("{}.{}.{}", qualifier, org, app));
105    }
106
107    /*
108    pub fn macro_new(
109        pkg_name: &'static str,
110        pkg_authors: &[&'static str],
111        (major, minor, patch, pre, build): (u64, u64, u64, &'static str, &'static str),
112        app_id: &'static str,
113        app: &'static str,
114        org: &'static str,
115        description: &'static str,
116        homepage: &'static str,
117        license: &'static str,
118        has_about: bool,
119        meta: &[(&'static str, &'static str)],
120    )
121     */
122    quote! {
123        #crate_::init(#crate_::About::macro_new(
124            #pkg_name,
125            &[#(#pkg_authors),*],
126            (#major, #minor, #patch, #pre, #build),
127            #app_id,
128            #app,
129            #org,
130            #description,
131            #homepage,
132            #license,
133            #has_about,
134            &[#( (#meta_keys, #meta_values) ),*],
135            cfg!(test),
136        ))
137    }
138    .into()
139}
140
141/// * At least one identifier, dot-separated.
142/// * Each identifier must contain ASCII letters, ASCII digits and underscore only.
143/// * Each identifier must start with a letter.
144/// * All lowercase.
145fn clean_id(raw: &str) -> String {
146    let mut r = String::new();
147    let mut sep = "";
148    for i in raw.split('.') {
149        let i = i.trim();
150        if i.is_empty() {
151            continue;
152        }
153        r.push_str(sep);
154        for (i, c) in i.trim().char_indices() {
155            if i == 0 {
156                if !c.is_ascii_alphabetic() {
157                    r.push('i');
158                } else {
159                    r.push(c.to_ascii_lowercase());
160                }
161            } else if c.is_ascii_alphanumeric() || c == '_' {
162                r.push(c.to_ascii_lowercase());
163            } else {
164                r.push('_');
165            }
166        }
167        sep = ".";
168    }
169    r
170}
171
172#[derive(serde::Deserialize)]
173struct Manifest {
174    package: Package,
175}
176#[derive(serde::Deserialize)]
177struct Package {
178    name: String,
179    version: Version,
180    description: Option<String>,
181    homepage: Option<String>,
182    license: Option<String>,
183    authors: Option<Box<[String]>>,
184    metadata: Option<Metadata>,
185}
186#[derive(serde::Deserialize)]
187struct Metadata {
188    zng: Option<Zng>,
189}
190#[derive(serde::Deserialize)]
191struct Zng {
192    about: toml::Table,
193}
194
195#[doc(hidden)]
196#[proc_macro]
197// #[cfg(target_arch = "wasm32")] // cannot do this, target_arch is the build system arch
198pub fn wasm_process_start(crate_closure: TokenStream) -> TokenStream {
199    use quote::TokenStreamExt as _;
200
201    let crate_closure = proc_macro2::TokenStream::from(crate_closure);
202    let mut crate_ = proc_macro2::TokenStream::new();
203    let mut crate_ok = false;
204    let mut closure = proc_macro2::TokenStream::new();
205
206    for tt in crate_closure {
207        if crate_ok {
208            closure.append(tt);
209        } else if matches!(&tt, proc_macro2::TokenTree::Punct(p) if p.as_char() == ',') {
210            crate_ok = true;
211        } else {
212            crate_.append(tt);
213        }
214    }
215
216    use sha2::Digest;
217    let mut start_ident = sha2::Sha256::new();
218    start_ident.update(closure.to_string().as_bytes());
219    let start_ident = format!("__zng_env_start_{:x}", start_ident.finalize());
220    let start_ident = proc_macro2::Ident::new(&start_ident, proc_macro2::Span::call_site());
221
222    quote! {
223        #[doc(hidden)]
224        #[#crate_::wasm_bindgen]
225        pub fn #start_ident() {
226            #crate_::WASM_INIT.with_borrow_mut(|v| {
227                v.push(_on_process_start);
228            })
229        }
230        fn _on_process_start(args: &#crate_::ProcessStartArgs) {
231            fn on_process_start(args: &#crate_::ProcessStartArgs, handler: impl FnOnce(&#crate_::ProcessStartArgs)) {
232                handler(args)
233            }
234            on_process_start(args, #closure)
235        }
236    }
237    .into()
238}