miku_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::parse::{Parse, ParseStream};
4use syn::punctuated::Punctuated;
5use syn::{
6    bracketed, parse_macro_input,
7    token::{self, Comma},
8    Attribute, FnArg, Ident, LitStr, Pat, Path, Result, ReturnType, Signature, Token,
9};
10
11mod kw {
12    syn::custom_keyword!(docs);
13}
14
15const OC2_DOC_BASE: &str =
16    "https://github.com/fnuecke/oc2/blob/1.18-forge/src/main/resources/assets/oc2/doc/en_us/";
17
18struct FnDef(Signature, Vec<Attribute>);
19impl Parse for FnDef {
20    fn parse(input: ParseStream) -> Result<Self> {
21        let attrs = input.call(Attribute::parse_outer)?;
22        let signature = input.parse::<Signature>()?;
23        input.parse::<Token![;]>()?;
24        Ok(FnDef(signature, attrs))
25    }
26}
27
28struct OC2RpcDef {
29    oc_method_name: LitStr,
30    doc_path: Option<LitStr>,
31}
32
33impl Parse for OC2RpcDef {
34    fn parse(input: ParseStream) -> Result<Self> {
35        Ok(OC2RpcDef {
36            oc_method_name: input.parse::<LitStr>()?,
37            doc_path: if input.peek(Token![,]) {
38                input.parse::<Token![,]>()?;
39                input.parse::<kw::docs>()?;
40                input.parse::<Token![=]>()?;
41                Some(input.parse::<LitStr>()?)
42            } else {
43                None
44            },
45        })
46    }
47}
48
49#[proc_macro_attribute]
50pub fn rpc(attr: TokenStream, input: TokenStream) -> TokenStream {
51    let OC2RpcDef {
52        oc_method_name,
53        doc_path,
54    } = parse_macro_input!(attr as OC2RpcDef);
55    let FnDef(
56        Signature {
57            ident,
58            generics,
59            inputs,
60            output,
61            ..
62        },
63        attrs,
64    ) = parse_macro_input!(input as FnDef);
65    let where_clause = &generics.where_clause;
66
67    let ret_type = match output {
68        ReturnType::Default => quote! { Option<()> },
69        ReturnType::Type(_, t) => quote! { #t },
70    };
71
72    let doc_path = doc_path.map(|path| {
73        let doc_url = format!("[OC2 Docs]({}/{}):", OC2_DOC_BASE, path.value());
74        quote! {
75            #[doc = #doc_url]
76        }
77    });
78
79    let arg_idents: Vec<Box<Pat>> = inputs
80        .iter()
81        .filter_map(|v| {
82            if let FnArg::Typed(t) = v {
83                Some(t.pat.clone())
84            } else {
85                None
86            }
87        })
88        .collect();
89
90    let tokens = if arg_idents.is_empty() {
91        let mut call_bytes = (String::from("\0")
92            + &format!(
93                r#"{{"type":"invoke","data":{{"name":"{}","parameters": [],"deviceId":""#,
94                oc_method_name.value()
95            ))
96            .into_bytes();
97        let call_start_len = call_bytes.len();
98        let call_id_end_len = call_start_len + 36;
99        call_bytes.extend(std::iter::repeat(0).take(36));
100        call_bytes.extend(b"\"}}\0");
101        let call_full_len = call_bytes.len();
102
103        let call_iter = call_bytes.into_iter();
104
105        quote! {
106            #doc_path
107            #(#attrs)*
108            fn #ident #generics (&self, bus: &mut crate::DeviceBus) -> std::io::Result<#ret_type> #where_clause {
109                let mut call_bytes: [u8; #call_full_len ] = [ #(#call_iter),* ];
110                call_bytes[ #call_start_len .. #call_id_end_len].copy_from_slice(self.id().as_bytes());
111
112                let response: crate::Response<#ret_type> = bus.call_preserialized(&call_bytes)?;
113                Ok(response.data)
114            }
115        }
116    } else {
117        let arg_defs = inputs.into_iter();
118
119        quote! {
120            #doc_path
121            #(#attrs)*
122            fn #ident #generics (&self, bus: &mut crate::DeviceBus, #(#arg_defs),*) -> std::io::Result<#ret_type> #where_clause {
123                let response: crate::Response<#ret_type> = bus.call(&crate::Call::invoke(self.id(), #oc_method_name, &[#(&#arg_idents),*]))?;
124                Ok(response.data)
125            }
126        }
127    };
128
129    TokenStream::from(tokens)
130}
131
132struct DeviceData {
133    rust_name: Ident,
134    oc2_identity: LitStr,
135    docs: LitStr,
136    _bracket_token: token::Bracket,
137    capabilities: Punctuated<Path, Comma>,
138}
139
140impl Parse for DeviceData {
141    fn parse(input: ParseStream) -> Result<Self> {
142        let rust_name = input.parse::<Ident>()?;
143        input.parse::<Comma>()?;
144        let oc2_identity = input.parse::<LitStr>()?;
145        input.parse::<Comma>()?;
146        let docs = input.parse::<LitStr>()?;
147        input.parse::<Comma>()?;
148
149        let content;
150        Ok(DeviceData {
151            rust_name,
152            oc2_identity,
153            docs,
154            _bracket_token: bracketed!(content in input),
155            capabilities: content.parse_terminated(Path::parse)?,
156        })
157    }
158}
159
160#[proc_macro]
161pub fn define_device(tokens: TokenStream) -> TokenStream {
162    let DeviceData {
163        rust_name,
164        docs,
165        oc2_identity,
166        capabilities,
167        ..
168    } = parse_macro_input!(tokens as DeviceData);
169
170    let capabilities = capabilities.into_iter();
171
172    let tokens = quote! {
173        #[doc = #docs ]
174        pub struct #rust_name(pub String);
175
176        impl RPCDevice for #rust_name {
177            fn id(&self) -> &str {
178                &self.0
179            }
180
181            fn from_id(id: String) -> Self {
182                #rust_name(id)
183            }
184        }
185
186        impl IdentifiedDevice for #rust_name {
187            const IDENTITY: &'static str = #oc2_identity;
188        }
189
190        #(impl #capabilities for #rust_name {})*
191    };
192
193    TokenStream::from(tokens)
194}