1use std::{collections::BTreeSet, env, fs, path::PathBuf};
4
5use proc_macro::TokenStream;
6use proc_macro_crate::{FoundCrate, crate_name};
7use quote::{format_ident, quote};
8use serde_json::{Map, Value, json};
9use syn::{
10 Attribute, Data, DeriveInput, Fields, GenericArgument, Item, ItemFn, ItemImpl, ItemStruct,
11 LitStr, Path, PathArguments, Token, Type, parse_macro_input, punctuated::Punctuated,
12};
13
14struct ModuleAttributes {
15 descriptor: Option<LitStr>,
16 configuration_schema: Option<LitStr>,
17 validate: Option<Path>,
18 prepare: Option<Path>,
19 activate: Option<Path>,
20 deactivate: Option<Path>,
21 lifecycle: bool,
22 consumer: bool,
23}
24
25impl syn::parse::Parse for ModuleAttributes {
26 fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
27 if input.is_empty() {
28 return Ok(Self {
29 descriptor: None,
30 configuration_schema: None,
31 validate: None,
32 prepare: None,
33 activate: None,
34 deactivate: None,
35 lifecycle: false,
36 consumer: false,
37 });
38 }
39 let mut descriptor = None;
40 let mut configuration_schema = None;
41 let mut validate = None;
42 let mut prepare = None;
43 let mut activate = None;
44 let mut deactivate = None;
45 let mut lifecycle = false;
46 let mut consumer = false;
47 while !input.is_empty() {
48 let name: syn::Ident = input.parse()?;
49 if name == "lifecycle" {
50 if lifecycle {
51 return Err(syn::Error::new(name.span(), "duplicate Module attribute"));
52 }
53 lifecycle = true;
54 if input.is_empty() {
55 break;
56 }
57 input.parse::<Token![,]>()?;
58 continue;
59 }
60 if name == "consumer" {
61 if consumer {
62 return Err(syn::Error::new(name.span(), "duplicate Module attribute"));
63 }
64 consumer = true;
65 if input.is_empty() {
66 break;
67 }
68 input.parse::<Token![,]>()?;
69 continue;
70 }
71 input.parse::<Token![=]>()?;
72 match name.to_string().as_str() {
73 "descriptor" if descriptor.is_none() => descriptor = Some(input.parse()?),
74 "configuration_schema" if configuration_schema.is_none() => {
75 configuration_schema = Some(input.parse()?);
76 }
77 "validate" if validate.is_none() => validate = Some(input.parse()?),
78 "prepare" if prepare.is_none() => prepare = Some(input.parse()?),
79 "activate" if activate.is_none() => activate = Some(input.parse()?),
80 "deactivate" if deactivate.is_none() => deactivate = Some(input.parse()?),
81 "descriptor"
82 | "configuration_schema"
83 | "validate"
84 | "prepare"
85 | "activate"
86 | "deactivate" => {
87 return Err(syn::Error::new(name.span(), "duplicate Module attribute"));
88 }
89 _ => {
90 return Err(syn::Error::new(
91 name.span(),
92 "expected `descriptor`, `configuration_schema`, `validate`, `prepare`, `activate`, `deactivate`, `lifecycle`, or `consumer`",
93 ));
94 }
95 }
96 if input.is_empty() {
97 break;
98 }
99 input.parse::<Token![,]>()?;
100 }
101 Ok(Self {
102 descriptor,
103 configuration_schema,
104 validate,
105 prepare,
106 activate,
107 deactivate,
108 lifecycle,
109 consumer,
110 })
111 }
112}
113
114#[proc_macro_attribute]
119pub fn module(attributes: TokenStream, item: TokenStream) -> TokenStream {
120 let attributes = parse_macro_input!(attributes as ModuleAttributes);
121 let item = parse_macro_input!(item as Item);
122 match item {
123 Item::Fn(function) => expand_module_function(&attributes, &function),
124 Item::Struct(module) => expand_module_struct(&attributes, module),
125 other => Err(syn::Error::new_spanned(
126 other,
127 "a native Module must be declared by a factory function or a named-field struct",
128 )),
129 }
130 .unwrap_or_else(syn::Error::into_compile_error)
131 .into()
132}
133
134#[proc_macro_derive(ModuleConfig, attributes(serde))]
136pub fn module_config(item: TokenStream) -> TokenStream {
137 let input = parse_macro_input!(item as DeriveInput);
138 expand_module_config(&input)
139 .unwrap_or_else(syn::Error::into_compile_error)
140 .into()
141}
142
143fn expand_module_config(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
144 let Data::Struct(data) = &input.data else {
145 return Err(syn::Error::new_spanned(
146 input,
147 "Module configuration must be a named-field struct",
148 ));
149 };
150 let Fields::Named(fields) = &data.fields else {
151 return Err(syn::Error::new_spanned(
152 &data.fields,
153 "Module configuration must use named fields",
154 ));
155 };
156 let mut properties = Map::new();
157 let mut required = Vec::new();
158 for field in &fields.named {
159 let ident = field.ident.as_ref().expect("named fields have identifiers");
160 let name = serde_field_name(&field.attrs, ident)?;
161 let (schema, optional) = configuration_type_schema(&field.ty)?;
162 properties.insert(name.clone(), schema);
163 if !optional {
164 required.push(Value::String(name));
165 }
166 }
167 let schema = canonical_json(&json!({
168 "$schema": "https://json-schema.org/draft/2020-12/schema",
169 "type": "object",
170 "additionalProperties": false,
171 "required": required,
172 "properties": properties,
173 }));
174 let macro_name = format_ident!("__lenso_config_schema_{}", snake(&input.ident.to_string()));
175 Ok(quote! {
176 #[doc(hidden)]
177 #[macro_export]
178 macro_rules! #macro_name {
179 () => { #schema };
180 }
181 })
182}
183
184fn serde_field_name(attributes: &[Attribute], ident: &syn::Ident) -> syn::Result<String> {
185 let mut name = ident.to_string();
186 for attribute in attributes {
187 if !attribute.path().is_ident("serde") {
188 continue;
189 }
190 attribute.parse_nested_meta(|meta| {
191 if meta.path.is_ident("rename") {
192 name = meta.value()?.parse::<LitStr>()?.value();
193 }
194 Ok(())
195 })?;
196 }
197 Ok(name)
198}
199
200fn configuration_type_schema(ty: &Type) -> syn::Result<(Value, bool)> {
201 let Type::Path(path) = ty else {
202 return Err(syn::Error::new_spanned(
203 ty,
204 "Module configuration fields must use portable named types",
205 ));
206 };
207 let segment = path.path.segments.last().expect("type paths are non-empty");
208 let name = segment.ident.to_string();
209 if name == "Option" {
210 return Ok((configuration_inner_schema(segment, ty)?, true));
211 }
212 if name == "Vec" {
213 return Ok((
214 json!({"type": "array", "items": configuration_inner_schema(segment, ty)?}),
215 false,
216 ));
217 }
218 let schema = match name.as_str() {
219 "String" => json!({"type": "string"}),
220 "bool" => json!({"type": "boolean"}),
221 "f32" | "f64" => json!({"type": "number"}),
222 "i8" | "i16" | "i32" | "i64" | "i128" | "isize" | "u8" | "u16" | "u32" | "u64" | "u128"
223 | "usize" => json!({"type": "integer"}),
224 _ => {
225 return Err(syn::Error::new_spanned(
226 ty,
227 "unsupported Module configuration field type; use String, bool, a number, Option<T>, or Vec<T>",
228 ));
229 }
230 };
231 Ok((schema, false))
232}
233
234fn configuration_inner_schema(segment: &syn::PathSegment, ty: &Type) -> syn::Result<Value> {
235 let PathArguments::AngleBracketed(arguments) = &segment.arguments else {
236 return Err(syn::Error::new_spanned(
237 ty,
238 "configuration container requires one type",
239 ));
240 };
241 let [GenericArgument::Type(inner)] = arguments.args.iter().collect::<Vec<_>>().as_slice()
242 else {
243 return Err(syn::Error::new_spanned(
244 ty,
245 "configuration container requires one type",
246 ));
247 };
248 configuration_type_schema(inner).map(|(schema, _)| schema)
249}
250
251fn expand_module_function(
252 attributes: &ModuleAttributes,
253 function: &ItemFn,
254) -> syn::Result<proc_macro2::TokenStream> {
255 let sdk = authoring_crate();
256 if attributes.descriptor.is_none() && attributes.configuration_schema.is_some() {
257 return Err(syn::Error::new_spanned(
258 function,
259 "`configuration_schema` requires `descriptor` on a factory function",
260 ));
261 }
262 if attributes.validate.is_some()
263 || attributes.prepare.is_some()
264 || attributes.activate.is_some()
265 || attributes.deactivate.is_some()
266 || attributes.lifecycle
267 || attributes.consumer
268 {
269 return Err(syn::Error::new_spanned(
270 function,
271 "struct-level Module attributes are unavailable on factory functions",
272 ));
273 }
274 let package_id = package_id()?;
275 let descriptor_json = attributes
276 .descriptor
277 .as_ref()
278 .map(|descriptor| {
279 module_descriptor(
280 &package_id,
281 descriptor,
282 attributes.configuration_schema.as_ref(),
283 )
284 })
285 .transpose()?;
286 let function_name = &function.sig.ident;
287 let generated_module = format_ident!("__lenso_module_{function_name}");
288 let descriptor_constant = descriptor_json.map(|descriptor| {
289 let artifact =
290 format!("LENSO_MODULE_DESCRIPTOR_V1\0{descriptor}\0END_LENSO_MODULE_DESCRIPTOR_V1");
291 let artifact_length = artifact.len();
292 let artifact = proc_macro2::Literal::byte_string(artifact.as_bytes());
293 let schema_tracking = attributes.configuration_schema.as_ref().map(|schema| {
294 quote! {
295 const _: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/", #schema));
296 }
297 });
298 quote! {
299 pub const MODULE_DESCRIPTOR_JSON: &str = #descriptor;
301 #[doc(hidden)]
303 #[used]
304 pub static __LENSO_MODULE_DESCRIPTOR_ARTIFACT: [u8; #artifact_length] = *#artifact;
305 #schema_tracking
306 }
307 });
308
309 Ok(quote! {
310 pub const PACKAGE_ID: &str = #package_id;
312 pub const PACKAGE_VERSION: &str = env!("CARGO_PKG_VERSION");
314 pub const FACTORY_IDENTITY: &str = concat!(#package_id, "@", env!("CARGO_PKG_VERSION"));
316 #descriptor_constant
317
318 #function
319
320 #[doc(hidden)]
321 mod #generated_module {
322 #[derive(Clone, Copy, Debug, Default)]
323 struct Factory;
324
325 impl #sdk::__private::NativeModuleFactory for Factory {
326 fn package_id(&self) -> &'static str {
327 #package_id
328 }
329
330 fn package_version(&self) -> &'static str {
331 env!("CARGO_PKG_VERSION")
332 }
333
334 fn instantiate(
335 &self,
336 context: #sdk::__private::NativeModuleFactoryContext<'_>,
337 ) -> Result<
338 #sdk::__private::NativeModuleInstance,
339 #sdk::__private::RuntimeFailure,
340 > {
341 super::#function_name(context)
342 }
343 }
344
345 fn factory() -> std::rc::Rc<dyn #sdk::__private::NativeModuleFactory> {
346 std::rc::Rc::new(Factory)
347 }
348
349 #sdk::__private::__inventory::submit! {
350 #sdk::__private::LinkedNativeModuleFactory::new(factory)
351 }
352
353 const _: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/Cargo.toml"));
355 }
356 })
357}
358
359#[proc_macro_attribute]
367pub fn provides(attributes: TokenStream, item: TokenStream) -> TokenStream {
368 let capabilities =
369 parse_macro_input!(attributes with Punctuated::<Path, Token![,]>::parse_terminated);
370 let implementation = parse_macro_input!(item as ItemImpl);
371 expand_provides(
372 &capabilities.into_iter().collect::<Vec<_>>(),
373 &implementation,
374 )
375 .unwrap_or_else(syn::Error::into_compile_error)
376 .into()
377}
378
379struct CapabilityContribution {
380 namespace: Path,
381 descriptor: syn::Ident,
382 endpoints: syn::Ident,
383 lower: syn::Ident,
384}
385
386fn capability_contributions(capabilities: &[Path]) -> syn::Result<Vec<CapabilityContribution>> {
387 let mut seen = BTreeSet::new();
388 capabilities
389 .iter()
390 .map(|capability| {
391 let path = quote!(#capability).to_string();
392 if !seen.insert(path) {
393 return Err(syn::Error::new_spanned(
394 capability,
395 "a Module cannot provide the same Capability more than once",
396 ));
397 }
398 let mut namespace = capability.clone();
399 let capability_ident = namespace
400 .segments
401 .pop()
402 .ok_or_else(|| syn::Error::new_spanned(capability, "Capability path is empty"))?
403 .into_value()
404 .ident;
405 namespace.segments.pop_punct();
406 if namespace.segments.is_empty() {
407 return Err(syn::Error::new_spanned(
408 capability,
409 "Capability must be namespace-qualified, for example `agent::Agent`",
410 ));
411 }
412 let capability_snake = snake(&capability_ident.to_string());
413 Ok(CapabilityContribution {
414 namespace,
415 descriptor: format_ident!("__lenso_provided_{capability_snake}"),
416 endpoints: format_ident!("__lenso_native_endpoints_{capability_snake}"),
417 lower: format_ident!("__lenso_native_lower_{capability_snake}"),
418 })
419 })
420 .collect()
421}
422
423fn provided_module(
424 capabilities: &[Path],
425 implementation: &ItemImpl,
426) -> syn::Result<(syn::Ident, bool)> {
427 if capabilities.is_empty() {
428 return Err(syn::Error::new_spanned(
429 implementation,
430 "`provides` requires at least one namespace-qualified Capability",
431 ));
432 }
433 if capabilities.len() > 1 && implementation.trait_.is_some() {
434 return Err(syn::Error::new_spanned(
435 implementation,
436 "multiple Capabilities require one inherent impl containing their domain methods",
437 ));
438 }
439 let Type::Path(module_type) = implementation.self_ty.as_ref() else {
440 return Err(syn::Error::new_spanned(
441 &implementation.self_ty,
442 "the Module provider type must be a path",
443 ));
444 };
445 let module_ident = module_type
446 .path
447 .segments
448 .last()
449 .ok_or_else(|| {
450 syn::Error::new_spanned(&module_type.path, "the Module provider type is empty")
451 })?
452 .ident
453 .clone();
454 Ok((module_ident, implementation.trait_.is_none()))
455}
456
457fn expand_provides(
458 capabilities: &[Path],
459 implementation: &ItemImpl,
460) -> syn::Result<proc_macro2::TokenStream> {
461 let sdk = authoring_crate();
462 let (module_ident, lowers_domain_methods) = provided_module(capabilities, implementation)?;
463 let contributions = capability_contributions(capabilities)?;
464 let provided_descriptors = contributions
465 .iter()
466 .map(|contribution| {
467 let namespace = &contribution.namespace;
468 let descriptor = &contribution.descriptor;
469 quote!(#namespace::#descriptor!())
470 })
471 .collect::<Vec<_>>();
472 let module_descriptor = format_ident!(
473 "__lenso_module_descriptor_{}",
474 snake(&module_ident.to_string())
475 );
476 let generated_module = format_ident!("__lenso_provider_{}", snake(&module_ident.to_string()));
477 let lifecycle = format_ident!("__LensoLifecycle{module_ident}");
478 let artifact = format_ident!("__LENSO_MODULE_DESCRIPTOR_ARTIFACT_{module_ident}");
479 let provider_implementations = if lowers_domain_methods {
480 contributions
481 .iter()
482 .map(|contribution| {
483 let namespace = &contribution.namespace;
484 let lower = &contribution.lower;
485 quote! { #namespace::#lower!(#module_ident, #sdk::__private); }
486 })
487 .collect::<Vec<_>>()
488 } else {
489 Vec::new()
490 };
491 let endpoint_contributions = contributions.iter().map(|contribution| {
492 let namespace = &contribution.namespace;
493 let endpoints = &contribution.endpoints;
494 quote! {
495 let (provided_requests, provided_streams, provided_events) =
496 super::#namespace::#endpoints!(module.clone(), #sdk::__private);
497 request_endpoints.extend(provided_requests);
498 stream_endpoints.extend(provided_streams);
499 event_endpoints.extend(provided_events);
500 }
501 });
502
503 let mut implementation = implementation.clone();
504 implementation
505 .attrs
506 .push(syn::parse_quote!(#[allow(clippy::unused_async, clippy::unused_async_trait_impl)]));
507
508 Ok(quote! {
509 #implementation
510 #(#provider_implementations)*
511
512 pub const MODULE_DESCRIPTOR_JSON: &str = #module_descriptor!(
514 #(#provided_descriptors),*
515 );
516 #[doc(hidden)]
517 const __LENSO_MODULE_DESCRIPTOR_ARTIFACT_TEXT: &str = concat!(
518 "LENSO_MODULE_DESCRIPTOR_V1\0",
519 #module_descriptor!(#(#provided_descriptors),*),
520 "\0END_LENSO_MODULE_DESCRIPTOR_V1",
521 );
522 #[doc(hidden)]
524 #[used]
525 pub static #artifact: &[u8] = __LENSO_MODULE_DESCRIPTOR_ARTIFACT_TEXT.as_bytes();
526
527 #[doc(hidden)]
528 mod #generated_module {
529 #[derive(Clone, Copy, Debug, Default)]
530 struct Factory;
531
532 impl #sdk::__private::NativeModuleFactory for Factory {
533 fn package_id(&self) -> &'static str { super::PACKAGE_ID }
534 fn package_version(&self) -> &'static str { super::PACKAGE_VERSION }
535
536 fn instantiate(
537 &self,
538 context: #sdk::__private::NativeModuleFactoryContext<'_>,
539 ) -> Result<
540 #sdk::__private::NativeModuleInstance,
541 #sdk::__private::RuntimeFailure,
542 > {
543 let module = super::#module_ident::__lenso_construct(context)?;
544 let lifecycle = super::#lifecycle { module: module.clone() };
545 let mut request_endpoints = Vec::new();
546 let mut stream_endpoints = Vec::new();
547 let mut event_endpoints = Vec::new();
548 #(#endpoint_contributions)*
549 Ok(#sdk::__private::NativeModuleInstance::with_all_endpoints(
550 request_endpoints,
551 stream_endpoints,
552 event_endpoints,
553 lifecycle,
554 ))
555 }
556 }
557
558 fn factory() -> ::std::rc::Rc<dyn #sdk::__private::NativeModuleFactory> {
559 ::std::rc::Rc::new(Factory)
560 }
561
562 #sdk::__private::__inventory::submit! {
563 #sdk::__private::LinkedNativeModuleFactory::new(factory)
564 }
565 }
566 })
567}
568
569#[allow(clippy::too_many_lines)]
570fn expand_module_struct(
571 attributes: &ModuleAttributes,
572 mut module: ItemStruct,
573) -> syn::Result<proc_macro2::TokenStream> {
574 let sdk = authoring_crate();
575 if attributes.descriptor.is_some() {
576 return Err(syn::Error::new_spanned(
577 &module.ident,
578 "struct-level Modules derive their Descriptor; remove `descriptor`",
579 ));
580 }
581 let package_id = package_id()?;
582 let package_version = env::var("CARGO_PKG_VERSION").map_err(|_| {
583 syn::Error::new_spanned(
584 &module.ident,
585 "CARGO_PKG_VERSION is unavailable while deriving Module Descriptor",
586 )
587 })?;
588 let StructFields {
589 config_type,
590 ports,
591 initializers,
592 } = analyze_struct_fields(&mut module)?;
593 let schema = configuration_schema_tokens(
594 attributes.configuration_schema.as_ref(),
595 config_type.as_ref(),
596 )?;
597 let name = &module.ident;
598 let lifecycle_name = format_ident!("__LensoLifecycle{name}");
599 let descriptor_macro = format_ident!("__lenso_module_descriptor_{}", snake(&name.to_string()));
600 let requirement_macros = ports
601 .iter()
602 .map(|(_, client, cardinality)| requirement_macro(client, *cardinality))
603 .collect::<syn::Result<Vec<_>>>()?;
604 let connect_ports = ports.iter().map(|(field, _, _)| {
605 quote! { self.module.#field.connect(context.dependencies())?; }
606 });
607 let requirement_parts = intersperse_commas(requirement_macros);
608 let (prefix, after_schema, suffix, defaults) =
609 descriptor_affixes(&package_id, &package_version);
610 let construct_configuration = if let Some(config_type) = &config_type {
611 let validate = attributes
612 .validate
613 .as_ref()
614 .map(|path| quote!(#path(&configuration)?;));
615 quote! {
616 let configuration = #sdk::__private::serde_json::from_str::<#config_type>(context.configuration())
617 .map_err(|error| #sdk::__private::RuntimeFailure::InvalidResolvedPlan {
618 detail: format!("invalid {} configuration: {error}", #package_id),
619 })?;
620 #validate
621 }
622 } else {
623 if attributes.configuration_schema.is_some() {
624 return Err(syn::Error::new_spanned(
625 &module.ident,
626 "`configuration_schema` requires a `#[config]` field",
627 ));
628 }
629 if attributes.validate.is_some() {
630 return Err(syn::Error::new_spanned(
631 &module.ident,
632 "`validate` requires a `#[config]` field",
633 ));
634 }
635 quote! {
636 let configuration = #sdk::__private::serde_json::from_str::<#sdk::__private::serde_json::Value>(context.configuration())
637 .map_err(|error| #sdk::__private::RuntimeFailure::InvalidResolvedPlan {
638 detail: format!("invalid {} configuration: {error}", #package_id),
639 })?;
640 if !configuration.as_object().is_some_and(|object| object.is_empty()) {
641 return Err(#sdk::__private::RuntimeFailure::InvalidResolvedPlan {
642 detail: format!("{} does not accept configuration", #package_id),
643 });
644 }
645 }
646 };
647 if attributes.lifecycle
648 && (attributes.prepare.is_some()
649 || attributes.activate.is_some()
650 || attributes.deactivate.is_some())
651 {
652 return Err(syn::Error::new_spanned(
653 &module.ident,
654 "`lifecycle` replaces the `prepare`, `activate`, and `deactivate` function attributes",
655 ));
656 }
657 let prepare = if attributes.lifecycle {
658 quote! {
659 let module = self.module.clone();
660 Box::pin(async move { #sdk::Lifecycle::prepare(&module, context).await })
661 }
662 } else {
663 hook(attributes.prepare.as_ref(), &sdk)
664 };
665 let activate = if attributes.lifecycle {
666 quote! {
667 let module = self.module.clone();
668 Box::pin(async move { #sdk::Lifecycle::activate(&module, context).await })
669 }
670 } else {
671 hook(attributes.activate.as_ref(), &sdk)
672 };
673 let deactivate = if attributes.lifecycle {
674 quote! {
675 let module = self.module.clone();
676 Box::pin(async move { #sdk::Lifecycle::deactivate(&module, context).await })
677 }
678 } else {
679 hook(attributes.deactivate.as_ref(), &sdk)
680 };
681 let schema_tracking = schema_tracking(attributes.configuration_schema.as_ref());
682 let consumer_finalizer = if attributes.consumer {
683 let generated_module = format_ident!("__lenso_consumer_{}", snake(&name.to_string()));
684 let artifact = format_ident!("__LENSO_MODULE_DESCRIPTOR_ARTIFACT_{name}");
685 Some(quote! {
686 pub const MODULE_DESCRIPTOR_JSON: &str = #descriptor_macro!();
688 #[doc(hidden)]
689 const __LENSO_MODULE_DESCRIPTOR_ARTIFACT_TEXT: &str = concat!(
690 "LENSO_MODULE_DESCRIPTOR_V1\0",
691 #descriptor_macro!(),
692 "\0END_LENSO_MODULE_DESCRIPTOR_V1",
693 );
694 #[doc(hidden)]
696 #[used]
697 pub static #artifact: &[u8] = __LENSO_MODULE_DESCRIPTOR_ARTIFACT_TEXT.as_bytes();
698
699 #[doc(hidden)]
700 mod #generated_module {
701 #[derive(Clone, Copy, Debug, Default)]
702 struct Factory;
703
704 impl #sdk::__private::NativeModuleFactory for Factory {
705 fn package_id(&self) -> &'static str { super::PACKAGE_ID }
706 fn package_version(&self) -> &'static str { super::PACKAGE_VERSION }
707
708 fn instantiate(
709 &self,
710 context: #sdk::__private::NativeModuleFactoryContext<'_>,
711 ) -> Result<
712 #sdk::__private::NativeModuleInstance,
713 #sdk::__private::RuntimeFailure,
714 > {
715 let module = super::#name::__lenso_construct(context)?;
716 let lifecycle = super::#lifecycle_name { module };
717 Ok(#sdk::__private::NativeModuleInstance::with_lifecycle(
718 Vec::new(),
719 lifecycle,
720 ))
721 }
722 }
723
724 fn factory() -> ::std::rc::Rc<dyn #sdk::__private::NativeModuleFactory> {
725 ::std::rc::Rc::new(Factory)
726 }
727
728 #sdk::__private::__inventory::submit! {
729 #sdk::__private::LinkedNativeModuleFactory::new(factory)
730 }
731 }
732 })
733 } else {
734 None
735 };
736
737 Ok(quote! {
738 pub const PACKAGE_ID: &str = #package_id;
740 pub const PACKAGE_VERSION: &str = env!("CARGO_PKG_VERSION");
742 pub const FACTORY_IDENTITY: &str = concat!(#package_id, "@", env!("CARGO_PKG_VERSION"));
744
745 #module
746
747 #[doc(hidden)]
748 macro_rules! #descriptor_macro {
749 () => {
750 concat!(#prefix, #schema, #after_schema, #suffix #(, #requirement_parts)*, #defaults)
751 };
752 ($first:expr $(, $rest:expr)*) => {
753 concat!(#prefix, #schema, #after_schema, $first $(, ",", $rest)*, #suffix #(, #requirement_parts)*, #defaults)
754 };
755 }
756
757 impl #name {
758 #[doc(hidden)]
759 fn __lenso_construct(
760 context: #sdk::__private::NativeModuleFactoryContext<'_>,
761 ) -> Result<Self, #sdk::__private::RuntimeFailure> {
762 if context.entrypoint() != "default" {
763 return Err(#sdk::__private::RuntimeFailure::InvalidResolvedPlan {
764 detail: format!("unsupported {} entrypoint {}", #package_id, context.entrypoint()),
765 });
766 }
767 #construct_configuration
768 Ok(Self { #(#initializers),* })
769 }
770 }
771
772 #[doc(hidden)]
773 #[derive(Clone, Debug)]
774 struct #lifecycle_name {
775 module: #name,
776 }
777
778 impl #sdk::__private::ModuleLifecycle for #lifecycle_name {
779 fn prepare(&self, context: #sdk::__private::PrepareContext) -> #sdk::__private::ModuleFuture {
780 #prepare
781 }
782
783 fn activate(&self, context: #sdk::__private::ActivateContext) -> #sdk::__private::ModuleFuture {
784 let connected = (|| -> Result<(), #sdk::__private::RuntimeFailure> {
785 #(#connect_ports)*
786 Ok(())
787 })();
788 if let Err(error) = connected {
789 return Box::pin(#sdk::__private::futures::future::ready(Err(error)));
790 }
791 #activate
792 }
793
794 fn deactivate(&self, context: #sdk::__private::DeactivateContext) -> #sdk::__private::ModuleFuture {
795 #deactivate
796 }
797 }
798
799 const _: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/Cargo.toml"));
800 #schema_tracking
801
802 #consumer_finalizer
803 })
804}
805
806fn descriptor_affixes(
807 package_id: &str,
808 package_version: &str,
809) -> (String, &'static str, &'static str, &'static str) {
810 let prefix = format!(
811 "{{\"package_id\":{},\"package_revision\":{},\"entrypoint\":\"default\",\"configuration_schema\":",
812 serde_json::to_string(package_id).expect("package ID serializes"),
813 serde_json::to_string(package_version).expect("package version serializes"),
814 );
815 let after_schema = ",\"provided_capabilities\":[";
816 let suffix = "],\"required_capabilities\":[";
817 let defaults = "],\"execution_class\":\"lenso.native-rust@1\",\"restart_policy\":{\"mode\":\"never\",\"max_attempts\":0,\"window\":{\"secs\":0,\"nanos\":0},\"backoff\":{\"secs\":0,\"nanos\":0},\"stability\":{\"secs\":0,\"nanos\":0},\"jitter\":{\"secs\":0,\"nanos\":0}},\"criticality\":\"non_critical\"}";
818 (prefix, after_schema, suffix, defaults)
819}
820
821fn schema_tracking(path: Option<&LitStr>) -> Option<proc_macro2::TokenStream> {
822 path.map(|path| {
823 quote!(
824 const _: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/", #path));
825 )
826 })
827}
828
829fn configuration_schema_tokens(
830 schema_path: Option<&LitStr>,
831 config_type: Option<&Type>,
832) -> syn::Result<proc_macro2::TokenStream> {
833 if let Some(path) = schema_path {
834 let schema = canonical_json(&read_configuration_schema(path)?);
835 return Ok(quote!(#schema));
836 }
837 let Some(config_type) = config_type else {
838 let schema = canonical_json(&json!({
839 "$schema": "https://json-schema.org/draft/2020-12/schema",
840 "type": "object",
841 "additionalProperties": false,
842 "required": [],
843 "properties": {},
844 }));
845 return Ok(quote!(#schema));
846 };
847 let Type::Path(config) = config_type else {
848 return Err(syn::Error::new_spanned(
849 config_type,
850 "the `#[config]` field type must be a path",
851 ));
852 };
853 let mut namespace = config.path.clone();
854 let config_name = namespace
855 .segments
856 .pop()
857 .expect("type paths are non-empty")
858 .into_value()
859 .ident;
860 namespace.segments.pop_punct();
861 let macro_name = format_ident!("__lenso_config_schema_{}", snake(&config_name.to_string()));
862 if namespace.segments.is_empty() {
863 Ok(quote!(#macro_name!()))
864 } else {
865 Ok(quote!(#namespace::#macro_name!()))
866 }
867}
868
869struct StructFields {
870 config_type: Option<Type>,
871 ports: Vec<(syn::Ident, Path, PortCardinality)>,
872 initializers: Vec<proc_macro2::TokenStream>,
873}
874
875#[derive(Clone, Copy)]
876enum PortCardinality {
877 One,
878 Many,
879}
880
881fn analyze_struct_fields(module: &mut ItemStruct) -> syn::Result<StructFields> {
882 let Fields::Named(fields) = &mut module.fields else {
883 return Err(syn::Error::new_spanned(
884 &module.fields,
885 "a struct-level Module requires named fields",
886 ));
887 };
888 let mut config = None;
889 let mut ports = Vec::new();
890 let mut initializers = Vec::new();
891 for field in &mut fields.named {
892 let name = field.ident.as_ref().expect("named fields have identifiers");
893 if take_marker(&mut field.attrs, "config") {
894 if config.replace(field.ty.clone()).is_some() {
895 return Err(syn::Error::new_spanned(
896 field,
897 "a Module has exactly one `#[config]` field",
898 ));
899 }
900 initializers.push(quote!(#name: configuration));
901 } else if let Some((client, cardinality)) = port_client(&field.ty)? {
902 ports.push((name.clone(), client, cardinality));
903 initializers.push(quote!(#name: ::core::default::Default::default()));
904 } else {
905 initializers.push(quote!(#name: ::core::default::Default::default()));
906 }
907 }
908 Ok(StructFields {
909 config_type: config,
910 ports,
911 initializers,
912 })
913}
914
915fn take_marker(attributes: &mut Vec<Attribute>, name: &str) -> bool {
916 let present = attributes
917 .iter()
918 .any(|attribute| attribute.path().is_ident(name));
919 attributes.retain(|attribute| !attribute.path().is_ident(name));
920 present
921}
922
923fn port_client(ty: &Type) -> syn::Result<Option<(Path, PortCardinality)>> {
924 let Type::Path(path) = ty else {
925 return Ok(None);
926 };
927 let Some(segment) = path.path.segments.last() else {
928 return Ok(None);
929 };
930 let cardinality = if segment.ident == "Port" {
931 PortCardinality::One
932 } else if segment.ident == "ManyPort" {
933 PortCardinality::Many
934 } else {
935 return Ok(None);
936 };
937 let syn::PathArguments::AngleBracketed(arguments) = &segment.arguments else {
938 return Err(syn::Error::new_spanned(
939 ty,
940 "Port or ManyPort requires one Capability client type",
941 ));
942 };
943 let Some(syn::GenericArgument::Type(Type::Path(client))) = arguments.args.first() else {
944 return Err(syn::Error::new_spanned(
945 ty,
946 "Port or ManyPort requires one Capability client type",
947 ));
948 };
949 if arguments.args.len() != 1 {
950 return Err(syn::Error::new_spanned(
951 ty,
952 "Port or ManyPort requires one Capability client type",
953 ));
954 }
955 Ok(Some((client.path.clone(), cardinality)))
956}
957
958fn requirement_macro(
959 client: &Path,
960 cardinality: PortCardinality,
961) -> syn::Result<proc_macro2::TokenStream> {
962 if client.segments.len() < 2 {
963 return Err(syn::Error::new_spanned(
964 client,
965 "a Port client must be namespace-qualified, for example `model::ModelClient`",
966 ));
967 }
968 let mut namespace = client.clone();
969 let client_name = namespace
970 .segments
971 .pop()
972 .expect("checked length")
973 .into_value()
974 .ident;
975 namespace.segments.pop_punct();
976 let prefix = match cardinality {
977 PortCardinality::One => "__lenso_required_",
978 PortCardinality::Many => "__lenso_required_many_",
979 };
980 let macro_name = format_ident!("{}{}", prefix, snake(&client_name.to_string()));
981 Ok(quote!(#namespace::#macro_name!()))
982}
983
984fn intersperse_commas(values: Vec<proc_macro2::TokenStream>) -> Vec<proc_macro2::TokenStream> {
985 values
986 .into_iter()
987 .enumerate()
988 .flat_map(|(index, value)| {
989 if index == 0 {
990 vec![value]
991 } else {
992 vec![quote!(","), value]
993 }
994 })
995 .collect()
996}
997
998fn hook(path: Option<&Path>, sdk: &proc_macro2::TokenStream) -> proc_macro2::TokenStream {
999 path.map_or_else(
1000 || quote!(Box::pin(#sdk::__private::futures::future::ready(Ok(())))),
1001 |path| quote!(#path(&self.module, &context)),
1002 )
1003}
1004
1005fn canonical_json(value: &Value) -> String {
1006 serde_json::to_string(value).expect("JSON values serialize")
1007}
1008
1009fn authoring_crate() -> proc_macro2::TokenStream {
1010 for package in ["lenso", "lenso-native-adapter"] {
1011 match crate_name(package) {
1012 Ok(FoundCrate::Itself) => {
1013 let ident = format_ident!("{}", package.replace('-', "_"));
1014 return quote!(::#ident);
1015 }
1016 Ok(FoundCrate::Name(name)) => {
1017 let ident = format_ident!("{name}");
1018 return quote!(::#ident);
1019 }
1020 Err(_) => {}
1021 }
1022 }
1023 quote!(::lenso_native_adapter)
1024}
1025
1026fn snake(value: &str) -> String {
1027 let mut output = String::new();
1028 for (index, character) in value.chars().enumerate() {
1029 if character.is_ascii_uppercase() && index > 0 {
1030 output.push('_');
1031 }
1032 output.push(character.to_ascii_lowercase());
1033 }
1034 output
1035}
1036
1037fn module_descriptor(
1038 package_id: &str,
1039 descriptor: &LitStr,
1040 configuration_schema: Option<&LitStr>,
1041) -> syn::Result<String> {
1042 let supplied: Value = serde_json::from_str(&descriptor.value()).map_err(|error| {
1043 syn::Error::new(
1044 descriptor.span(),
1045 format!("Module Descriptor input is not valid JSON: {error}"),
1046 )
1047 })?;
1048 let mut supplied = supplied.as_object().cloned().ok_or_else(|| {
1049 syn::Error::new(
1050 descriptor.span(),
1051 "Module Descriptor input must be an object",
1052 )
1053 })?;
1054 if supplied.contains_key("configuration_schema") {
1055 return Err(syn::Error::new(
1056 descriptor.span(),
1057 "Module Descriptor input cannot contain `configuration_schema`; use the package-owned schema path attribute",
1058 ));
1059 }
1060 if let Some(schema_path) = configuration_schema {
1061 supplied.insert(
1062 "configuration_schema".to_owned(),
1063 read_configuration_schema(schema_path)?,
1064 );
1065 }
1066 for owned in [
1067 "package_id",
1068 "package_revision",
1069 "entrypoint",
1070 "execution_class",
1071 "restart_policy",
1072 "criticality",
1073 ] {
1074 if supplied.contains_key(owned) {
1075 return Err(syn::Error::new(
1076 descriptor.span(),
1077 format!("Module Descriptor input cannot override generated field `{owned}`"),
1078 ));
1079 }
1080 }
1081 let package_version = env::var("CARGO_PKG_VERSION").map_err(|_| {
1082 syn::Error::new(
1083 descriptor.span(),
1084 "CARGO_PKG_VERSION is unavailable while deriving Module Descriptor",
1085 )
1086 })?;
1087 Ok(complete_module_descriptor(
1088 package_id,
1089 &package_version,
1090 supplied,
1091 ))
1092}
1093
1094fn read_configuration_schema(schema_path: &LitStr) -> syn::Result<Value> {
1095 let relative = PathBuf::from(schema_path.value());
1096 if relative.is_absolute()
1097 || relative
1098 .components()
1099 .any(|component| !matches!(component, std::path::Component::Normal(_)))
1100 {
1101 return Err(syn::Error::new(
1102 schema_path.span(),
1103 "configuration Schema path must stay inside the Module package",
1104 ));
1105 }
1106 let manifest_dir = env::var_os("CARGO_MANIFEST_DIR").ok_or_else(|| {
1107 syn::Error::new(
1108 schema_path.span(),
1109 "CARGO_MANIFEST_DIR is unavailable while deriving configuration Schema",
1110 )
1111 })?;
1112 let path = PathBuf::from(manifest_dir).join(relative);
1113 let bytes = fs::read(&path).map_err(|error| {
1114 syn::Error::new(
1115 schema_path.span(),
1116 format!(
1117 "failed to read configuration Schema {}: {error}",
1118 path.display()
1119 ),
1120 )
1121 })?;
1122 let schema: Value = serde_json::from_slice(&bytes).map_err(|error| {
1123 syn::Error::new(
1124 schema_path.span(),
1125 format!(
1126 "configuration Schema {} is invalid JSON: {error}",
1127 path.display()
1128 ),
1129 )
1130 })?;
1131 if !schema.is_object() {
1132 return Err(syn::Error::new(
1133 schema_path.span(),
1134 "configuration Schema must be a JSON object",
1135 ));
1136 }
1137 Ok(schema)
1138}
1139
1140fn complete_module_descriptor(
1141 package_id: &str,
1142 package_version: &str,
1143 mut supplied: Map<String, Value>,
1144) -> String {
1145 let mut generated = Map::new();
1146 generated.insert("package_id".to_owned(), json!(package_id));
1147 generated.insert("package_revision".to_owned(), json!(package_version));
1148 generated.insert("entrypoint".to_owned(), json!("default"));
1149 for (key, value) in std::mem::take(&mut supplied) {
1150 generated.insert(key, value);
1151 }
1152 generated.insert("execution_class".to_owned(), json!("lenso.native-rust@1"));
1153 generated.insert(
1154 "restart_policy".to_owned(),
1155 json!({
1156 "mode": "never",
1157 "max_attempts": 0,
1158 "window": {"secs": 0, "nanos": 0},
1159 "backoff": {"secs": 0, "nanos": 0},
1160 "stability": {"secs": 0, "nanos": 0},
1161 "jitter": {"secs": 0, "nanos": 0}
1162 }),
1163 );
1164 generated.insert("criticality".to_owned(), json!("non_critical"));
1165 serde_json::to_string(&Value::Object(generated))
1166 .expect("generated Module Descriptor values must serialize")
1167}
1168
1169fn package_id() -> syn::Result<String> {
1170 let manifest_dir = env::var_os("CARGO_MANIFEST_DIR").ok_or_else(|| {
1171 syn::Error::new(
1172 proc_macro2::Span::call_site(),
1173 "CARGO_MANIFEST_DIR is unavailable",
1174 )
1175 })?;
1176 let manifest_path = PathBuf::from(manifest_dir).join("Cargo.toml");
1177 let manifest = fs::read_to_string(&manifest_path).map_err(|error| {
1178 syn::Error::new(
1179 proc_macro2::Span::call_site(),
1180 format!("failed to read {}: {error}", manifest_path.display()),
1181 )
1182 })?;
1183 let manifest: toml::Value = toml::from_str(&manifest).map_err(|error| {
1184 syn::Error::new(
1185 proc_macro2::Span::call_site(),
1186 format!("failed to parse {}: {error}", manifest_path.display()),
1187 )
1188 })?;
1189 manifest
1190 .get("package")
1191 .and_then(|package| package.get("metadata"))
1192 .and_then(|metadata| metadata.get("lenso"))
1193 .and_then(|lenso| lenso.get("package-id"))
1194 .and_then(toml::Value::as_str)
1195 .map(str::to_owned)
1196 .ok_or_else(|| {
1197 syn::Error::new(
1198 proc_macro2::Span::call_site(),
1199 "missing `[package.metadata.lenso] package-id = \"...\"` in Cargo.toml",
1200 )
1201 })
1202}
1203
1204#[cfg(test)]
1205mod tests {
1206 use super::*;
1207 use syn::parse_quote;
1208
1209 #[test]
1210 fn generated_descriptor_owns_identity_and_execution_defaults() {
1211 let supplied = serde_json::from_value::<Map<String, Value>>(json!({
1212 "provided_capabilities": [],
1213 "required_capabilities": []
1214 }))
1215 .unwrap();
1216 let descriptor = complete_module_descriptor("example.tool", "1.2.3", supplied);
1217 let descriptor: Value = serde_json::from_str(&descriptor).unwrap();
1218
1219 assert_eq!(descriptor["package_id"], "example.tool");
1220 assert_eq!(descriptor["package_revision"], "1.2.3");
1221 assert_eq!(descriptor["entrypoint"], "default");
1222 assert_eq!(descriptor["execution_class"], "lenso.native-rust@1");
1223 assert_eq!(descriptor["restart_policy"]["mode"], "never");
1224 assert_eq!(descriptor["criticality"], "non_critical");
1225 }
1226
1227 #[test]
1228 fn package_schema_is_embedded_as_descriptor_data() {
1229 let path = LitStr::new(
1230 "tests/fixtures/config.schema.json",
1231 proc_macro2::Span::call_site(),
1232 );
1233 let schema = read_configuration_schema(&path).unwrap();
1234
1235 assert_eq!(schema["type"], "object");
1236 assert_eq!(schema["required"], json!(["name"]));
1237 }
1238
1239 #[test]
1240 fn typed_ports_preserve_client_paths_and_cardinality() {
1241 let one: Type = parse_quote!(Port<secrets::SecretsClient>);
1242 let many: Type = parse_quote!(ManyPort<auth::AuthClient>);
1243
1244 let (one_client, one_cardinality) = port_client(&one).unwrap().unwrap();
1245 let (many_client, many_cardinality) = port_client(&many).unwrap().unwrap();
1246
1247 assert_eq!(quote!(#one_client).to_string(), "secrets :: SecretsClient");
1248 assert!(matches!(one_cardinality, PortCardinality::One));
1249 assert_eq!(quote!(#many_client).to_string(), "auth :: AuthClient");
1250 assert!(matches!(many_cardinality, PortCardinality::Many));
1251 }
1252
1253 #[test]
1254 fn multiple_capabilities_reject_trait_impls() {
1255 let implementation: ItemImpl = parse_quote! {
1256 impl fixture::Provider for ExampleModule {}
1257 };
1258 let error = expand_provides(
1259 &[parse_quote!(fixture::One), parse_quote!(fixture::Two)],
1260 &implementation,
1261 )
1262 .expect_err("multi-Capability authoring must have one inherent impl");
1263
1264 assert!(
1265 error
1266 .to_string()
1267 .contains("multiple Capabilities require one inherent impl")
1268 );
1269 }
1270
1271 #[test]
1272 fn duplicate_capabilities_are_rejected() {
1273 let implementation: ItemImpl = parse_quote! { impl ExampleModule {} };
1274 let error = expand_provides(
1275 &[parse_quote!(fixture::One), parse_quote!(fixture::One)],
1276 &implementation,
1277 )
1278 .expect_err("one Capability cannot be contributed twice");
1279
1280 assert!(error.to_string().contains("same Capability more than once"));
1281 }
1282
1283 #[test]
1284 fn capability_paths_must_be_namespace_qualified() {
1285 let implementation: ItemImpl = parse_quote! { impl ExampleModule {} };
1286 let error = expand_provides(&[parse_quote!(One)], &implementation)
1287 .expect_err("generated Capability macros live in their namespace");
1288
1289 assert!(error.to_string().contains("namespace-qualified"));
1290 }
1291}