libobs_source_macro/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::Span;
3use quote::quote;
4use syn::{
5    parse_macro_input, punctuated::Punctuated, Attribute, Data, DeriveInput, Expr, Fields, LitStr,
6    MetaNameValue, Token,
7};
8
9#[allow(unused_assignments)]
10#[proc_macro_attribute]
11/// This macro is used to generate a builder pattern for an obs source. <br>
12/// The attribute should be the id of the source.<br>
13/// The struct should have named fields, each field should have an attribute `#[obs_property(type_t="your_type")]`. <br>
14/// `type_t` can be `enum`, `enum_string`, `string`, `bool` or `int`. <br>
15/// - `enum`: the field should be an enum with `num_derive::{FromPrimitive, ToPrimitive}`.
16/// - `enum_string`: the field should be an enum which implements `StringEnum`.
17/// - `string`: the field should be a string.
18/// - `bool`: the field should be a bool.
19/// - `type_t`: `int`, the field should be an i64.
20/// The attribute can also have a `settings_key` which is the key used in the settings, if this attribute is not given, the macro defaults to the field name. <br>
21/// Documentation is inherited from the field to the setter function.<br>
22/// Example: <br>
23/// ```
24/// use libobs_wrapper::data::StringEnum;
25/// use libobs_source_macro::obs_object_builder;
26/// use num_derive::{FromPrimitive, ToPrimitive};
27///
28/// #[repr(i32)]
29/// #[derive(Clone, Copy, Debug, PartialEq, Eq, FromPrimitive, ToPrimitive)]
30/// pub enum ObsWindowCaptureMethod {
31///     MethodAuto = libobs::window_capture_method_METHOD_AUTO,
32/// 	MethodBitBlt = libobs::window_capture_method_METHOD_BITBLT,
33/// 	MethodWgc = libobs::window_capture_method_METHOD_WGC,
34/// }
35///
36/// #[derive(Clone, Copy, Debug, PartialEq, Eq)]
37/// pub enum ObsGameCaptureRgbaSpace {
38///     SRgb,
39///     RGBA2100pq
40/// }
41///
42/// impl StringEnum for ObsGameCaptureRgbaSpace {
43///     fn to_str(&self) -> &str {
44///         match self {
45///             ObsGameCaptureRgbaSpace::SRgb => "sRGB",
46///             ObsGameCaptureRgbaSpace::RGBA2100pq => "Rec. 2100 (PQ)"
47///         }
48///     }
49/// }
50///
51/// /// Provides a easy to use builder for the window capture source.
52/// #[derive(Debug)]
53/// #[obs_object_builder("window_capture")]
54/// pub struct WindowCaptureSourceBuilder {
55///     #[obs_property(type_t="enum")]
56///     /// Sets the capture method for the window capture
57///     capture_method: ObsWindowCaptureMethod,
58///
59///     /// Sets the window to capture.
60///     #[obs_property(type_t = "string", settings_key = "window")]
61///     window_raw: String,
62///
63///     #[obs_property(type_t = "bool")]
64///     /// Sets whether the cursor should be captured
65///     cursor: bool,
66///
67///     /// Sets the capture mode for the game capture source. Look at doc for `ObsGameCaptureMode`
68///     #[obs_property(type_t = "enum_string")]
69///     capture_mode: ObsGameCaptureMode,
70/// }
71/// ```
72pub fn obs_object_builder(attr: TokenStream, item: TokenStream) -> TokenStream {
73    let id = parse_macro_input!(attr as LitStr);
74
75    let input = parse_macro_input!(item as DeriveInput);
76
77    let name = input.ident;
78    let generics = input.generics;
79    let visibility = input.vis;
80    let attributes = input.attrs;
81
82    let fields = match input.data {
83        Data::Struct(data) => match data.fields {
84            Fields::Named(fields) => fields.named,
85            _ => panic!("Only named fields are supported"),
86        },
87        _ => panic!("Only structs are supported"),
88    };
89
90    let id_value = id.value();
91    let fields_tokens = fields.iter().map(|f| {
92        let name = &f.ident;
93        quote! {
94            /// IGNORE THIS FIELD. This is just so intellisense doesn't get confused and isn't complaining
95            #name: u8
96        }
97    });
98
99    let field_initializers = fields.iter().map(|f| {
100        let name = &f.ident;
101        quote! {
102            #name: 0
103        }
104    });
105
106    let obs_properties = fields
107        .iter()
108        .filter_map(|f| {
109            let attr = f.attrs.iter().find(|e| e.path().is_ident("obs_property"));
110
111            attr.map(|a| (f, a))
112        })
113        .collect::<Vec<_>>();
114
115    let mut functions = Vec::new();
116    for (field, attr) in obs_properties {
117        let field_type = &field.ty;
118        let field_name = field.ident.as_ref().unwrap();
119
120        let name_values: Punctuated<MetaNameValue, Token![,]> = attr
121            .parse_args_with(Punctuated::parse_terminated)
122            .expect(&format!(
123                "Field {} has invalid obs_property, should be name value",
124                field_name
125            ));
126
127        let type_t = &name_values
128            .iter()
129            .find(|e| e.path.get_ident().unwrap().to_string() == "type_t")
130            .expect("type_t is required for obs_property")
131            .value;
132
133        let type_t = match type_t {
134            syn::Expr::Lit(e) => match &e.lit {
135                syn::Lit::Str(s) => s.value(),
136                _ => panic!("type_t must be a string"),
137            },
138            _ => panic!("type_t must be a string"),
139        };
140
141        #[allow(unused_variables)]
142        let mut obs_settings_name = field_name.to_string();
143        let pot_name = &name_values
144            .iter()
145            .find(|e| e.path.get_ident().unwrap().to_string() == "settings_key");
146
147        if let Some(n) = pot_name {
148            obs_settings_name = match &n.value {
149                syn::Expr::Lit(e) => match &e.lit {
150                    syn::Lit::Str(s) => s.value(),
151                    _ => panic!("setings_key must be a string"),
152                },
153                _ => panic!("settings_key must be a string"),
154            };
155        }
156
157        let (_docs_str, docs_attr) = collect_doc(&field.attrs);
158
159        let obs_settings_key = LitStr::new(&obs_settings_name, Span::call_site());
160        let set_field = quote::format_ident!("set_{}", field_name);
161        let type_t_str = type_t.as_str();
162        let to_add = match type_t_str {
163            "enum" => {
164                quote! {
165                    #(#docs_attr)*
166                    pub fn #set_field(mut self, #field_name: #field_type) -> Self {
167                        use num_traits::ToPrimitive;
168                        use libobs_wrapper::data::ObsObjectBuilder;
169                        let val = #field_name.to_i32().unwrap();
170
171                        self.get_or_create_settings()
172                            .set_int(#obs_settings_key, val as i64);
173
174                        self
175                    }
176                }
177            }
178            "enum_string" => {
179                quote! {
180                    #(#docs_attr)*
181                    pub fn #set_field(mut self, #field_name: #field_type) -> Self {
182                        use libobs_wrapper::data::{ObsObjectBuilder, StringEnum};
183
184                        self.get_or_create_settings()
185                            .set_string(#obs_settings_key, #field_name.to_str());
186
187                        self
188                    }
189                }
190            }
191            "string" => {
192                quote! {
193                    #(#docs_attr)*
194                    pub fn #set_field(mut self, #field_name: impl Into<libobs_wrapper::utils::ObsString>) -> Self {
195                        use libobs_wrapper::data::ObsObjectBuilder;
196                        self.get_or_create_settings()
197                            .set_string(#obs_settings_key, #field_name);
198                        self
199                    }
200                }
201            }
202            "bool" => {
203                quote! {
204                    #(#docs_attr)*
205                    pub fn #set_field(mut self, #field_name: bool) -> Self {
206                        use libobs_wrapper::data::ObsObjectBuilder;
207                        self.get_or_create_settings()
208                            .set_bool(#obs_settings_key, #field_name);
209                        self
210                    }
211                }
212            }
213            "int" => {
214                quote! {
215                    #(#docs_attr)*
216                    pub fn #set_field(mut self, #field_name: i64) -> Self {
217                        use libobs_wrapper::data::ObsObjectBuilder;
218                        self.get_or_create_settings()
219                            .set_int(#obs_settings_key, #field_name);
220                        self
221                    }
222                }
223            }
224            _ => panic!(
225                "Unsupported type_t {}. Should either be `enum`, `string`, `bool` or `int`",
226                type_t
227            ),
228        };
229
230        functions.push(to_add);
231    }
232
233    let expanded = quote! {
234        #(#attributes)*
235        #[allow(dead_code)]
236        #visibility struct #name #generics {
237            #(#fields_tokens,)*
238            settings: Option<libobs_wrapper::data::ObsData>,
239            hotkeys: Option<libobs_wrapper::data::ObsData>,
240            name: libobs_wrapper::utils::ObsString
241        }
242
243        impl libobs_wrapper::data::ObsObjectBuilder for #name {
244            fn new(name: impl Into<libobs_wrapper::utils::ObsString>) -> Self {
245                Self {
246                    #(#field_initializers,)*
247                    settings: None,
248                    hotkeys: None,
249                    name: name.into(),
250                }
251            }
252
253            fn get_settings(&self) -> &Option<libobs_wrapper::data::ObsData> {
254                &self.settings
255            }
256
257            fn get_settings_mut(&mut self) -> &mut Option<libobs_wrapper::data::ObsData> {
258                &mut self.settings
259            }
260
261            fn get_hotkeys(&self) -> &Option<libobs_wrapper::data::ObsData> {
262                &self.hotkeys
263            }
264
265            fn get_hotkeys_mut(&mut self) -> &mut Option<libobs_wrapper::data::ObsData> {
266                &mut self.hotkeys
267            }
268
269            fn get_name(&self) -> libobs_wrapper::utils::ObsString {
270                self.name.clone()
271            }
272
273            fn get_id() -> libobs_wrapper::utils::ObsString {
274                #id_value.into()
275            }
276        }
277
278        impl #name {
279            #(#functions)*
280        }
281    };
282
283    TokenStream::from(expanded)
284}
285
286fn collect_doc(attrs: &Vec<Attribute>) -> (Vec<String>, Vec<&Attribute>) {
287    let mut docs_str = Vec::new();
288    let mut docs_attr = Vec::new();
289    for attr in attrs {
290        let name_val = match &attr.meta {
291            syn::Meta::NameValue(n) => n,
292            _ => continue,
293        };
294
295        let is_doc = name_val.path.is_ident("doc");
296        if !is_doc {
297            continue;
298        }
299
300        let lit = match &name_val.value {
301            Expr::Lit(l) => match &l.lit {
302                syn::Lit::Str(s) => s.value(),
303                _ => continue,
304            },
305            _ => continue,
306        };
307
308        docs_str.push(lit);
309        docs_attr.push(attr);
310    }
311
312    (docs_str, docs_attr)
313}