Skip to main content

lenso_native_adapter_macros/
lib.rs

1//! Derivation macros for statically linked Lenso Plugins and built-in Modules.
2
3use 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, Expr, Fields, GenericArgument, Item, ItemFn, ItemImpl,
11    ItemStruct, LitStr, Path, PathArguments, Token, Type, parse_macro_input,
12    punctuated::Punctuated,
13};
14
15struct ModuleAttributes {
16    descriptor: Option<LitStr>,
17    configuration_schema: Option<LitStr>,
18    configuration_defaults: Option<LitStr>,
19    validate: Option<Path>,
20    prepare: Option<Path>,
21    activate: Option<Path>,
22    deactivate: Option<Path>,
23    lifecycle: bool,
24    consumer: bool,
25}
26
27impl syn::parse::Parse for ModuleAttributes {
28    fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
29        if input.is_empty() {
30            return Ok(Self {
31                descriptor: None,
32                configuration_schema: None,
33                configuration_defaults: None,
34                validate: None,
35                prepare: None,
36                activate: None,
37                deactivate: None,
38                lifecycle: false,
39                consumer: false,
40            });
41        }
42        let mut descriptor = None;
43        let mut configuration_schema = None;
44        let mut configuration_defaults = None;
45        let mut validate = None;
46        let mut prepare = None;
47        let mut activate = None;
48        let mut deactivate = None;
49        let mut lifecycle = false;
50        let mut consumer = false;
51        while !input.is_empty() {
52            let name: syn::Ident = input.parse()?;
53            if name == "lifecycle" {
54                if lifecycle {
55                    return Err(syn::Error::new(name.span(), "duplicate Module attribute"));
56                }
57                lifecycle = true;
58                if input.is_empty() {
59                    break;
60                }
61                input.parse::<Token![,]>()?;
62                continue;
63            }
64            if name == "consumer" {
65                if consumer {
66                    return Err(syn::Error::new(name.span(), "duplicate Module attribute"));
67                }
68                consumer = true;
69                if input.is_empty() {
70                    break;
71                }
72                input.parse::<Token![,]>()?;
73                continue;
74            }
75            input.parse::<Token![=]>()?;
76            match name.to_string().as_str() {
77                "descriptor" if descriptor.is_none() => descriptor = Some(input.parse()?),
78                "configuration_schema" if configuration_schema.is_none() => {
79                    configuration_schema = Some(input.parse()?);
80                }
81                "configuration_defaults" if configuration_defaults.is_none() => {
82                    configuration_defaults = Some(input.parse()?);
83                }
84                "validate" if validate.is_none() => validate = Some(input.parse()?),
85                "prepare" if prepare.is_none() => prepare = Some(input.parse()?),
86                "activate" if activate.is_none() => activate = Some(input.parse()?),
87                "deactivate" if deactivate.is_none() => deactivate = Some(input.parse()?),
88                "descriptor"
89                | "configuration_schema"
90                | "configuration_defaults"
91                | "validate"
92                | "prepare"
93                | "activate"
94                | "deactivate" => {
95                    return Err(syn::Error::new(name.span(), "duplicate Module attribute"));
96                }
97                _ => {
98                    return Err(syn::Error::new(
99                        name.span(),
100                        "expected `descriptor`, `configuration_schema`, `configuration_defaults`, `validate`, `prepare`, `activate`, `deactivate`, `lifecycle`, or `consumer`",
101                    ));
102                }
103            }
104            if input.is_empty() {
105                break;
106            }
107            input.parse::<Token![,]>()?;
108        }
109        Ok(Self {
110            descriptor,
111            configuration_schema,
112            configuration_defaults,
113            validate,
114            prepare,
115            activate,
116            deactivate,
117            lifecycle,
118            consumer,
119        })
120    }
121}
122
123/// Derives native Module source and, for `consumer`, its factory registration.
124///
125/// The package identity comes from `[package.metadata.lenso].package-id` in the
126/// consuming crate's `Cargo.toml`; the package version remains Cargo-owned.
127#[proc_macro_attribute]
128pub fn module(attributes: TokenStream, item: TokenStream) -> TokenStream {
129    expand_authoring_item(attributes, item, "Module")
130}
131
132/// Derives native Plugin source through the same runtime lowering as [`module`].
133///
134/// ```compile_fail
135/// use lenso_native_adapter_macros::plugin;
136///
137/// #[plugin]
138/// enum InvalidPlugin {}
139/// ```
140#[proc_macro_attribute]
141pub fn plugin(attributes: TokenStream, item: TokenStream) -> TokenStream {
142    expand_authoring_item(attributes, item, "Plugin")
143}
144
145fn expand_authoring_item(
146    attributes: TokenStream,
147    item: TokenStream,
148    authoring_unit: &str,
149) -> TokenStream {
150    let attributes = match syn::parse::<ModuleAttributes>(attributes) {
151        Ok(attributes) => attributes,
152        Err(error) => return authoring_error(&error, authoring_unit),
153    };
154    let item = match syn::parse::<Item>(item) {
155        Ok(item) => item,
156        Err(error) => return authoring_error(&error, authoring_unit),
157    };
158    let expanded = match item {
159        Item::Fn(function) => expand_module_function(&attributes, &function),
160        Item::Struct(module) => expand_module_struct(&attributes, module),
161        other => Err(syn::Error::new_spanned(
162            other,
163            format!(
164                "a native {authoring_unit} must be declared by a factory function or a named-field struct"
165            ),
166        )),
167    };
168    match expanded {
169        Ok(tokens) => tokens.into(),
170        Err(error) => authoring_error(&error, authoring_unit),
171    }
172}
173
174fn authoring_error(error: &syn::Error, authoring_unit: &str) -> TokenStream {
175    let message = if authoring_unit == "Plugin" {
176        error.to_string().replace("Module", "Plugin")
177    } else {
178        error.to_string()
179    };
180    syn::Error::new(error.span(), message)
181        .into_compile_error()
182        .into()
183}
184
185/// Derives the locked JSON Schema fragment consumed by a struct-level Module.
186#[proc_macro_derive(ModuleConfig, attributes(lenso, serde))]
187pub fn module_config(item: TokenStream) -> TokenStream {
188    let input = parse_macro_input!(item as DeriveInput);
189    expand_module_config(&input)
190        .unwrap_or_else(syn::Error::into_compile_error)
191        .into()
192}
193
194/// Derives Plugin configuration through the retained Module runtime schema.
195#[proc_macro_derive(PluginConfig, attributes(lenso, serde))]
196pub fn plugin_config(item: TokenStream) -> TokenStream {
197    let input = match syn::parse::<DeriveInput>(item) {
198        Ok(input) => input,
199        Err(error) => return authoring_error(&error, "Plugin"),
200    };
201    match expand_module_config(&input) {
202        Ok(tokens) => tokens.into(),
203        Err(error) => authoring_error(&error, "Plugin"),
204    }
205}
206
207fn expand_module_config(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
208    let Data::Struct(data) = &input.data else {
209        return Err(syn::Error::new_spanned(
210            input,
211            "Module configuration must be a named-field struct",
212        ));
213    };
214    let Fields::Named(fields) = &data.fields else {
215        return Err(syn::Error::new_spanned(
216            &data.fields,
217            "Module configuration must use named fields",
218        ));
219    };
220    let mut properties = Map::new();
221    let mut defaults = Map::new();
222    let mut required = Vec::new();
223    for field in &fields.named {
224        let ident = field.ident.as_ref().expect("named fields have identifiers");
225        let name = serde_field_name(&field.attrs, ident)?;
226        let (schema, optional) = configuration_type_schema(&field.ty)?;
227        if let Some(default) = configuration_field_default(&field.attrs)? {
228            if !configuration_value_matches_schema(&default, &schema) {
229                return Err(syn::Error::new_spanned(
230                    field,
231                    "Module configuration default does not match the field type",
232                ));
233            }
234            defaults.insert(name.clone(), default);
235        }
236        properties.insert(name.clone(), schema);
237        if !optional {
238            required.push(Value::String(name));
239        }
240    }
241    let schema = canonical_json(&json!({
242        "$schema": "https://json-schema.org/draft/2020-12/schema",
243        "type": "object",
244        "additionalProperties": false,
245        "required": required,
246        "properties": properties,
247    }));
248    let defaults = canonical_json(&Value::Object(defaults));
249    let macro_name = format_ident!("__lenso_config_schema_{}", snake(&input.ident.to_string()));
250    let defaults_macro_name = format_ident!(
251        "__lenso_config_defaults_{}",
252        snake(&input.ident.to_string())
253    );
254    Ok(quote! {
255        #[doc(hidden)]
256        #[macro_export]
257        macro_rules! #macro_name {
258            () => { #schema };
259        }
260        #[doc(hidden)]
261        #[macro_export]
262        macro_rules! #defaults_macro_name {
263            () => { #defaults };
264        }
265    })
266}
267
268fn configuration_field_default(attributes: &[Attribute]) -> syn::Result<Option<Value>> {
269    let mut default = None;
270    for attribute in attributes {
271        if !attribute.path().is_ident("lenso") {
272            continue;
273        }
274        attribute.parse_nested_meta(|meta| {
275            if !meta.path.is_ident("default") {
276                return Err(meta.error("expected `default = <JSON literal>`"));
277            }
278            if default.is_some() {
279                return Err(meta.error("duplicate Module configuration default"));
280            }
281            let expression = meta.value()?.parse::<Expr>()?;
282            let encoded = quote!(#expression).to_string();
283            default = Some(serde_json::from_str(&encoded).map_err(|error| {
284                meta.error(format!(
285                    "Module configuration default must be a JSON literal: {error}"
286                ))
287            })?);
288            Ok(())
289        })?;
290    }
291    Ok(default)
292}
293
294fn configuration_value_matches_schema(value: &Value, schema: &Value) -> bool {
295    match schema.get("type").and_then(Value::as_str) {
296        Some("array") => value.as_array().is_some_and(|items| {
297            schema.get("items").is_some_and(|schema| {
298                items
299                    .iter()
300                    .all(|item| configuration_value_matches_schema(item, schema))
301            })
302        }),
303        Some("boolean") => value.is_boolean(),
304        Some("integer") => value
305            .as_number()
306            .is_some_and(|number| number.is_i64() || number.is_u64()),
307        Some("number") => value.is_number(),
308        Some("string") => value.is_string(),
309        _ => false,
310    }
311}
312
313fn serde_field_name(attributes: &[Attribute], ident: &syn::Ident) -> syn::Result<String> {
314    let mut name = ident.to_string();
315    for attribute in attributes {
316        if !attribute.path().is_ident("serde") {
317            continue;
318        }
319        attribute.parse_nested_meta(|meta| {
320            if meta.path.is_ident("rename") {
321                name = meta.value()?.parse::<LitStr>()?.value();
322            }
323            Ok(())
324        })?;
325    }
326    Ok(name)
327}
328
329fn configuration_type_schema(ty: &Type) -> syn::Result<(Value, bool)> {
330    let Type::Path(path) = ty else {
331        return Err(syn::Error::new_spanned(
332            ty,
333            "Module configuration fields must use portable named types",
334        ));
335    };
336    let segment = path.path.segments.last().expect("type paths are non-empty");
337    let name = segment.ident.to_string();
338    if name == "Option" {
339        return Ok((configuration_inner_schema(segment, ty)?, true));
340    }
341    if name == "Vec" {
342        return Ok((
343            json!({"type": "array", "items": configuration_inner_schema(segment, ty)?}),
344            false,
345        ));
346    }
347    let schema = match name.as_str() {
348        "String" => json!({"type": "string"}),
349        "bool" => json!({"type": "boolean"}),
350        "f32" | "f64" => json!({"type": "number"}),
351        "i8" | "i16" | "i32" | "i64" | "i128" | "isize" | "u8" | "u16" | "u32" | "u64" | "u128"
352        | "usize" => json!({"type": "integer"}),
353        _ => {
354            return Err(syn::Error::new_spanned(
355                ty,
356                "unsupported Module configuration field type; use String, bool, a number, Option<T>, or Vec<T>",
357            ));
358        }
359    };
360    Ok((schema, false))
361}
362
363fn configuration_inner_schema(segment: &syn::PathSegment, ty: &Type) -> syn::Result<Value> {
364    let PathArguments::AngleBracketed(arguments) = &segment.arguments else {
365        return Err(syn::Error::new_spanned(
366            ty,
367            "configuration container requires one type",
368        ));
369    };
370    let [GenericArgument::Type(inner)] = arguments.args.iter().collect::<Vec<_>>().as_slice()
371    else {
372        return Err(syn::Error::new_spanned(
373            ty,
374            "configuration container requires one type",
375        ));
376    };
377    configuration_type_schema(inner).map(|(schema, _)| schema)
378}
379
380fn expand_module_function(
381    attributes: &ModuleAttributes,
382    function: &ItemFn,
383) -> syn::Result<proc_macro2::TokenStream> {
384    let sdk = authoring_crate();
385    if attributes.descriptor.is_none()
386        && (attributes.configuration_schema.is_some()
387            || attributes.configuration_defaults.is_some())
388    {
389        return Err(syn::Error::new_spanned(
390            function,
391            "`configuration_schema` and `configuration_defaults` require `descriptor` on a factory function",
392        ));
393    }
394    if attributes.validate.is_some()
395        || attributes.prepare.is_some()
396        || attributes.activate.is_some()
397        || attributes.deactivate.is_some()
398        || attributes.lifecycle
399        || attributes.consumer
400    {
401        return Err(syn::Error::new_spanned(
402            function,
403            "struct-level Module attributes are unavailable on factory functions",
404        ));
405    }
406    let package_id = package_id()?;
407    let descriptor_json = attributes
408        .descriptor
409        .as_ref()
410        .map(|descriptor| {
411            module_descriptor(
412                &package_id,
413                descriptor,
414                attributes.configuration_schema.as_ref(),
415                attributes.configuration_defaults.as_ref(),
416            )
417        })
418        .transpose()?;
419    let function_name = &function.sig.ident;
420    let generated_module = format_ident!("__lenso_module_{function_name}");
421    let descriptor_constant = descriptor_json.map(|descriptor| {
422        let artifact =
423            format!("LENSO_MODULE_DESCRIPTOR_V1\0{descriptor}\0END_LENSO_MODULE_DESCRIPTOR_V1");
424        let artifact_length = artifact.len();
425        let artifact = proc_macro2::Literal::byte_string(artifact.as_bytes());
426        let package_file_tracking = package_file_tracking([
427            attributes.configuration_schema.as_ref(),
428            attributes.configuration_defaults.as_ref(),
429        ]);
430        quote! {
431            /// Generated package-owned Module Descriptor bytes.
432            pub const MODULE_DESCRIPTOR_JSON: &str = #descriptor;
433            /// Linker-retained descriptor artifact consumed without executing package code.
434            #[doc(hidden)]
435            #[used]
436            pub static __LENSO_MODULE_DESCRIPTOR_ARTIFACT: [u8; #artifact_length] = *#artifact;
437            #(#package_file_tracking)*
438        }
439    });
440
441    Ok(quote! {
442        /// Runtime package identity derived from Cargo package metadata.
443        pub const PACKAGE_ID: &str = #package_id;
444        /// Exact linked Cargo package version.
445        pub const PACKAGE_VERSION: &str = env!("CARGO_PKG_VERSION");
446        /// Exact Host Build identity for this linked package.
447        pub const FACTORY_IDENTITY: &str = concat!(#package_id, "@", env!("CARGO_PKG_VERSION"));
448        #descriptor_constant
449
450        #function
451
452        #[doc(hidden)]
453        mod #generated_module {
454            #[derive(Clone, Copy, Debug, Default)]
455            struct Factory;
456
457            impl #sdk::__private::NativeModuleFactory for Factory {
458                fn package_id(&self) -> &'static str {
459                    #package_id
460                }
461
462                fn package_version(&self) -> &'static str {
463                    env!("CARGO_PKG_VERSION")
464                }
465
466                fn instantiate(
467                    &self,
468                    context: #sdk::__private::NativeModuleFactoryContext<'_>,
469                ) -> Result<
470                    #sdk::__private::NativeModuleInstance,
471                    #sdk::__private::RuntimeFailure,
472                > {
473                    super::#function_name(context)
474                }
475            }
476
477            fn factory() -> std::rc::Rc<dyn #sdk::__private::NativeModuleFactory> {
478                std::rc::Rc::new(Factory)
479            }
480
481            #sdk::__private::__inventory::submit! {
482                #sdk::__private::LinkedNativeModuleFactory::new(factory)
483            }
484
485            // Make Cargo track the manifest that supplied the generated identity.
486            const _: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/Cargo.toml"));
487        }
488    })
489}
490
491/// Derives one or more provided Capability endpoints, one native factory, and static registration.
492///
493/// Apply this to one inherent implementation containing the Capabilities' domain methods
494/// for a struct already annotated with [`module`]. Generated bindings lower the
495/// methods into the Adapter-facing Provider trait. Existing explicit Provider
496/// trait implementations remain supported as a single-Capability compatibility
497/// escape hatch. Multi-Capability Modules must use one inherent implementation.
498#[proc_macro_attribute]
499pub fn provides(attributes: TokenStream, item: TokenStream) -> TokenStream {
500    let capabilities =
501        parse_macro_input!(attributes with Punctuated::<Path, Token![,]>::parse_terminated);
502    let implementation = parse_macro_input!(item as ItemImpl);
503    expand_provides(
504        &capabilities.into_iter().collect::<Vec<_>>(),
505        &implementation,
506    )
507    .unwrap_or_else(syn::Error::into_compile_error)
508    .into()
509}
510
511struct CapabilityContribution {
512    namespace: Path,
513    descriptor: syn::Ident,
514    endpoints: syn::Ident,
515    lower: syn::Ident,
516}
517
518fn capability_contributions(capabilities: &[Path]) -> syn::Result<Vec<CapabilityContribution>> {
519    let mut seen = BTreeSet::new();
520    capabilities
521        .iter()
522        .map(|capability| {
523            let path = quote!(#capability).to_string();
524            if !seen.insert(path) {
525                return Err(syn::Error::new_spanned(
526                    capability,
527                    "a Module cannot provide the same Capability more than once",
528                ));
529            }
530            let mut namespace = capability.clone();
531            let capability_ident = namespace
532                .segments
533                .pop()
534                .ok_or_else(|| syn::Error::new_spanned(capability, "Capability path is empty"))?
535                .into_value()
536                .ident;
537            namespace.segments.pop_punct();
538            if namespace.segments.is_empty() {
539                return Err(syn::Error::new_spanned(
540                    capability,
541                    "Capability must be namespace-qualified, for example `agent::Agent`",
542                ));
543            }
544            let capability_snake = snake(&capability_ident.to_string());
545            Ok(CapabilityContribution {
546                namespace,
547                descriptor: format_ident!("__lenso_provided_{capability_snake}"),
548                endpoints: format_ident!("__lenso_native_endpoints_{capability_snake}"),
549                lower: format_ident!("__lenso_native_lower_{capability_snake}"),
550            })
551        })
552        .collect()
553}
554
555fn provided_module(
556    capabilities: &[Path],
557    implementation: &ItemImpl,
558) -> syn::Result<(syn::Ident, bool)> {
559    if capabilities.is_empty() {
560        return Err(syn::Error::new_spanned(
561            implementation,
562            "`provides` requires at least one namespace-qualified Capability",
563        ));
564    }
565    if capabilities.len() > 1 && implementation.trait_.is_some() {
566        return Err(syn::Error::new_spanned(
567            implementation,
568            "multiple Capabilities require one inherent impl containing their domain methods",
569        ));
570    }
571    let Type::Path(module_type) = implementation.self_ty.as_ref() else {
572        return Err(syn::Error::new_spanned(
573            &implementation.self_ty,
574            "the Module provider type must be a path",
575        ));
576    };
577    let module_ident = module_type
578        .path
579        .segments
580        .last()
581        .ok_or_else(|| {
582            syn::Error::new_spanned(&module_type.path, "the Module provider type is empty")
583        })?
584        .ident
585        .clone();
586    Ok((module_ident, implementation.trait_.is_none()))
587}
588
589fn expand_provides(
590    capabilities: &[Path],
591    implementation: &ItemImpl,
592) -> syn::Result<proc_macro2::TokenStream> {
593    let sdk = authoring_crate();
594    let (module_ident, lowers_domain_methods) = provided_module(capabilities, implementation)?;
595    let contributions = capability_contributions(capabilities)?;
596    let provided_descriptors = contributions
597        .iter()
598        .map(|contribution| {
599            let namespace = &contribution.namespace;
600            let descriptor = &contribution.descriptor;
601            quote!(#namespace::#descriptor!())
602        })
603        .collect::<Vec<_>>();
604    let module_descriptor = format_ident!(
605        "__lenso_module_descriptor_{}",
606        snake(&module_ident.to_string())
607    );
608    let generated_module = format_ident!("__lenso_provider_{}", snake(&module_ident.to_string()));
609    let lifecycle = format_ident!("__LensoLifecycle{module_ident}");
610    let artifact = format_ident!("__LENSO_MODULE_DESCRIPTOR_ARTIFACT_{module_ident}");
611    let provider_implementations = if lowers_domain_methods {
612        contributions
613            .iter()
614            .map(|contribution| {
615                let namespace = &contribution.namespace;
616                let lower = &contribution.lower;
617                quote! { #namespace::#lower!(#module_ident, #sdk::__private); }
618            })
619            .collect::<Vec<_>>()
620    } else {
621        Vec::new()
622    };
623    let endpoint_contributions = contributions.iter().map(|contribution| {
624        let namespace = &contribution.namespace;
625        let endpoints = &contribution.endpoints;
626        quote! {
627            let (provided_requests, provided_streams, provided_events) =
628                super::#namespace::#endpoints!(module.clone(), #sdk::__private);
629            request_endpoints.extend(provided_requests);
630            stream_endpoints.extend(provided_streams);
631            event_endpoints.extend(provided_events);
632        }
633    });
634
635    let mut implementation = implementation.clone();
636    implementation
637        .attrs
638        .push(syn::parse_quote!(#[allow(clippy::unused_async, clippy::unused_async_trait_impl)]));
639
640    Ok(quote! {
641        #implementation
642        #(#provider_implementations)*
643
644        /// Generated package-owned Module Descriptor bytes.
645        pub const MODULE_DESCRIPTOR_JSON: &str = #module_descriptor!(
646            #(#provided_descriptors),*
647        );
648        #[doc(hidden)]
649        const __LENSO_MODULE_DESCRIPTOR_ARTIFACT_TEXT: &str = concat!(
650            "LENSO_MODULE_DESCRIPTOR_V1\0",
651            #module_descriptor!(#(#provided_descriptors),*),
652            "\0END_LENSO_MODULE_DESCRIPTOR_V1",
653        );
654        /// Linker-retained descriptor artifact consumed without executing package code.
655        #[doc(hidden)]
656        #[used]
657        pub static #artifact: &[u8] = __LENSO_MODULE_DESCRIPTOR_ARTIFACT_TEXT.as_bytes();
658
659        #[doc(hidden)]
660        mod #generated_module {
661            #[derive(Clone, Copy, Debug, Default)]
662            struct Factory;
663
664            impl #sdk::__private::NativeModuleFactory for Factory {
665                fn package_id(&self) -> &'static str { super::PACKAGE_ID }
666                fn package_version(&self) -> &'static str { super::PACKAGE_VERSION }
667
668                fn instantiate(
669                    &self,
670                    context: #sdk::__private::NativeModuleFactoryContext<'_>,
671                ) -> Result<
672                    #sdk::__private::NativeModuleInstance,
673                    #sdk::__private::RuntimeFailure,
674                > {
675                    let module = super::#module_ident::__lenso_construct(context)?;
676                    let lifecycle = super::#lifecycle { module: module.clone() };
677                    let mut request_endpoints = Vec::new();
678                    let mut stream_endpoints = Vec::new();
679                    let mut event_endpoints = Vec::new();
680                    #(#endpoint_contributions)*
681                    Ok(#sdk::__private::NativeModuleInstance::with_all_endpoints(
682                        request_endpoints,
683                        stream_endpoints,
684                        event_endpoints,
685                        lifecycle,
686                    ))
687                }
688            }
689
690            fn factory() -> ::std::rc::Rc<dyn #sdk::__private::NativeModuleFactory> {
691                ::std::rc::Rc::new(Factory)
692            }
693
694            #sdk::__private::__inventory::submit! {
695                #sdk::__private::LinkedNativeModuleFactory::new(factory)
696            }
697        }
698    })
699}
700
701#[allow(clippy::too_many_lines)]
702fn expand_module_struct(
703    attributes: &ModuleAttributes,
704    mut module: ItemStruct,
705) -> syn::Result<proc_macro2::TokenStream> {
706    let sdk = authoring_crate();
707    if attributes.descriptor.is_some() {
708        return Err(syn::Error::new_spanned(
709            &module.ident,
710            "struct-level Modules derive their Descriptor; remove `descriptor`",
711        ));
712    }
713    let package_id = package_id()?;
714    let package_version = env::var("CARGO_PKG_VERSION").map_err(|_| {
715        syn::Error::new_spanned(
716            &module.ident,
717            "CARGO_PKG_VERSION is unavailable while deriving Module Descriptor",
718        )
719    })?;
720    let StructFields {
721        config_type,
722        ports,
723        tasks,
724        initializers,
725    } = analyze_struct_fields(&mut module)?;
726    let schema = configuration_schema_tokens(
727        attributes.configuration_schema.as_ref(),
728        config_type.as_ref(),
729    )?;
730    let configuration_defaults = configuration_defaults_tokens(
731        attributes.configuration_schema.as_ref(),
732        attributes.configuration_defaults.as_ref(),
733        config_type.as_ref(),
734    )?;
735    let name = &module.ident;
736    let lifecycle_name = format_ident!("__LensoLifecycle{name}");
737    let descriptor_macro = format_ident!("__lenso_module_descriptor_{}", snake(&name.to_string()));
738    let requirement_macros = ports
739        .iter()
740        .map(|(_, client, cardinality)| requirement_macro(client, *cardinality))
741        .collect::<syn::Result<Vec<_>>>()?;
742    let connect_ports = ports.iter().map(|(field, _, _)| {
743        quote! { self.module.#field.connect(context.dependencies())?; }
744    });
745    let connect_tasks = task_connectors(&tasks);
746    let requirement_parts = intersperse_commas(requirement_macros);
747    let (prefix, after_schema, suffix, defaults) =
748        descriptor_affixes(&package_id, &package_version);
749    let construct_configuration = if let Some(config_type) = &config_type {
750        let validate = attributes
751            .validate
752            .as_ref()
753            .map(|path| quote!(#path(&configuration)?;));
754        quote! {
755            let configuration = #sdk::__private::serde_json::from_str::<#config_type>(context.configuration())
756                .map_err(|error| #sdk::__private::RuntimeFailure::InvalidResolvedPlan {
757                    detail: format!("invalid {} configuration: {error}", #package_id),
758                })?;
759            #validate
760        }
761    } else {
762        if attributes.configuration_schema.is_some() {
763            return Err(syn::Error::new_spanned(
764                &module.ident,
765                "`configuration_schema` requires a `#[config]` field",
766            ));
767        }
768        if attributes.validate.is_some() {
769            return Err(syn::Error::new_spanned(
770                &module.ident,
771                "`validate` requires a `#[config]` field",
772            ));
773        }
774        if attributes.configuration_defaults.is_some() {
775            return Err(syn::Error::new_spanned(
776                &module.ident,
777                "`configuration_defaults` requires a `#[config]` field",
778            ));
779        }
780        quote! {
781            let configuration = #sdk::__private::serde_json::from_str::<#sdk::__private::serde_json::Value>(context.configuration())
782                .map_err(|error| #sdk::__private::RuntimeFailure::InvalidResolvedPlan {
783                    detail: format!("invalid {} configuration: {error}", #package_id),
784                })?;
785            if !configuration.as_object().is_some_and(|object| object.is_empty()) {
786                return Err(#sdk::__private::RuntimeFailure::InvalidResolvedPlan {
787                    detail: format!("{} does not accept configuration", #package_id),
788                });
789            }
790        }
791    };
792    if attributes.lifecycle
793        && (attributes.prepare.is_some()
794            || attributes.activate.is_some()
795            || attributes.deactivate.is_some())
796    {
797        return Err(syn::Error::new_spanned(
798            &module.ident,
799            "`lifecycle` replaces the `prepare`, `activate`, and `deactivate` function attributes",
800        ));
801    }
802    let prepare = if attributes.lifecycle {
803        quote! {
804            let module = self.module.clone();
805            Box::pin(async move { #sdk::Lifecycle::prepare(&module, context).await })
806        }
807    } else {
808        hook(attributes.prepare.as_ref(), &sdk)
809    };
810    let activate_hook = if attributes.lifecycle {
811        quote! {
812            let module = self.module.clone();
813            Box::pin(async move { #sdk::Lifecycle::activate(&module, context).await })
814        }
815    } else {
816        hook(attributes.activate.as_ref(), &sdk)
817    };
818    let deactivate_hook = if attributes.lifecycle {
819        quote! {
820            let module = self.module.clone();
821            Box::pin(async move { #sdk::Lifecycle::deactivate(&module, context).await })
822        }
823    } else {
824        hook(attributes.deactivate.as_ref(), &sdk)
825    };
826    let disconnect_tasks = task_disconnectors(&tasks);
827    let activate = if tasks.is_empty() {
828        activate_hook
829    } else {
830        quote! {
831            let module = self.module.clone();
832            let activation = { #activate_hook };
833            Box::pin(async move {
834                let result = activation.await;
835                if result.is_err() {
836                    #(#disconnect_tasks)*
837                }
838                result
839            })
840        }
841    };
842    let disconnect_tasks = task_disconnectors(&tasks);
843    let deactivate = if tasks.is_empty() {
844        deactivate_hook
845    } else {
846        quote! {
847            let module = self.module.clone();
848            #(#disconnect_tasks)*
849            let deactivation = { #deactivate_hook };
850            deactivation
851        }
852    };
853    let package_file_tracking = package_file_tracking([
854        attributes.configuration_schema.as_ref(),
855        attributes.configuration_defaults.as_ref(),
856    ]);
857    let consumer_finalizer = if attributes.consumer {
858        let generated_module = format_ident!("__lenso_consumer_{}", snake(&name.to_string()));
859        let artifact = format_ident!("__LENSO_MODULE_DESCRIPTOR_ARTIFACT_{name}");
860        Some(quote! {
861            /// Generated package-owned Module Descriptor bytes.
862            pub const MODULE_DESCRIPTOR_JSON: &str = #descriptor_macro!();
863            #[doc(hidden)]
864            const __LENSO_MODULE_DESCRIPTOR_ARTIFACT_TEXT: &str = concat!(
865                "LENSO_MODULE_DESCRIPTOR_V1\0",
866                #descriptor_macro!(),
867                "\0END_LENSO_MODULE_DESCRIPTOR_V1",
868            );
869            /// Linker-retained descriptor artifact consumed without executing package code.
870            #[doc(hidden)]
871            #[used]
872            pub static #artifact: &[u8] = __LENSO_MODULE_DESCRIPTOR_ARTIFACT_TEXT.as_bytes();
873
874            #[doc(hidden)]
875            mod #generated_module {
876                #[derive(Clone, Copy, Debug, Default)]
877                struct Factory;
878
879                impl #sdk::__private::NativeModuleFactory for Factory {
880                    fn package_id(&self) -> &'static str { super::PACKAGE_ID }
881                    fn package_version(&self) -> &'static str { super::PACKAGE_VERSION }
882
883                    fn instantiate(
884                        &self,
885                        context: #sdk::__private::NativeModuleFactoryContext<'_>,
886                    ) -> Result<
887                        #sdk::__private::NativeModuleInstance,
888                        #sdk::__private::RuntimeFailure,
889                    > {
890                        let module = super::#name::__lenso_construct(context)?;
891                        let lifecycle = super::#lifecycle_name { module };
892                        Ok(#sdk::__private::NativeModuleInstance::with_lifecycle(
893                            Vec::new(),
894                            lifecycle,
895                        ))
896                    }
897                }
898
899                fn factory() -> ::std::rc::Rc<dyn #sdk::__private::NativeModuleFactory> {
900                    ::std::rc::Rc::new(Factory)
901                }
902
903                #sdk::__private::__inventory::submit! {
904                    #sdk::__private::LinkedNativeModuleFactory::new(factory)
905                }
906            }
907        })
908    } else {
909        None
910    };
911
912    Ok(quote! {
913        /// Runtime package identity derived from Cargo package metadata.
914        pub const PACKAGE_ID: &str = #package_id;
915        /// Exact linked Cargo package version.
916        pub const PACKAGE_VERSION: &str = env!("CARGO_PKG_VERSION");
917        /// Exact Host Build identity for this linked package.
918        pub const FACTORY_IDENTITY: &str = concat!(#package_id, "@", env!("CARGO_PKG_VERSION"));
919
920        #module
921
922        #[doc(hidden)]
923        macro_rules! #descriptor_macro {
924            () => {
925                concat!(#prefix, #schema, ",\"configuration_defaults\":", #configuration_defaults, #after_schema, #suffix #(, #requirement_parts)*, #defaults)
926            };
927            ($first:expr $(, $rest:expr)*) => {
928                concat!(#prefix, #schema, ",\"configuration_defaults\":", #configuration_defaults, #after_schema, $first $(, ",", $rest)*, #suffix #(, #requirement_parts)*, #defaults)
929            };
930        }
931
932        impl #name {
933            #[doc(hidden)]
934            fn __lenso_construct(
935                context: #sdk::__private::NativeModuleFactoryContext<'_>,
936            ) -> Result<Self, #sdk::__private::RuntimeFailure> {
937                if context.entrypoint() != "default" {
938                    return Err(#sdk::__private::RuntimeFailure::InvalidResolvedPlan {
939                        detail: format!("unsupported {} entrypoint {}", #package_id, context.entrypoint()),
940                    });
941                }
942                #construct_configuration
943                Ok(Self { #(#initializers),* })
944            }
945        }
946
947        #[doc(hidden)]
948        #[derive(Clone, Debug)]
949        struct #lifecycle_name {
950            module: #name,
951        }
952
953        impl #sdk::__private::ModuleLifecycle for #lifecycle_name {
954            fn prepare(&self, context: #sdk::__private::PrepareContext) -> #sdk::__private::ModuleFuture {
955                #prepare
956            }
957
958            fn activate(&self, context: #sdk::__private::ActivateContext) -> #sdk::__private::ModuleFuture {
959                let connected = (|| -> Result<(), #sdk::__private::RuntimeFailure> {
960                    #(#connect_ports)*
961                    #(#connect_tasks)*
962                    Ok(())
963                })();
964                if let Err(error) = connected {
965                    return Box::pin(#sdk::__private::futures::future::ready(Err(error)));
966                }
967                #activate
968            }
969
970            fn deactivate(&self, context: #sdk::__private::DeactivateContext) -> #sdk::__private::ModuleFuture {
971                #deactivate
972            }
973        }
974
975        const _: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/Cargo.toml"));
976        #(#package_file_tracking)*
977
978        #consumer_finalizer
979    })
980}
981
982fn descriptor_affixes(
983    package_id: &str,
984    package_version: &str,
985) -> (String, &'static str, &'static str, &'static str) {
986    let prefix = format!(
987        "{{\"package_id\":{},\"package_revision\":{},\"entrypoint\":\"default\",\"configuration_schema\":",
988        serde_json::to_string(package_id).expect("package ID serializes"),
989        serde_json::to_string(package_version).expect("package version serializes"),
990    );
991    let after_schema = ",\"provided_capabilities\":[";
992    let suffix = "],\"required_capabilities\":[";
993    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\"}";
994    (prefix, after_schema, suffix, defaults)
995}
996
997fn package_file_tracking<'a>(
998    paths: impl IntoIterator<Item = Option<&'a LitStr>>,
999) -> Vec<proc_macro2::TokenStream> {
1000    paths
1001        .into_iter()
1002        .flatten()
1003        .map(|path| {
1004            quote!(
1005                const _: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/", #path));
1006            )
1007        })
1008        .collect()
1009}
1010
1011fn configuration_schema_tokens(
1012    schema_path: Option<&LitStr>,
1013    config_type: Option<&Type>,
1014) -> syn::Result<proc_macro2::TokenStream> {
1015    if let Some(path) = schema_path {
1016        let schema = canonical_json(&read_configuration_schema(path)?);
1017        return Ok(quote!(#schema));
1018    }
1019    let Some(config_type) = config_type else {
1020        let schema = canonical_json(&json!({
1021            "$schema": "https://json-schema.org/draft/2020-12/schema",
1022            "type": "object",
1023            "additionalProperties": false,
1024            "required": [],
1025            "properties": {},
1026        }));
1027        return Ok(quote!(#schema));
1028    };
1029    let Type::Path(config) = config_type else {
1030        return Err(syn::Error::new_spanned(
1031            config_type,
1032            "the `#[config]` field type must be a path",
1033        ));
1034    };
1035    let mut namespace = config.path.clone();
1036    let config_name = namespace
1037        .segments
1038        .pop()
1039        .expect("type paths are non-empty")
1040        .into_value()
1041        .ident;
1042    namespace.segments.pop_punct();
1043    let macro_name = format_ident!("__lenso_config_schema_{}", snake(&config_name.to_string()));
1044    if namespace.segments.is_empty() {
1045        Ok(quote!(#macro_name!()))
1046    } else {
1047        Ok(quote!(#namespace::#macro_name!()))
1048    }
1049}
1050
1051fn configuration_defaults_tokens(
1052    schema_path: Option<&LitStr>,
1053    defaults_path: Option<&LitStr>,
1054    config_type: Option<&Type>,
1055) -> syn::Result<proc_macro2::TokenStream> {
1056    if let Some(path) = defaults_path {
1057        if schema_path.is_none() {
1058            return Err(syn::Error::new(
1059                path.span(),
1060                "`configuration_defaults` requires an explicit `configuration_schema`",
1061            ));
1062        }
1063        let defaults = read_configuration_defaults(path)?;
1064        let schema = read_configuration_schema(schema_path.expect("checked above"))?;
1065        validate_configuration_defaults(&defaults, &schema).map_err(|detail| {
1066            syn::Error::new(
1067                path.span(),
1068                format!("invalid package configuration defaults: {detail}"),
1069            )
1070        })?;
1071        let defaults = canonical_json(&defaults);
1072        return Ok(quote!(#defaults));
1073    }
1074    if schema_path.is_some() || config_type.is_none() {
1075        let defaults = canonical_json(&json!({}));
1076        return Ok(quote!(#defaults));
1077    }
1078    let config_type = config_type.expect("checked above");
1079    let Type::Path(config) = config_type else {
1080        return Err(syn::Error::new_spanned(
1081            config_type,
1082            "the `#[config]` field type must be a path",
1083        ));
1084    };
1085    let mut namespace = config.path.clone();
1086    let config_name = namespace
1087        .segments
1088        .pop()
1089        .expect("type paths are non-empty")
1090        .into_value()
1091        .ident;
1092    namespace.segments.pop_punct();
1093    let macro_name = format_ident!(
1094        "__lenso_config_defaults_{}",
1095        snake(&config_name.to_string())
1096    );
1097    if namespace.segments.is_empty() {
1098        Ok(quote!(#macro_name!()))
1099    } else {
1100        Ok(quote!(#namespace::#macro_name!()))
1101    }
1102}
1103
1104struct StructFields {
1105    config_type: Option<Type>,
1106    ports: Vec<(syn::Ident, Path, PortCardinality)>,
1107    tasks: Vec<syn::Ident>,
1108    initializers: Vec<proc_macro2::TokenStream>,
1109}
1110
1111#[derive(Clone, Copy)]
1112enum PortCardinality {
1113    One,
1114    Many,
1115}
1116
1117fn analyze_struct_fields(module: &mut ItemStruct) -> syn::Result<StructFields> {
1118    let Fields::Named(fields) = &mut module.fields else {
1119        return Err(syn::Error::new_spanned(
1120            &module.fields,
1121            "a struct-level Module requires named fields",
1122        ));
1123    };
1124    let mut config = None;
1125    let mut ports = Vec::new();
1126    let mut tasks = Vec::new();
1127    let mut initializers = Vec::new();
1128    for field in &mut fields.named {
1129        let name = field.ident.as_ref().expect("named fields have identifiers");
1130        let is_config = take_marker(&mut field.attrs, "config");
1131        let is_tasks = take_marker(&mut field.attrs, "tasks");
1132        if is_config && is_tasks {
1133            return Err(syn::Error::new_spanned(
1134                field,
1135                "a Module field cannot be both `#[config]` and `#[tasks]`",
1136            ));
1137        }
1138        if is_config {
1139            if config.replace(field.ty.clone()).is_some() {
1140                return Err(syn::Error::new_spanned(
1141                    field,
1142                    "a Module has exactly one `#[config]` field",
1143                ));
1144            }
1145            initializers.push(quote!(#name: configuration));
1146        } else if is_tasks {
1147            if !is_named_type(&field.ty, "ManagedTasks") {
1148                return Err(syn::Error::new_spanned(
1149                    &field.ty,
1150                    "a `#[tasks]` field must have type `ManagedTasks`",
1151                ));
1152            }
1153            if !tasks.is_empty() {
1154                return Err(syn::Error::new_spanned(
1155                    field,
1156                    "a Module has at most one `#[tasks]` field",
1157                ));
1158            }
1159            tasks.push(name.clone());
1160            initializers.push(quote!(#name: ::core::default::Default::default()));
1161        } else if let Some((client, cardinality)) = port_client(&field.ty)? {
1162            ports.push((name.clone(), client, cardinality));
1163            initializers.push(quote!(#name: ::core::default::Default::default()));
1164        } else {
1165            initializers.push(quote!(#name: ::core::default::Default::default()));
1166        }
1167    }
1168    Ok(StructFields {
1169        config_type: config,
1170        ports,
1171        tasks,
1172        initializers,
1173    })
1174}
1175
1176fn is_named_type(ty: &Type, expected: &str) -> bool {
1177    let Type::Path(path) = ty else {
1178        return false;
1179    };
1180    path.path
1181        .segments
1182        .last()
1183        .is_some_and(|segment| segment.ident == expected && segment.arguments.is_empty())
1184}
1185
1186fn take_marker(attributes: &mut Vec<Attribute>, name: &str) -> bool {
1187    let present = attributes
1188        .iter()
1189        .any(|attribute| attribute.path().is_ident(name));
1190    attributes.retain(|attribute| !attribute.path().is_ident(name));
1191    present
1192}
1193
1194fn task_connectors(tasks: &[syn::Ident]) -> Vec<proc_macro2::TokenStream> {
1195    tasks
1196        .iter()
1197        .map(|field| {
1198            quote! { self.module.#field.__lenso_connect(context.tasks().clone())?; }
1199        })
1200        .collect()
1201}
1202
1203fn task_disconnectors(tasks: &[syn::Ident]) -> Vec<proc_macro2::TokenStream> {
1204    tasks
1205        .iter()
1206        .map(|field| quote! { module.#field.__lenso_disconnect(); })
1207        .collect()
1208}
1209
1210fn port_client(ty: &Type) -> syn::Result<Option<(Path, PortCardinality)>> {
1211    let Type::Path(path) = ty else {
1212        return Ok(None);
1213    };
1214    let Some(segment) = path.path.segments.last() else {
1215        return Ok(None);
1216    };
1217    let cardinality = if segment.ident == "Port" {
1218        PortCardinality::One
1219    } else if segment.ident == "ManyPort" {
1220        PortCardinality::Many
1221    } else {
1222        return Ok(None);
1223    };
1224    let syn::PathArguments::AngleBracketed(arguments) = &segment.arguments else {
1225        return Err(syn::Error::new_spanned(
1226            ty,
1227            "Port or ManyPort requires one Capability client type",
1228        ));
1229    };
1230    let Some(syn::GenericArgument::Type(Type::Path(client))) = arguments.args.first() else {
1231        return Err(syn::Error::new_spanned(
1232            ty,
1233            "Port or ManyPort requires one Capability client type",
1234        ));
1235    };
1236    if arguments.args.len() != 1 {
1237        return Err(syn::Error::new_spanned(
1238            ty,
1239            "Port or ManyPort requires one Capability client type",
1240        ));
1241    }
1242    Ok(Some((client.path.clone(), cardinality)))
1243}
1244
1245fn requirement_macro(
1246    client: &Path,
1247    cardinality: PortCardinality,
1248) -> syn::Result<proc_macro2::TokenStream> {
1249    if client.segments.len() < 2 {
1250        return Err(syn::Error::new_spanned(
1251            client,
1252            "a Port client must be namespace-qualified, for example `model::ModelClient`",
1253        ));
1254    }
1255    let mut namespace = client.clone();
1256    let client_name = namespace
1257        .segments
1258        .pop()
1259        .expect("checked length")
1260        .into_value()
1261        .ident;
1262    namespace.segments.pop_punct();
1263    let prefix = match cardinality {
1264        PortCardinality::One => "__lenso_required_",
1265        PortCardinality::Many => "__lenso_required_many_",
1266    };
1267    let macro_name = format_ident!("{}{}", prefix, snake(&client_name.to_string()));
1268    Ok(quote!(#namespace::#macro_name!()))
1269}
1270
1271fn intersperse_commas(values: Vec<proc_macro2::TokenStream>) -> Vec<proc_macro2::TokenStream> {
1272    values
1273        .into_iter()
1274        .enumerate()
1275        .flat_map(|(index, value)| {
1276            if index == 0 {
1277                vec![value]
1278            } else {
1279                vec![quote!(","), value]
1280            }
1281        })
1282        .collect()
1283}
1284
1285fn hook(path: Option<&Path>, sdk: &proc_macro2::TokenStream) -> proc_macro2::TokenStream {
1286    path.map_or_else(
1287        || quote!(Box::pin(#sdk::__private::futures::future::ready(Ok(())))),
1288        |path| quote!(#path(&self.module, &context)),
1289    )
1290}
1291
1292fn canonical_json(value: &Value) -> String {
1293    serde_json::to_string(value).expect("JSON values serialize")
1294}
1295
1296fn authoring_crate() -> proc_macro2::TokenStream {
1297    for package in ["lenso", "lenso-native-adapter"] {
1298        match crate_name(package) {
1299            Ok(FoundCrate::Itself) => {
1300                let ident = format_ident!("{}", package.replace('-', "_"));
1301                return quote!(::#ident);
1302            }
1303            Ok(FoundCrate::Name(name)) => {
1304                let ident = format_ident!("{name}");
1305                return quote!(::#ident);
1306            }
1307            Err(_) => {}
1308        }
1309    }
1310    quote!(::lenso_native_adapter)
1311}
1312
1313fn snake(value: &str) -> String {
1314    let mut output = String::new();
1315    for (index, character) in value.chars().enumerate() {
1316        if character.is_ascii_uppercase() && index > 0 {
1317            output.push('_');
1318        }
1319        output.push(character.to_ascii_lowercase());
1320    }
1321    output
1322}
1323
1324fn module_descriptor(
1325    package_id: &str,
1326    descriptor: &LitStr,
1327    configuration_schema: Option<&LitStr>,
1328    configuration_defaults: Option<&LitStr>,
1329) -> syn::Result<String> {
1330    let supplied: Value = serde_json::from_str(&descriptor.value()).map_err(|error| {
1331        syn::Error::new(
1332            descriptor.span(),
1333            format!("Module Descriptor input is not valid JSON: {error}"),
1334        )
1335    })?;
1336    let mut supplied = supplied.as_object().cloned().ok_or_else(|| {
1337        syn::Error::new(
1338            descriptor.span(),
1339            "Module Descriptor input must be an object",
1340        )
1341    })?;
1342    if supplied.contains_key("configuration_schema") {
1343        return Err(syn::Error::new(
1344            descriptor.span(),
1345            "Module Descriptor input cannot contain `configuration_schema`; use the package-owned schema path attribute",
1346        ));
1347    }
1348    if supplied.contains_key("configuration_defaults") {
1349        return Err(syn::Error::new(
1350            descriptor.span(),
1351            "Module Descriptor input cannot contain `configuration_defaults`; use the package-owned defaults path attribute",
1352        ));
1353    }
1354    if let Some(schema_path) = configuration_schema {
1355        supplied.insert(
1356            "configuration_schema".to_owned(),
1357            read_configuration_schema(schema_path)?,
1358        );
1359    }
1360    if let Some(defaults_path) = configuration_defaults {
1361        if configuration_schema.is_none() {
1362            return Err(syn::Error::new(
1363                defaults_path.span(),
1364                "`configuration_defaults` requires `configuration_schema`",
1365            ));
1366        }
1367        let defaults = read_configuration_defaults(defaults_path)?;
1368        let schema = supplied
1369            .get("configuration_schema")
1370            .expect("explicit configuration Schema was inserted above");
1371        validate_configuration_defaults(&defaults, schema).map_err(|detail| {
1372            syn::Error::new(
1373                defaults_path.span(),
1374                format!("invalid package configuration defaults: {detail}"),
1375            )
1376        })?;
1377        supplied.insert("configuration_defaults".to_owned(), defaults);
1378    }
1379    for owned in [
1380        "package_id",
1381        "package_revision",
1382        "entrypoint",
1383        "execution_class",
1384        "restart_policy",
1385        "criticality",
1386    ] {
1387        if supplied.contains_key(owned) {
1388            return Err(syn::Error::new(
1389                descriptor.span(),
1390                format!("Module Descriptor input cannot override generated field `{owned}`"),
1391            ));
1392        }
1393    }
1394    let package_version = env::var("CARGO_PKG_VERSION").map_err(|_| {
1395        syn::Error::new(
1396            descriptor.span(),
1397            "CARGO_PKG_VERSION is unavailable while deriving Module Descriptor",
1398        )
1399    })?;
1400    Ok(complete_module_descriptor(
1401        package_id,
1402        &package_version,
1403        supplied,
1404    ))
1405}
1406
1407fn read_configuration_schema(schema_path: &LitStr) -> syn::Result<Value> {
1408    let schema = read_package_json(schema_path, "configuration Schema")?;
1409    if !schema.is_object() {
1410        return Err(syn::Error::new(
1411            schema_path.span(),
1412            "configuration Schema must be a JSON object",
1413        ));
1414    }
1415    Ok(schema)
1416}
1417
1418fn read_configuration_defaults(defaults_path: &LitStr) -> syn::Result<Value> {
1419    let defaults = read_package_json(defaults_path, "configuration defaults")?;
1420    if !defaults.is_object() {
1421        return Err(syn::Error::new(
1422            defaults_path.span(),
1423            "configuration defaults must be a JSON object",
1424        ));
1425    }
1426    Ok(defaults)
1427}
1428
1429fn read_package_json(path: &LitStr, label: &str) -> syn::Result<Value> {
1430    let relative = PathBuf::from(path.value());
1431    if relative.is_absolute()
1432        || relative
1433            .components()
1434            .any(|component| !matches!(component, std::path::Component::Normal(_)))
1435    {
1436        return Err(syn::Error::new(
1437            path.span(),
1438            format!("{label} path must stay inside the Module package"),
1439        ));
1440    }
1441    let manifest_dir = env::var_os("CARGO_MANIFEST_DIR").ok_or_else(|| {
1442        syn::Error::new(
1443            path.span(),
1444            format!("CARGO_MANIFEST_DIR is unavailable while deriving {label}"),
1445        )
1446    })?;
1447    let full_path = PathBuf::from(manifest_dir).join(relative);
1448    let bytes = fs::read(&full_path).map_err(|error| {
1449        syn::Error::new(
1450            path.span(),
1451            format!("failed to read {label} {}: {error}", full_path.display()),
1452        )
1453    })?;
1454    serde_json::from_slice(&bytes).map_err(|error| {
1455        syn::Error::new(
1456            path.span(),
1457            format!("{label} {} is invalid JSON: {error}", full_path.display()),
1458        )
1459    })
1460}
1461
1462fn validate_configuration_defaults(defaults: &Value, schema: &Value) -> Result<(), String> {
1463    if !defaults.is_object() {
1464        return Err("$: defaults must be an object".to_owned());
1465    }
1466    validate_default_value(defaults, schema, "$")
1467}
1468
1469fn validate_default_value(value: &Value, schema: &Value, path: &str) -> Result<(), String> {
1470    let schema = schema
1471        .as_object()
1472        .ok_or_else(|| format!("{path}: configuration Schema must be an object"))?;
1473    if schema
1474        .get("x-lenso-sensitive")
1475        .and_then(Value::as_bool)
1476        .unwrap_or(false)
1477    {
1478        return Err(format!(
1479            "{path}: sensitive configuration cannot have a package default"
1480        ));
1481    }
1482    if let Some(expected) = schema.get("type").and_then(Value::as_str) {
1483        let valid = match expected {
1484            "array" => value.is_array(),
1485            "boolean" => value.is_boolean(),
1486            "integer" => value
1487                .as_number()
1488                .is_some_and(|number| number.is_i64() || number.is_u64()),
1489            "null" => value.is_null(),
1490            "number" => value.is_number(),
1491            "object" => value.is_object(),
1492            "string" => value.is_string(),
1493            _ => false,
1494        };
1495        if !valid {
1496            return Err(format!(
1497                "{path}: default does not match Schema type `{expected}`"
1498            ));
1499        }
1500    }
1501    if let (Some(minimum), Some(number)) = (schema.get("minimum"), value.as_f64()) {
1502        let minimum = minimum
1503            .as_f64()
1504            .ok_or_else(|| format!("{path}: Schema minimum must be a number"))?;
1505        if number < minimum {
1506            return Err(format!(
1507                "{path}: default must be greater than or equal to {minimum}"
1508            ));
1509        }
1510    }
1511    if let Some(expected) = schema.get("const")
1512        && value != expected
1513    {
1514        return Err(format!("{path}: default does not match Schema const"));
1515    }
1516    if let Some(allowed) = schema.get("enum") {
1517        let allowed = allowed
1518            .as_array()
1519            .ok_or_else(|| format!("{path}: Schema enum must be an array"))?;
1520        if !allowed.contains(value) {
1521            return Err(format!("{path}: default is not in Schema enum"));
1522        }
1523    }
1524    validate_default_object(value, schema, path)?;
1525    validate_default_array(value, schema, path)
1526}
1527
1528fn validate_default_object(
1529    value: &Value,
1530    schema: &Map<String, Value>,
1531    path: &str,
1532) -> Result<(), String> {
1533    let Some(object) = value.as_object() else {
1534        return Ok(());
1535    };
1536    let empty = Map::new();
1537    let properties = schema.get("properties").map_or(Ok(&empty), |properties| {
1538        properties
1539            .as_object()
1540            .ok_or_else(|| format!("{path}: Schema properties must be an object"))
1541    })?;
1542    for (name, child) in object {
1543        if let Some(child_schema) = properties.get(name) {
1544            validate_default_value(child, child_schema, &format!("{path}.{name}"))?;
1545            continue;
1546        }
1547        match schema.get("additionalProperties") {
1548            Some(Value::Bool(false)) => {
1549                return Err(format!("{path}.{name}: additional property is not allowed"));
1550            }
1551            Some(Value::Object(additional_schema)) => validate_default_value(
1552                child,
1553                &Value::Object(additional_schema.clone()),
1554                &format!("{path}.{name}"),
1555            )?,
1556            _ => {}
1557        }
1558    }
1559    Ok(())
1560}
1561
1562fn validate_default_array(
1563    value: &Value,
1564    schema: &Map<String, Value>,
1565    path: &str,
1566) -> Result<(), String> {
1567    let (Some(items), Some(item_schema)) = (value.as_array(), schema.get("items")) else {
1568        return Ok(());
1569    };
1570    for (index, item) in items.iter().enumerate() {
1571        validate_default_value(item, item_schema, &format!("{path}[{index}]"))?;
1572    }
1573    Ok(())
1574}
1575
1576fn complete_module_descriptor(
1577    package_id: &str,
1578    package_version: &str,
1579    mut supplied: Map<String, Value>,
1580) -> String {
1581    let mut generated = Map::new();
1582    generated.insert("package_id".to_owned(), json!(package_id));
1583    generated.insert("package_revision".to_owned(), json!(package_version));
1584    generated.insert("entrypoint".to_owned(), json!("default"));
1585    for (key, value) in std::mem::take(&mut supplied) {
1586        generated.insert(key, value);
1587    }
1588    generated.insert("execution_class".to_owned(), json!("lenso.native-rust@1"));
1589    generated.insert(
1590        "restart_policy".to_owned(),
1591        json!({
1592            "mode": "never",
1593            "max_attempts": 0,
1594            "window": {"secs": 0, "nanos": 0},
1595            "backoff": {"secs": 0, "nanos": 0},
1596            "stability": {"secs": 0, "nanos": 0},
1597            "jitter": {"secs": 0, "nanos": 0}
1598        }),
1599    );
1600    generated.insert("criticality".to_owned(), json!("non_critical"));
1601    serde_json::to_string(&Value::Object(generated))
1602        .expect("generated Module Descriptor values must serialize")
1603}
1604
1605fn package_id() -> syn::Result<String> {
1606    let manifest_dir = env::var_os("CARGO_MANIFEST_DIR").ok_or_else(|| {
1607        syn::Error::new(
1608            proc_macro2::Span::call_site(),
1609            "CARGO_MANIFEST_DIR is unavailable",
1610        )
1611    })?;
1612    let manifest_path = PathBuf::from(manifest_dir).join("Cargo.toml");
1613    let manifest = fs::read_to_string(&manifest_path).map_err(|error| {
1614        syn::Error::new(
1615            proc_macro2::Span::call_site(),
1616            format!("failed to read {}: {error}", manifest_path.display()),
1617        )
1618    })?;
1619    let manifest: toml::Value = toml::from_str(&manifest).map_err(|error| {
1620        syn::Error::new(
1621            proc_macro2::Span::call_site(),
1622            format!("failed to parse {}: {error}", manifest_path.display()),
1623        )
1624    })?;
1625    manifest
1626        .get("package")
1627        .and_then(|package| package.get("metadata"))
1628        .and_then(|metadata| metadata.get("lenso"))
1629        .and_then(|lenso| lenso.get("package-id"))
1630        .and_then(toml::Value::as_str)
1631        .map(str::to_owned)
1632        .ok_or_else(|| {
1633            syn::Error::new(
1634                proc_macro2::Span::call_site(),
1635                "missing `[package.metadata.lenso] package-id = \"...\"` in Cargo.toml",
1636            )
1637        })
1638}
1639
1640#[cfg(test)]
1641mod tests {
1642    use super::*;
1643    use syn::parse_quote;
1644
1645    #[test]
1646    fn generated_descriptor_owns_identity_and_execution_defaults() {
1647        let supplied = serde_json::from_value::<Map<String, Value>>(json!({
1648            "provided_capabilities": [],
1649            "required_capabilities": []
1650        }))
1651        .unwrap();
1652        let descriptor = complete_module_descriptor("example.tool", "1.2.3", supplied);
1653        let descriptor: Value = serde_json::from_str(&descriptor).unwrap();
1654
1655        assert_eq!(descriptor["package_id"], "example.tool");
1656        assert_eq!(descriptor["package_revision"], "1.2.3");
1657        assert_eq!(descriptor["entrypoint"], "default");
1658        assert_eq!(descriptor["execution_class"], "lenso.native-rust@1");
1659        assert_eq!(descriptor["restart_policy"]["mode"], "never");
1660        assert_eq!(descriptor["criticality"], "non_critical");
1661    }
1662
1663    #[test]
1664    fn package_schema_is_embedded_as_descriptor_data() {
1665        let path = LitStr::new(
1666            "tests/fixtures/config.schema.json",
1667            proc_macro2::Span::call_site(),
1668        );
1669        let schema = read_configuration_schema(&path).unwrap();
1670
1671        assert_eq!(schema["type"], "object");
1672        assert_eq!(schema["required"], json!(["name", "retries"]));
1673    }
1674
1675    #[test]
1676    fn package_defaults_are_embedded_as_descriptor_data() {
1677        let path = LitStr::new(
1678            "tests/fixtures/config.defaults.json",
1679            proc_macro2::Span::call_site(),
1680        );
1681        let defaults = read_configuration_defaults(&path).unwrap();
1682
1683        assert_eq!(defaults, json!({"name": "fixture", "retries": 3}));
1684    }
1685
1686    #[test]
1687    fn factory_function_descriptor_embeds_package_defaults() {
1688        let descriptor = LitStr::new(
1689            r#"{"provided_capabilities":[],"required_capabilities":[]}"#,
1690            proc_macro2::Span::call_site(),
1691        );
1692        let schema = LitStr::new(
1693            "tests/fixtures/config.schema.json",
1694            proc_macro2::Span::call_site(),
1695        );
1696        let defaults = LitStr::new(
1697            "tests/fixtures/config.defaults.json",
1698            proc_macro2::Span::call_site(),
1699        );
1700
1701        let generated =
1702            module_descriptor("example.tool", &descriptor, Some(&schema), Some(&defaults)).unwrap();
1703        let generated: Value = serde_json::from_str(&generated).unwrap();
1704        assert_eq!(
1705            generated["configuration_defaults"],
1706            json!({"name": "fixture", "retries": 3})
1707        );
1708    }
1709
1710    #[test]
1711    fn typed_configuration_defaults_must_match_the_field_type() {
1712        let input: DeriveInput = parse_quote! {
1713            struct InvalidConfig {
1714                #[lenso(default = 3)]
1715                name: String,
1716            }
1717        };
1718
1719        let error = expand_module_config(&input).unwrap_err();
1720        assert!(error.to_string().contains("does not match the field type"));
1721    }
1722
1723    #[test]
1724    fn package_defaults_fail_closed_against_schema_constraints() {
1725        let schema = json!({
1726            "type": "object",
1727            "properties": {
1728                "retries": {"type": "integer", "minimum": 1},
1729                "token": {"x-lenso-sensitive": true}
1730            },
1731            "additionalProperties": false
1732        });
1733
1734        assert_eq!(
1735            validate_configuration_defaults(&json!({"retries": 0}), &schema),
1736            Err("$.retries: default must be greater than or equal to 1".to_owned())
1737        );
1738        assert_eq!(
1739            validate_configuration_defaults(&json!({"token": {"secret_ref": "TOKEN"}}), &schema),
1740            Err("$.token: sensitive configuration cannot have a package default".to_owned())
1741        );
1742    }
1743
1744    #[test]
1745    fn typed_ports_preserve_client_paths_and_cardinality() {
1746        let one: Type = parse_quote!(Port<secrets::SecretsClient>);
1747        let many: Type = parse_quote!(ManyPort<auth::AuthClient>);
1748
1749        let (one_client, one_cardinality) = port_client(&one).unwrap().unwrap();
1750        let (many_client, many_cardinality) = port_client(&many).unwrap().unwrap();
1751
1752        assert_eq!(quote!(#one_client).to_string(), "secrets :: SecretsClient");
1753        assert!(matches!(one_cardinality, PortCardinality::One));
1754        assert_eq!(quote!(#many_client).to_string(), "auth :: AuthClient");
1755        assert!(matches!(many_cardinality, PortCardinality::Many));
1756    }
1757
1758    #[test]
1759    fn managed_tasks_fields_are_initialized_and_connected_on_activate() {
1760        let mut module: ItemStruct = parse_quote! {
1761            struct Worker {
1762                #[tasks]
1763                tasks: ManagedTasks,
1764            }
1765        };
1766        let fields = analyze_struct_fields(&mut module).unwrap();
1767
1768        let task_field: syn::Ident = parse_quote!(tasks);
1769        assert_eq!(fields.tasks, vec![task_field]);
1770        assert_eq!(
1771            fields.initializers[0].to_string(),
1772            "tasks : :: core :: default :: Default :: default ()"
1773        );
1774        assert_eq!(
1775            task_connectors(&fields.tasks)[0].to_string(),
1776            "self . module . tasks . __lenso_connect (context . tasks () . clone ()) ? ;"
1777        );
1778        assert_eq!(
1779            task_disconnectors(&fields.tasks)[0].to_string(),
1780            "module . tasks . __lenso_disconnect () ;"
1781        );
1782        assert!(module.fields.iter().next().unwrap().attrs.is_empty());
1783    }
1784
1785    #[test]
1786    fn multiple_capabilities_reject_trait_impls() {
1787        let implementation: ItemImpl = parse_quote! {
1788            impl fixture::Provider for ExampleModule {}
1789        };
1790        let error = expand_provides(
1791            &[parse_quote!(fixture::One), parse_quote!(fixture::Two)],
1792            &implementation,
1793        )
1794        .expect_err("multi-Capability authoring must have one inherent impl");
1795
1796        assert!(
1797            error
1798                .to_string()
1799                .contains("multiple Capabilities require one inherent impl")
1800        );
1801    }
1802
1803    #[test]
1804    fn duplicate_capabilities_are_rejected() {
1805        let implementation: ItemImpl = parse_quote! { impl ExampleModule {} };
1806        let error = expand_provides(
1807            &[parse_quote!(fixture::One), parse_quote!(fixture::One)],
1808            &implementation,
1809        )
1810        .expect_err("one Capability cannot be contributed twice");
1811
1812        assert!(error.to_string().contains("same Capability more than once"));
1813    }
1814
1815    #[test]
1816    fn capability_paths_must_be_namespace_qualified() {
1817        let implementation: ItemImpl = parse_quote! { impl ExampleModule {} };
1818        let error = expand_provides(&[parse_quote!(One)], &implementation)
1819            .expect_err("generated Capability macros live in their namespace");
1820
1821        assert!(error.to_string().contains("namespace-qualified"));
1822    }
1823}