1use proc_macro::{Delimiter, TokenStream, TokenTree};
19
20#[derive(Default)]
25struct TestOpts {
26 name: Option<String>,
27 timeout_time: Option<u64>,
28 timeout_unit: Option<String>,
29 skip: bool,
30 expect_fail: bool,
31 expect_error: Option<String>,
32}
33
34fn strip_quotes(s: &str) -> String {
35 s.trim_matches('"').to_string()
36}
37
38fn parse_test_opts(attr: TokenStream) -> Result<TestOpts, String> {
39 let mut opts = TestOpts::default();
40 let mut iter = attr.into_iter().peekable();
41 while let Some(tt) = iter.next() {
42 let key = match &tt {
43 TokenTree::Ident(i) => i.to_string(),
44 TokenTree::Punct(p) if p.as_char() == ',' => continue,
45 other => return Err(format!("unexpected token in #[rustdv::test(...)]: {other}")),
46 };
47 let mut value: Option<String> = None;
49 if let Some(TokenTree::Punct(p)) = iter.peek() {
50 if p.as_char() == '=' {
51 iter.next(); match iter.next() {
53 Some(TokenTree::Literal(l)) => value = Some(l.to_string()),
54 Some(TokenTree::Ident(i)) => value = Some(i.to_string()),
55 other => return Err(format!("expected value after '{key} =', got {other:?}")),
56 }
57 }
58 }
59 match key.as_str() {
60 "name" => opts.name = value.map(|v| strip_quotes(&v)),
61 "timeout_time" => {
62 let v = value.ok_or("timeout_time needs a value")?;
63 opts.timeout_time =
64 Some(v.parse::<u64>().map_err(|_| format!("bad timeout_time '{v}'"))?);
65 }
66 "timeout_unit" => opts.timeout_unit = value.map(|v| strip_quotes(&v)),
67 "skip" => opts.skip = value.map(|v| v == "true").unwrap_or(true),
68 "expect_fail" => opts.expect_fail = value.map(|v| v == "true").unwrap_or(true),
69 "expect_error" => {
72 let v = value.ok_or("expect_error needs a value, e.g. expect_error = \"config_not_found\"")?;
73 opts.expect_error = Some(strip_quotes(&v));
74 }
75 other => return Err(format!("unknown #[rustdv::test] option '{other}'")),
76 }
77 }
78 Ok(opts)
79}
80
81#[derive(Copy, Clone, PartialEq, Eq)]
83enum TestForm {
84 Function,
87 Component,
91}
92
93fn find_item(item: &TokenStream) -> Option<(TestForm, String)> {
98 let mut form: Option<TestForm> = None;
99 for tt in item.clone() {
100 if let TokenTree::Ident(i) = tt {
101 let s = i.to_string();
102 if let Some(f) = form {
103 return Some((f, s));
104 }
105 form = match s.as_str() {
106 "fn" => Some(TestForm::Function),
107 "struct" | "type" => Some(TestForm::Component),
108 _ => None,
109 };
110 }
111 }
112 None
113}
114
115fn compile_error(msg: &str) -> TokenStream {
116 format!("compile_error!({msg:?});").parse().unwrap()
117}
118
119#[proc_macro_attribute]
129pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream {
130 let opts = match parse_test_opts(attr) {
131 Ok(o) => o,
132 Err(e) => return compile_error(&e),
133 };
134 let Some((form, item_name)) = find_item(&item) else {
135 return compile_error(
136 "#[rustdv::test] must be applied to an async fn, a struct, or a type alias",
137 );
138 };
139 let fn_name = item_name;
140 let test_name = opts.name.unwrap_or_else(|| fn_name.clone());
141 let timeout = match (opts.timeout_time, opts.timeout_unit) {
142 (Some(t), Some(u)) => format!("::core::option::Option::Some(({t}u64, \"{u}\"))"),
143 (Some(t), None) => format!("::core::option::Option::Some(({t}u64, \"ns\"))"),
144 _ => "::core::option::Option::None".to_string(),
145 };
146 let skip = opts.skip;
147 let expect_fail = opts.expect_fail;
148 let expect_error = match &opts.expect_error {
149 Some(k) => format!("::core::option::Option::Some(\"{k}\")"),
150 None => "::core::option::Option::None".to_string(),
151 };
152
153 let body = match form {
158 TestForm::Function => format!("::std::boxed::Box::pin({fn_name}(ctx))"),
159 TestForm::Component => format!(
160 r#"::std::boxed::Box::pin(async move {{
161 let mut __ctx = ctx;
162 let mut __test = <{fn_name} as ::core::default::Default>::default();
163 ::rustdv::run_component_test(&mut __test, &mut __ctx).await
164 }})"#
165 ),
166 };
167
168 let reg = format!(
169 r#"
170const _: () = {{
171 fn __rustdv_shim(
172 ctx: ::rustdv::RustdvCtx,
173 ) -> ::std::pin::Pin<::std::boxed::Box<
174 dyn ::std::future::Future<Output = ::core::result::Result<(), ::rustdv::TestError>>,
175 >> {{
176 {body}
177 }}
178 #[used]
179 #[cfg_attr(not(target_vendor = "apple"), link_section = "rustdv_tests")]
180 #[cfg_attr(target_vendor = "apple", link_section = "__DATA,rustdv_tests")]
181 static __RUSTDV_TEST_REG: &'static ::rustdv::TestRegistration = &::rustdv::TestRegistration {{
182 name: "{test_name}",
183 module: ::core::module_path!(),
184 file: ::core::file!(),
185 line: ::core::line!(),
186 run: __rustdv_shim,
187 timeout: {timeout},
188 skip: {skip},
189 expect_fail: {expect_fail},
190 expect_error: {expect_error},
191 }};
192}};
193"#
194 );
195
196 let mut out = item;
197 out.extend(reg.parse::<TokenStream>().expect("rustdv-macros: generated code failed to parse"));
198 out
199}
200
201struct Field {
206 name: String,
207 ty: String,
208 is_child: bool,
209 port: Option<String>,
212}
213
214fn parse_struct(input: TokenStream) -> Result<(String, String, String, Vec<Field>), String> {
218 let mut iter = input.into_iter().peekable();
219 let mut struct_name: Option<String> = None;
220
221 while let Some(tt) = iter.next() {
223 if let TokenTree::Ident(i) = &tt {
224 if i.to_string() == "struct" {
225 match iter.next() {
226 Some(TokenTree::Ident(n)) => {
227 struct_name = Some(n.to_string());
228 break;
229 }
230 _ => return Err("expected struct name".into()),
231 }
232 }
233 }
234 }
235 let name = struct_name.ok_or("#[derive(Component)] supports only structs")?;
236
237 let mut fields_group = None;
242 let mut unit_struct = false;
243 let mut generics_tokens: Vec<TokenTree> = Vec::new();
244 let mut depth = 0i32;
245 for tt in iter {
246 match &tt {
247 TokenTree::Group(g) if g.delimiter() == Delimiter::Brace && depth == 0 => {
248 fields_group = Some(g.clone());
249 break;
250 }
251 TokenTree::Punct(p) if p.as_char() == ';' && depth == 0 => {
252 unit_struct = true;
253 break;
254 }
255 TokenTree::Punct(p) if p.as_char() == '<' => {
256 depth += 1;
257 generics_tokens.push(tt.clone());
258 continue;
259 }
260 TokenTree::Punct(p) if p.as_char() == '>' => {
261 depth -= 1;
262 generics_tokens.push(tt.clone());
263 continue;
264 }
265 _ => {}
266 }
267 if depth > 0 {
268 generics_tokens.push(tt.clone());
269 }
270 }
271 let impl_generics: String = {
273 let mut out = String::new();
274 for t in &generics_tokens {
275 let text = t.to_string();
276 if !out.is_empty() && !out.ends_with('\'') {
277 out.push(' ');
278 }
279 out.push_str(&text);
280 }
281 out
282 };
283 let type_params = {
284 let mut params: Vec<String> = Vec::new();
286 let mut d = 0i32;
287 let mut take_next_ident = true;
288 let mut lifetime = false;
289 for t in &generics_tokens {
290 match t {
291 TokenTree::Punct(p) if p.as_char() == '<' => d += 1,
292 TokenTree::Punct(p) if p.as_char() == '>' => d -= 1,
293 TokenTree::Punct(p) if p.as_char() == ',' && d == 1 => take_next_ident = true,
294 TokenTree::Punct(p) if p.as_char() == '\'' && d == 1 && take_next_ident => {
295 lifetime = true
296 }
297 TokenTree::Ident(i) if d == 1 && take_next_ident => {
298 let word = i.to_string();
299 if word == "const" {
300 continue; }
302 params.push(if lifetime { format!("'{word}") } else { word });
303 take_next_ident = false;
304 lifetime = false;
305 }
306 _ => {}
307 }
308 }
309 if params.is_empty() { String::new() } else { format!("< {} >", params.join(" , ")) }
310 };
311 if unit_struct {
312 return Ok((name, impl_generics, type_params, Vec::new()));
313 }
314 let group = fields_group
315 .ok_or("#[derive(Component)] requires named fields, or a unit struct")?;
316
317 let mut fields = Vec::new();
319 let mut pending_child = false;
320 let mut pending_port: Option<String> = None;
321 let mut current: Vec<TokenTree> = Vec::new();
322
323 let mut toks = group.stream().into_iter().peekable();
324 let mut angle_depth = 0i32;
325 while let Some(tt) = toks.next() {
326 match &tt {
327 TokenTree::Punct(p) if p.as_char() == '<' => angle_depth += 1,
328 TokenTree::Punct(p) if p.as_char() == '>' => angle_depth -= 1,
329 TokenTree::Punct(p) if p.as_char() == '#' => {
330 if let Some(TokenTree::Group(g)) = toks.peek() {
332 if g.delimiter() == Delimiter::Bracket {
333 let text = g.stream().to_string();
334 if text.starts_with("component") {
335 pending_child = true;
341 }
342 if text.starts_with("port") {
343 pending_port = attr_arg(&text);
344 }
345 toks.next(); continue;
347 }
348 }
349 }
350 TokenTree::Punct(p) if p.as_char() == ',' && angle_depth == 0 => {
351 if !current.is_empty() {
352 fields.push(make_field(¤t, pending_child, pending_port.take())?);
353 current.clear();
354 pending_child = false;
355 }
356 continue;
357 }
358 _ => {}
359 }
360 current.push(tt);
361 }
362 if !current.is_empty() {
363 fields.push(make_field(¤t, pending_child, pending_port.take())?);
364 }
365
366 Ok((name, impl_generics, type_params, fields))
367}
368
369fn attr_arg(text: &str) -> Option<String> {
372 let open = text.find('(')?;
373 let close = text.rfind(')')?;
374 let arg = text[open + 1..close].trim();
375 if arg.is_empty() {
376 None
377 } else {
378 Some(arg.to_string())
379 }
380}
381
382fn make_field(
383 tokens: &[TokenTree],
384 is_child: bool,
385 port: Option<String>,
386) -> Result<Field, String> {
387 let mut name = None;
388 let mut colon_at = None;
389 for (i, tt) in tokens.iter().enumerate() {
390 if let TokenTree::Punct(p) = tt {
391 if p.as_char() == ':' && colon_at.is_none() {
392 colon_at = Some(i);
393 break;
394 }
395 }
396 }
397 let colon = colon_at.ok_or("field without ':' (tuple structs unsupported)")?;
398 for tt in tokens[..colon].iter().rev() {
400 if let TokenTree::Ident(i) = tt {
401 name = Some(i.to_string());
402 break;
403 }
404 }
405 let name = name.ok_or("could not find field name")?;
406 let ty: String = tokens[colon + 1..].iter().map(|t| t.to_string()).collect::<Vec<_>>().join(" ");
407 Ok(Field { name, ty, is_child, port })
408}
409
410#[proc_macro_derive(Component, attributes(component, port))]
417pub fn derive_component(input: TokenStream) -> TokenStream {
418 let (name, impl_generics, type_params, fields) = match parse_struct(input) {
419 Ok(v) => v,
420 Err(e) => return compile_error(&e),
421 };
422
423 let mut visits = String::new();
424 let mut resolves = String::new();
425 let mut takes = String::new();
426 let mut restores = String::new();
427 for f in fields.iter().filter(|f| f.is_child) {
428 let fname = &f.name;
429 let ty = f.ty.trim_start();
430 if ty.starts_with("RustdvComp") {
431 visits.push_str(&format!(
434 "if let ::core::option::Option::Some(__c) = self.{fname}.as_node_mut() {{ __out.push((::std::string::String::from(\"{fname}\"), __c)); }}\n"
435 ));
436 resolves.push_str(&format!("self.{fname}.resolve(__ctx, \"{fname}\");\n"));
437 takes.push_str(&format!(
441 "if let ::core::option::Option::Some(__c) = self.{fname}.take_node() {{ __out.push((::std::string::String::from(\"{fname}\"), __c)); }}\n"
442 ));
443 restores.push_str(&format!(
444 "if __name == \"{fname}\" {{ self.{fname}.put_node(__node); continue; }}\n"
445 ));
446 } else if ty.starts_with("Option") {
447 visits.push_str(&format!(
450 "if let ::core::option::Option::Some(__c) = &mut self.{fname} {{ __out.push((::std::string::String::from(\"{fname}\"), __c as &mut (dyn ::rustdv::ComponentNode + 'static))); }}\n"
451 ));
452 } else if ty.starts_with("Vec") {
453 visits.push_str(&format!(
454 "for (__i, __c) in self.{fname}.iter_mut().enumerate() {{ __out.push((::std::format!(\"{fname}[{{}}]\", __i), __c as &mut (dyn ::rustdv::ComponentNode + 'static))); }}\n"
455 ));
456 } else {
457 visits.push_str(&format!(
458 "__out.push((::std::string::String::from(\"{fname}\"), &mut self.{fname} as &mut (dyn ::rustdv::ComponentNode + 'static)));\n"
459 ));
460 }
461 }
462
463 let mut port_arms = String::new();
470 let mut port_items = String::new();
471 let mut port_consts = String::new();
472 for f in fields.iter() {
473 let Some(kind) = f.port.as_deref() else { continue };
474 if !matches!(kind, "put" | "get" | "peek" | "publish" | "subscribe" | "seq_item") {
475 return compile_error(&format!(
476 "#[port({kind})]: expected put, get, peek, peek, publish, subscribe or seq_item"
477 ));
478 }
479 let required = !matches!(kind, "publish" | "subscribe");
482 let fname = &f.name;
483 let ty = f.ty.trim();
484 let konst = fname.to_uppercase();
485 port_arms.push_str(&format!(
486 "\"{fname}\" => ::core::option::Option::Some(::rustdv::PortField::slot_any(&self.{fname})),\n "
487 ));
488 port_items.push_str(&format!(
489 "::rustdv::PortInfo {{ name: \"{fname}\", kind: \"{kind}\", required: {required}, connected: ::rustdv::PortField::bound(&self.{fname}) }},\n "
490 ));
491 port_consts.push_str(&format!(
492 " /// The `{fname}` port, for `connect`.\n pub const {konst}: ::rustdv::PortName<<{ty} as ::rustdv::PortField>::Iface> = ::rustdv::PortName::new(\"{fname}\");\n"
493 ));
494 }
495
496 let port_impl = if port_arms.is_empty() {
497 String::new()
498 } else {
499 format!(
500 " fn port_slot(&self, __name: &str) -> ::core::option::Option<::std::rc::Rc<dyn ::std::any::Any>> {{\n \
501 match __name {{\n {port_arms}_ => ::core::option::Option::None,\n }}\n }}\n\
502 \n fn port_infos(&self) -> ::std::vec::Vec<::rustdv::PortInfo> {{\n \
503 ::std::vec![\n {port_items}]\n }}\n"
504 )
505 };
506
507 let owner_impl = format!(
510 r#"
511impl {impl_generics} ::rustdv::PortOwner for {name} {type_params} {{
512 fn owner_port_slot(&self, __name: &str) -> ::core::option::Option<::std::rc::Rc<dyn ::std::any::Any>> {{
513 ::rustdv::ComponentNode::port_slot(self, __name)
514 }}
515 fn owner_label(&self) -> &'static str {{ "{name}" }}
516}}
517"#
518 );
519
520 let const_impl = if port_consts.is_empty() {
521 String::new()
522 } else {
523 format!("\nimpl {impl_generics} {name} {type_params} {{\n{port_consts}}}\n")
524 };
525
526 let resolve_impl = if resolves.is_empty() {
528 String::new()
529 } else {
530 format!(
531 " fn resolve_children(&mut self, __ctx: &::rustdv::RustdvCtx) {{\n {resolves} }}\n"
532 )
533 };
534
535 let take_impl = if takes.is_empty() {
538 String::new()
539 } else {
540 format!(
541 " fn take_children(&mut self) -> ::std::vec::Vec<(::std::string::String, ::std::boxed::Box<dyn ::rustdv::ComponentNode>)> {{\n \
542 let mut __out: ::std::vec::Vec<(::std::string::String, ::std::boxed::Box<dyn ::rustdv::ComponentNode>)> = ::std::vec::Vec::new();\n \
543 {takes} __out\n }}\n\
544 \n fn restore_children(&mut self, __taken: ::std::vec::Vec<(::std::string::String, ::std::boxed::Box<dyn ::rustdv::ComponentNode>)>) {{\n \
545 for (__name, __node) in __taken {{\n \
546 let __name: &str = &__name;\n {restores} }}\n }}\n"
547 )
548 };
549
550 let registration = if type_params.is_empty() {
555 format!(
556 r#"
557const _: () = {{
558 fn __rustdv_comp_name() -> &'static str {{ "{name}" }}
559 fn __rustdv_comp_make() -> ::std::boxed::Box<dyn ::rustdv::ComponentNode> {{
560 ::std::boxed::Box::new(<{name} as ::core::default::Default>::default())
561 }}
562 #[used]
563 #[cfg_attr(not(target_vendor = "apple"), link_section = "rustdv_comps")]
564 #[cfg_attr(target_vendor = "apple", link_section = "__DATA,rustdv_comps")]
565 static __RUSTDV_COMP_REG: &::rustdv::ComponentReg = &::rustdv::ComponentReg {{
566 name: __rustdv_comp_name,
567 make: __rustdv_comp_make,
568 }};
569}};
570"#
571 )
572 } else {
573 String::new()
574 };
575
576 let out = format!(
577 r#"
578impl {impl_generics} ::rustdv::ComponentNode for {name} {type_params} {{
579 fn node_name(&self) -> &'static str {{ "{name}" }}
580 fn children_mut(&mut self) -> ::std::vec::Vec<(::std::string::String, &mut (dyn ::rustdv::ComponentNode + 'static))> {{
581 let mut __out: ::std::vec::Vec<(::std::string::String, &mut (dyn ::rustdv::ComponentNode + 'static))> = ::std::vec::Vec::new();
582 {visits}
583 __out
584 }}
585{port_impl}{resolve_impl}{take_impl}}}
586{owner_impl}{const_impl}{registration}"#
587 );
588 out.parse().expect("rustdv-macros: generated ComponentNode impl failed to parse")
589}