1use proc_macro::TokenStream;
2use quote::{format_ident, quote};
3use syn::{FnArg, Item, ItemFn, Pat, PatIdent, parse_macro_input};
4
5fn into_target(ty: &syn::Type) -> Option<syn::Type> {
8 let syn::Type::ImplTrait(it) = ty else {
9 return None;
10 };
11 for bound in &it.bounds {
12 let syn::TypeParamBound::Trait(tb) = bound else {
13 continue;
14 };
15 let seg = tb.path.segments.last()?;
16 if seg.ident != "Into" {
17 continue;
18 }
19 if let syn::PathArguments::AngleBracketed(args) = &seg.arguments
20 && let Some(syn::GenericArgument::Type(inner)) = args.args.first()
21 {
22 return Some(inner.clone());
23 }
24 }
25 None
26}
27
28#[proc_macro_attribute]
35pub fn wire(_args: TokenStream, input: TokenStream) -> TokenStream {
36 let item = parse_macro_input!(input as Item);
37
38 quote! {
39 #[derive(
40 ::std::fmt::Debug,
41 cindy::__reexports::serde::Serialize,
42 cindy::__reexports::serde::Deserialize
43 )]
44 #[serde(crate = "cindy::__reexports::serde")]
45 #item
46 }
47 .into()
48}
49
50#[proc_macro_attribute]
67pub fn main(_args: TokenStream, input: TokenStream) -> TokenStream {
68 let input = parse_macro_input!(input as ItemFn);
69
70 let (attrs, vis, sig, block) = (&input.attrs, &input.vis, &input.sig, &input.block);
71 let output = &sig.output;
72 let inputs = &sig.inputs;
73
74 let invoke_user_main = match inputs.len() {
75 0 => quote! {
76 cindy::__reexports::tokio::spawn(__user_main())
77 },
78 1 => {
79 let host_type = match &inputs[0] {
80 FnArg::Typed(pt) => &pt.ty,
81 FnArg::Receiver(_) => {
82 panic!("`#[cindy::main]` cannot take `self`");
83 }
84 };
85 quote! {
86 {
87 let __host_json = ::std::env::var("CINDY_HOST_CONTEXT").expect(
88 "CINDY_HOST_CONTEXT not set!\n\
89 The orchestrator process is meant to be launched by `cindy` command line tool. \
90 If you're trying to run the binary directly, set CINDY_HOST_CONTEXT to a \
91 JSON-serialised `cindy::Host<V>` first."
92 );
93 let __host: #host_type =
94 cindy::__reexports::serde_json::from_str(&__host_json)
95 .expect("CINDY_HOST_CONTEXT was not valid JSON for the declared `cindy::Host<V>` type");
96 cindy::__reexports::tokio::spawn(__user_main(__host))
97 }
98 }
99 }
100 _ => panic!(
101 "`#[cindy::main]` accepts at most one parameter (the host context, `cindy::Host<V>`)"
102 ),
103 };
104
105 quote! {
106 #(#attrs)*
107 #[cindy::__reexports::tokio::main(crate = "cindy::__reexports::tokio")]
108 #vis async fn main() {
109 let (rpc_in, rpc_out) = cindy::common::quarantine_stdio();
110
111 #[cfg(feature = "orchestrator")]
112 if ::std::env::var_os("CINDY_DUMP_INVENTORY").is_some() {
113 let entries: ::std::vec::Vec<&cindy::inventory::RegisteredInventory> =
114 cindy::__reexports::inventory::iter::<cindy::inventory::RegisteredInventory>
115 .into_iter()
116 .collect();
117 let dump: cindy::inventory::InventoryDump = match entries.as_slice() {
118 [one] => (one.dump)().await,
119 _ => {
120 ::std::eprintln!(
121 "There must be exactly 1 `#[cindy::inventory]` registered."
122 );
123 ::std::process::exit(2);
124 }
125 };
126 let bytes = cindy::__reexports::serde_json::to_vec(&dump)
127 .expect("Failed to serialize inventory dump to JSON");
128 {
129 use cindy::__reexports::tokio::io::AsyncWriteExt as _;
130 let mut out = rpc_out;
131 out.write_all(&bytes).await.expect("Failed to write inventory");
132 out.flush().await.expect("Failed to flush inventory");
133 }
134 ::std::process::exit(0);
135 }
136
137 #[cfg(feature = "orchestrator")]
143 if ::std::env::var_os("CINDY_DUMP_VAULTS").is_some() {
144 use cindy::__reexports::tokio::io::AsyncWriteExt as _;
145 let vaults = cindy::secret::registered_vaults();
146 let bytes = cindy::__reexports::serde_json::to_vec(&vaults)
147 .expect("Failed to serialise vault list");
148 let mut out = rpc_out;
149 out.write_all(&bytes).await.expect("Failed to write vault list");
150 out.flush().await.expect("Failed to flush vault list");
151 ::std::process::exit(0);
152 }
153
154 #[cfg(feature = "orchestrator")]
155 if ::std::env::var_os("CINDY_SEAL_SECRETS").is_some() {
156 use cindy::__reexports::tokio::io::AsyncWriteExt as _;
157 let mut out = rpc_out;
158 let mut failed = false;
159 for pending in cindy::__reexports::inventory::iter::<cindy::secret::PendingSecret>() {
160 let plaintext = (pending.serialize)();
161 let dek = match cindy::secret::keychain::get_dek(pending.vault) {
170 Ok(d) => d,
171 Err(e) => {
172 ::std::eprintln!(
173 "cindy secret seal: couldn't load DEK for vault `{}` \
174 (referenced from {}:{}:{}): {e:#}",
175 pending.vault, pending.file, pending.line, pending.column,
176 );
177 failed = true;
178 continue;
179 }
180 };
181 let ciphertext = match cindy::secret::crypto::seal(&dek, &plaintext) {
182 Ok(c) => c,
183 Err(e) => {
184 ::std::eprintln!(
185 "cindy secret seal: encryption failed for {}:{}:{} ({e:#})",
186 pending.file, pending.line, pending.column,
187 );
188 failed = true;
189 continue;
190 }
191 };
192 use cindy::__reexports::base64::Engine as _;
193 let b64 = cindy::__reexports::base64::engine::general_purpose::STANDARD
194 .encode(&ciphertext);
195 let line = cindy::__reexports::serde_json::json!({
196 "file": pending.file,
197 "line": pending.line,
198 "column": pending.column,
199 "vault": pending.vault,
200 "ciphertext": b64,
201 });
202 let mut bytes = cindy::__reexports::serde_json::to_vec(&line)
203 .expect("Failed to serialise seal record");
204 bytes.push(b'\n');
205 out.write_all(&bytes).await.expect("Failed to write seal record");
206 }
207 out.flush().await.expect("Failed to flush seal records");
208 ::std::process::exit(if failed { 2 } else { 0 });
209 }
210
211 #[cfg(all(feature = "remote", not(feature = "orchestrator")))]
218 {
219 cindy::remote::rpc(rpc_in, rpc_out).await;
220 ::std::process::exit(0);
221 }
222
223 #[cfg(feature = "orchestrator")]
229 {
230 async fn __user_main(#inputs) #output #block
231
232 let __cindy_vault_keys = match cindy::secret::keychain::decode_env_keys() {
239 ::std::result::Result::Ok(m) => m.unwrap_or_default(),
240 ::std::result::Result::Err(e) => {
241 ::std::eprintln!("\x1b[31m{:?}\x1b[0m", e);
242 ::std::process::exit(1);
243 }
244 };
245 if let ::std::result::Result::Err(e) =
246 cindy::secret::keychain::install_raw_keys(__cindy_vault_keys.clone())
247 {
248 ::std::eprintln!("\x1b[31m{:?}\x1b[0m", e);
249 ::std::process::exit(1);
250 }
251
252 if let ::std::result::Result::Err(e) = cindy::secret::preflight(
258 "the orchestrator",
259 ::std::env::var("CINDY_HOST_CONTEXT").ok().as_deref(),
260 ) {
261 ::std::eprintln!("\x1b[31m{:?}\x1b[0m", e);
262 ::std::process::exit(1);
263 }
264
265 let (tx, rx) = cindy::__reexports::tokio::sync::mpsc::unbounded_channel();
266 cindy::orchestrator::ORCHESTRATOR_TX
267 .set(tx)
268 .expect("ORCHESTRATOR_TX already set");
269 cindy::__reexports::tokio::spawn(cindy::orchestrator::rpc(
270 rx, rpc_in, rpc_out, __cindy_vault_keys,
271 ));
272 match #invoke_user_main.await {
273 Ok(Ok(_)) => {
274 ::std::process::exit(0);
275 }
276 Ok(Err(e)) => {
277 ::std::eprintln!("\x1b[31m{:?}\x1b[0m", e);
278 ::std::process::exit(1);
279 }
280 Err(_) => {
281 ::std::process::exit(1);
282 }
283 };
284 }
285 }
286 }
287 .into()
288}
289
290#[proc_macro_attribute]
317pub fn inventory(_args: TokenStream, input: TokenStream) -> TokenStream {
318 let input = parse_macro_input!(input as ItemFn);
319
320 let (attrs, vis, sig, block) = (&input.attrs, &input.vis, &input.sig, &input.block);
321 let function_ident = &sig.ident;
322 let asyncness = &sig.asyncness;
323 let output = &sig.output;
324 let inputs = &sig.inputs;
325
326 if !inputs.is_empty() {
327 panic!("`#[cindy::inventory]` functions must take no arguments");
328 }
329
330 let invocation = if asyncness.is_some() {
331 quote! { #function_ident().await }
332 } else {
333 quote! {
334 match cindy::__reexports::tokio::task::spawn_blocking(move || #function_ident())
335 .await
336 {
337 Ok(v) => v,
338 Err(je) => ::std::panic::resume_unwind(je.into_panic()),
339 }
340 }
341 };
342
343 quote! {
344 #(#attrs)*
345 #vis #asyncness fn #function_ident () #output #block
346
347 cindy::__reexports::inventory::submit! {
348 cindy::inventory::RegisteredInventory {
349 dump: || ::std::boxed::Box::pin(async move {
350 cindy::inventory::IntoInventoryDump::into_inventory_dump(#invocation)
351 }),
352 }
353 }
354 }
355 .into()
356}
357
358#[proc_macro_attribute]
374pub fn remote(_args: TokenStream, input: TokenStream) -> TokenStream {
375 let input = parse_macro_input!(input as ItemFn);
376
377 let (attrs, vis, sig, block) = (&input.attrs, &input.vis, &input.sig, &input.block);
378
379 if !sig.generics.params.is_empty() {
380 panic!("Generics not allowed. Remote functions cannot be generic.");
381 }
382
383 let function_ident = &sig.ident;
384 let asyncness = &sig.asyncness;
385 let inputs = &sig.inputs;
386 let return_type = match &sig.output {
387 syn::ReturnType::Default => quote! { () },
388 syn::ReturnType::Type(_, ty) => quote! { #ty },
389 };
390
391 let mut arg_idents = vec![];
392 let mut arg_types = vec![];
393 for input_arg in &sig.inputs {
394 match input_arg {
395 FnArg::Receiver(..) => panic!("Argument `self` not allowed"),
396 FnArg::Typed(pat_type) => match &*pat_type.pat {
397 Pat::Ident(PatIdent { ident, .. }) => {
398 arg_idents.push(ident);
399 arg_types.push(&pat_type.ty)
400 }
401 other => panic!("Only standard named arguments are supported. Found: {other:?}"),
402 },
403 }
404 }
405
406 let type_signature = arg_types
407 .iter()
408 .map(|ty| quote! { #ty }.to_string().replace(' ', ""))
409 .collect::<Vec<String>>()
410 .join(",");
411 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_default();
412 let absolute_span_path = std::path::PathBuf::from(proc_macro::Span::call_site().file());
413 let relative_path = absolute_span_path
414 .strip_prefix(&manifest_dir)
415 .unwrap_or(&absolute_span_path)
416 .to_string_lossy()
417 .to_string();
418 let crate_name = std::env::var("CARGO_PKG_NAME").unwrap_or_default();
419
420 let remote_fn_id = format!(
421 "::{}::{}::{}({})",
422 crate_name, relative_path, function_ident, type_signature
423 );
424
425 let invocation = if asyncness.is_some() {
426 quote! { #function_ident::inner( #(#arg_idents),* ).await }
427 } else {
428 quote! {
429 match cindy::__reexports::tokio::task::spawn_blocking(move || {
430 #function_ident::inner( #(#arg_idents),* )
431 })
432 .await
433 {
434 Ok(v) => v,
435 Err(je) => ::std::panic::resume_unwind(je.into_panic()),
436 }
437 }
438 };
439
440 let outer_docstring = format!(
441 "This function can be called from the orchestrator.
442
443To run the remote-side version of this function (e.g. to call it from another remote function),
444see [`{function_ident}::inner`]."
445 );
446 let inner_docstring = format!(
447 "This function can be called from another remote functions.
448
449For documentation about the actual function, please refer to [`{function_ident}`]."
450 );
451 quote! {
452 #[allow(non_camel_case_types)]
453 #[doc(hidden)]
454 #vis enum #function_ident {}
455
456 #[doc(hidden)]
457 impl #function_ident {
458 #[doc = #inner_docstring]
459 #(#attrs)*
460 pub #asyncness fn inner (#inputs) -> #return_type #block
461 }
462
463 #[cfg(feature = "orchestrator")]
466 #[doc = #outer_docstring]
467 #(#attrs)*
468 #vis fn #function_ident (#inputs) -> cindy::orchestrator::Future<#return_type> {
469 let uuid = cindy::__reexports::uuid::Uuid::new_v4();
470 let payload = cindy::common::RemoteFnPayload {
471 uuid,
472 fn_id: #remote_fn_id.to_string(),
473 data: cindy::__reexports::postcard::to_allocvec(&( #(#arg_idents),* ))
474 .expect("Failed to serialize args"),
475 };
476 let (tx, rx) = cindy::__reexports::tokio::sync::oneshot::channel();
477 cindy::orchestrator::ORCHESTRATOR_TX
478 .get()
479 .expect("ORCHESTRATOR_TX not set")
480 .send(cindy::orchestrator::OutboundRegistration { payload, tx })
481 .expect("Orchestrator channel closed");
482 cindy::orchestrator::Future::new(rx)
483 }
484
485 #[cfg(feature = "remote")]
486 cindy::__reexports::inventory::submit! {
487 cindy::remote::RemoteFn {
488 id: #remote_fn_id,
489 function: |args_bytes| {
490 let ( #(#arg_idents),* ): ( #(#arg_types),* ) =
491 cindy::__reexports::postcard::from_bytes(&args_bytes)
492 .expect("Failed to deserialize args");
493
494 ::std::boxed::Box::pin(async move {
495 let result = #invocation;
496 cindy::__reexports::postcard::to_allocvec(&result)
497 .expect("Failed to serialize return value")
498 })
499 },
500 }
501 }
502 }
503 .into()
504}
505
506#[proc_macro_attribute]
535pub fn action(_args: TokenStream, input: TokenStream) -> TokenStream {
536 let func = parse_macro_input!(input as ItemFn);
537 let (attrs, vis, sig, block) = (&func.attrs, &func.vis, &func.sig, &func.block);
538
539 if !sig.generics.params.is_empty() {
540 panic!("`#[action]` functions cannot have generic parameters (use `impl Into<T>` args)");
541 }
542
543 let ident = &sig.ident;
544 let raw_ident = format_ident!("{ident}_raw");
545 let is_async = sig.asyncness.is_some();
546 let (maybe_async, maybe_await) = if is_async {
547 (quote! { async }, quote! { .await })
548 } else {
549 (quote! {}, quote! {})
550 };
551 let return_type = match &sig.output {
552 syn::ReturnType::Default => quote! { () },
553 syn::ReturnType::Type(_, ty) => quote! { #ty },
554 };
555
556 let mut ergonomic_inputs = vec![];
562 let mut raw_inputs = vec![];
563 let mut arg_idents = vec![];
564 for input_arg in &sig.inputs {
565 let FnArg::Typed(pat_type) = input_arg else {
566 panic!("`#[action]` functions cannot take `self`");
567 };
568 let Pat::Ident(PatIdent { ident, .. }) = &*pat_type.pat else {
569 panic!("only standard named arguments are supported in `#[action]` fns");
570 };
571 arg_idents.push(ident.clone());
572 ergonomic_inputs.push(input_arg.clone());
573
574 let concrete_ty = into_target(&pat_type.ty).unwrap_or_else(|| (*pat_type.ty).clone());
575 raw_inputs.push(quote! { #ident: #concrete_ty });
576 }
577
578 quote! {
579 #[doc(hidden)]
582 #[cindy::remote]
583 #vis #maybe_async fn #raw_ident (#(#raw_inputs),*) -> #return_type #block
584
585 #[cfg(feature = "orchestrator")]
587 #(#attrs)*
588 #vis async fn #ident (#(#ergonomic_inputs),*) -> #return_type {
589 #raw_ident ( #(#arg_idents.into()),* ).await
590 }
591
592 #[allow(non_camel_case_types)]
597 #vis enum #ident {}
598
599 impl #ident {
600 #(#attrs)*
601 #vis #maybe_async fn inner (#(#ergonomic_inputs),*) -> #return_type {
602 #raw_ident ::inner( #(#arg_idents.into()),* ) #maybe_await
603 }
604 }
605 }
606 .into()
607}