fj_proc/
lib.rs

1mod expand;
2mod parse;
3
4use proc_macro::TokenStream;
5use syn::{parse_macro_input, FnArg, ItemFn};
6
7/// Define a function-based model.
8///
9/// The simplest model function takes no parameters and returns a hard-coded
10/// `fj::Shape`.
11///
12/// ``` rust ignore
13/// # use fj_proc::model;
14/// use fj::{Circle, Sketch, Shape};
15/// #[model]
16/// fn model() -> Shape {
17///     let circle = Circle::from_radius(10.0);
18///     Sketch::from_circle(circle).into()
19/// }
20/// ```
21///
22/// For convenience, you can also return anything that could be converted into
23/// a `fj::Shape` (e.g. a `fj::Sketch`).
24///
25/// ``` rust ignore
26/// # use fj_proc::model;
27/// use fj::{Circle, Sketch};
28/// #[model]
29/// fn model() -> Sketch {
30///     let circle = Circle::from_radius(10.0);
31///     Sketch::from_circle(circle)
32/// }
33/// ```
34///
35/// The return type is checked at compile time. That means something like this
36/// won't work because `()` can't be converted into a `fj::Shape`.
37///
38/// ``` rust ignore
39/// # use fj_proc::model;
40/// #[model]
41/// fn model() { todo!() }
42/// ```
43///
44/// The model function's arguments can be anything that implement
45/// [`std::str::FromStr`].
46///
47/// ``` rust ignore
48/// # use fj_proc::model;
49/// #[model]
50/// fn cylinder(height: f64, label: String, is_horizontal: bool) -> fj::Shape { todo!() }
51/// ```
52///
53/// Constraints and default values can be added to an argument using the
54/// `#[param]` attribute.
55///
56/// ``` rust ignore
57/// use fj::syntax::*;
58///
59/// #[fj::model]
60/// pub fn spacer(
61///     #[param(default = 1.0, min = inner * 1.01)] outer: f64,
62///     #[param(default = 0.5, max = outer * 0.99)] inner: f64,
63///     #[param(default = 1.0)] height: f64,
64/// ) -> fj::Shape {
65///     let outer_edge = fj::Sketch::from_circle(fj::Circle::from_radius(outer));
66///     let inner_edge = fj::Sketch::from_circle(fj::Circle::from_radius(inner));
67///
68///     let footprint = outer_edge.difference(&inner_edge);
69///     let spacer = footprint.sweep([0., 0., height]);
70///
71///     spacer.into()
72/// }
73/// ```
74///
75/// For more complex situations, model functions are allowed to return any
76/// error type that converts into a model error.
77///
78/// ``` rust ignore
79/// #[fj::model]
80/// pub fn model() -> Result<fj::Shape, std::env::VarError> {
81///     let home_dir = std::env::var("HOME")?;
82///
83///     todo!("Do something with {home_dir}")
84/// }
85///
86/// fn assert_convertible(e: std::env::VarError) -> fj::models::Error { e.into() }
87/// ```
88#[proc_macro_attribute]
89pub fn model(_: TokenStream, input: TokenStream) -> TokenStream {
90    let item = parse_macro_input!(input as syn::ItemFn);
91
92    match parse::parse(&item) {
93        Ok(init) => {
94            let item = without_param_attrs(item);
95
96            let attrs = item.attrs;
97            let vis = item.vis;
98            let sig = item.sig;
99            let statements = item.block.stmts;
100
101            let item = quote::quote! {
102                #(#attrs)* #vis #sig {
103                    fj::abi::initialize_panic_handling();
104                    #(#statements)*
105                }
106            };
107
108            let tokens = quote::quote! {
109                #item
110                #init
111
112            };
113
114            tokens.into()
115        }
116        Err(e) => e.into_compile_error().into(),
117    }
118}
119
120/// Strip out any of our `#[param(...)]` attributes so the item will compile.
121fn without_param_attrs(mut item: ItemFn) -> ItemFn {
122    for input in &mut item.sig.inputs {
123        let attrs = match input {
124            FnArg::Receiver(r) => &mut r.attrs,
125            FnArg::Typed(t) => &mut t.attrs,
126        };
127        attrs.retain(|attr| !attr.path().is_ident("param"));
128    }
129
130    item
131}