Skip to main content

lenso_native_adapter_macros/
lib.rs

1//! Derivation macros for statically linked Lenso Plugins and built-in Plugins.
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 PluginAttributes {
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 PluginAttributes {
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 Plugin 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 Plugin 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 Plugin 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 Plugin source and, for `consumer`, its factory registration.
124///
125/// ```compile_fail
126/// use lenso_native_adapter_macros::plugin;
127///
128/// #[plugin]
129/// enum InvalidPlugin {}
130/// ```
131#[proc_macro_attribute]
132pub fn plugin(attributes: TokenStream, item: TokenStream) -> TokenStream {
133    expand_authoring_item(attributes, item)
134}
135
136/// Binds an optional complete-object constructor and stop hook to a Plugin type.
137#[proc_macro_attribute]
138pub fn plugin_impl(attributes: TokenStream, item: TokenStream) -> TokenStream {
139    if !attributes.is_empty() {
140        return syn::Error::new(
141            proc_macro2::Span::call_site(),
142            "#[plugin_impl] does not accept arguments",
143        )
144        .into_compile_error()
145        .into();
146    }
147    let implementation = parse_macro_input!(item as ItemImpl);
148    expand_plugin_impl(implementation)
149        .unwrap_or_else(syn::Error::into_compile_error)
150        .into()
151}
152
153#[allow(clippy::too_many_lines)]
154fn expand_plugin_impl(mut implementation: ItemImpl) -> syn::Result<proc_macro2::TokenStream> {
155    if implementation.trait_.is_some() || !implementation.generics.params.is_empty() {
156        return Err(syn::Error::new_spanned(
157            &implementation,
158            "#[plugin_impl] requires a non-generic inherent impl",
159        ));
160    }
161    let Type::Path(plugin_path) = implementation.self_ty.as_ref() else {
162        return Err(syn::Error::new_spanned(
163            &implementation.self_ty,
164            "Plugin implementation type must be a path",
165        ));
166    };
167    let plugin_type = &plugin_path.path;
168    let plugin_ident = plugin_type
169        .segments
170        .last()
171        .expect("type paths are non-empty")
172        .ident
173        .clone();
174    let inputs_name = format_ident!("__LensoInputs{plugin_ident}");
175    let module_name = format_ident!(
176        "__lenso_custom_construction_{}",
177        snake(&plugin_ident.to_string())
178    );
179    let sdk = authoring_crate();
180    let mut create = None;
181    let mut stop = None;
182    for item in &mut implementation.items {
183        let syn::ImplItem::Fn(method) = item else {
184            continue;
185        };
186        let is_create = take_marker(&mut method.attrs, "create");
187        let is_stop = take_marker(&mut method.attrs, "stop");
188        let marked_method = method.clone();
189        for input in &mut method.sig.inputs {
190            if let syn::FnArg::Typed(argument) = input {
191                argument
192                    .attrs
193                    .retain(|attribute| !attribute.path().is_ident("lifecycle"));
194            }
195        }
196        if is_create && is_stop {
197            return Err(syn::Error::new_spanned(
198                method,
199                "one method cannot be both create and stop",
200            ));
201        }
202        if is_create && create.replace(marked_method.clone()).is_some() {
203            return Err(syn::Error::new_spanned(
204                method,
205                "duplicate #[create] method",
206            ));
207        }
208        if is_stop && stop.replace(marked_method).is_some() {
209            return Err(syn::Error::new_spanned(method, "duplicate #[stop] method"));
210        }
211    }
212    if create.is_none() && stop.is_none() {
213        return Ok(quote!(#implementation));
214    }
215
216    let construct_body = if let Some(create) = &create {
217        expand_create_call(create, plugin_type, &inputs_name, &sdk)?
218    } else {
219        quote!(super::#plugin_type::__lenso_auto_construct(context))
220    };
221    let (stop_function, stop_entry) = if let Some(stop) = &stop {
222        let body = expand_stop_call(stop, plugin_type, &sdk)?;
223        (
224            quote! {
225                fn stop(
226                    object: ::std::rc::Rc<dyn ::std::any::Any>,
227                    lifecycle: #sdk::__private::LifecycleContext,
228                ) -> #sdk::__private::PluginFuture {
229                    let object = match object.downcast::<super::#plugin_type>() {
230                        Ok(object) => object,
231                        Err(_) => return Box::pin(async {
232                            Err(#sdk::__private::RuntimeFailure::InvalidResolvedPlan {
233                                detail: "linked Plugin stop hook received the wrong type".to_owned(),
234                            })
235                        }),
236                    };
237                    #body
238                }
239            },
240            quote!(Some(stop)),
241        )
242    } else {
243        (quote!(), quote!(None))
244    };
245
246    Ok(quote! {
247        #implementation
248
249        #[doc(hidden)]
250        mod #module_name {
251            const _: () = assert!(
252                super::#plugin_type::__LENSO_AUTHORING_VERSION == 2,
253                "#[plugin_impl] create/stop hooks cannot be combined with legacy lifecycle, Port, resources, or tasks fields",
254            );
255
256            fn plugin_type() -> ::std::any::TypeId {
257                ::std::any::TypeId::of::<super::#plugin_type>()
258            }
259
260            fn construct(
261                context: #sdk::__private::ConstructionContext,
262            ) -> #sdk::__private::ErasedConstructionFuture {
263                #construct_body
264            }
265
266            #stop_function
267
268            #sdk::__private::__inventory::submit! {
269                #sdk::__private::LinkedPluginConstruction::new(
270                    plugin_type,
271                    true,
272                    construct,
273                    #stop_entry,
274                )
275            }
276        }
277    })
278}
279
280fn expand_create_call(
281    method: &syn::ImplItemFn,
282    plugin_type: &Path,
283    inputs_name: &syn::Ident,
284    sdk: &proc_macro2::TokenStream,
285) -> syn::Result<proc_macro2::TokenStream> {
286    let mut names = Vec::new();
287    let mut arguments = Vec::new();
288    for input in &method.sig.inputs {
289        let syn::FnArg::Typed(argument) = input else {
290            return Err(syn::Error::new_spanned(
291                input,
292                "#[create] is an associated function without a receiver",
293            ));
294        };
295        let syn::Pat::Ident(pattern) = argument.pat.as_ref() else {
296            return Err(syn::Error::new_spanned(
297                &argument.pat,
298                "#[create] inputs must be plain identifiers",
299            ));
300        };
301        let lifecycle = argument
302            .attrs
303            .iter()
304            .any(|attribute| attribute.path().is_ident("lifecycle"));
305        if lifecycle {
306            if !is_named_type(&argument.ty, "LifecycleContext") {
307                return Err(syn::Error::new_spanned(
308                    &argument.ty,
309                    "#[lifecycle] input must have type LifecycleContext",
310                ));
311            }
312            arguments.push(quote!(context.lifecycle().clone()));
313        } else {
314            names.push(pattern.ident.clone());
315            arguments.push(quote!(#pattern));
316        }
317    }
318    let method_name = &method.sig.ident;
319    let invoke = if method.sig.asyncness.is_some() {
320        quote!(super::#plugin_type::#method_name(#(#arguments),*).await)
321    } else {
322        quote!(super::#plugin_type::#method_name(#(#arguments),*))
323    };
324    let value = if returns_result(&method.sig.output) {
325        quote!(#invoke.map_err(|error| #sdk::__private::RuntimeFailure::PluginFailure {
326            detail: format!("Plugin construction failed: {error}"),
327        })?)
328    } else {
329        invoke
330    };
331    Ok(quote! {
332        Box::pin(async move {
333            let super::#inputs_name { #(#names),* } =
334                super::#plugin_type::__lenso_inputs(&context)?;
335            let plugin = #value;
336            Ok(::std::rc::Rc::new(plugin) as ::std::rc::Rc<dyn ::std::any::Any>)
337        })
338    })
339}
340
341fn expand_stop_call(
342    method: &syn::ImplItemFn,
343    plugin_type: &Path,
344    sdk: &proc_macro2::TokenStream,
345) -> syn::Result<proc_macro2::TokenStream> {
346    let mut inputs = method.sig.inputs.iter();
347    let Some(syn::FnArg::Receiver(receiver)) = inputs.next() else {
348        return Err(syn::Error::new_spanned(
349            &method.sig,
350            "#[stop] requires an &self receiver",
351        ));
352    };
353    if receiver.reference.is_none() || receiver.mutability.is_some() {
354        return Err(syn::Error::new_spanned(receiver, "#[stop] requires &self"));
355    }
356    let mut arguments = Vec::new();
357    for input in inputs {
358        let syn::FnArg::Typed(argument) = input else {
359            return Err(syn::Error::new_spanned(input, "invalid #[stop] input"));
360        };
361        if !argument
362            .attrs
363            .iter()
364            .any(|attribute| attribute.path().is_ident("lifecycle"))
365            || !is_named_type(&argument.ty, "LifecycleContext")
366        {
367            return Err(syn::Error::new_spanned(
368                input,
369                "#[stop] accepts only an optional #[lifecycle] LifecycleContext",
370            ));
371        }
372        arguments.push(quote!(lifecycle));
373    }
374    if arguments.len() > 1 {
375        return Err(syn::Error::new_spanned(
376            &method.sig,
377            "#[stop] accepts at most one lifecycle input",
378        ));
379    }
380    let method_name = &method.sig.ident;
381    let invoke = if method.sig.asyncness.is_some() {
382        quote!(super::#plugin_type::#method_name(object.as_ref(), #(#arguments),*).await)
383    } else {
384        quote!(super::#plugin_type::#method_name(object.as_ref(), #(#arguments),*))
385    };
386    let result = if returns_result(&method.sig.output) {
387        quote!(#invoke.map_err(|error| #sdk::__private::RuntimeFailure::PluginFailure {
388            detail: format!("Plugin stop failed: {error}"),
389        }))
390    } else {
391        quote!({ #invoke; Ok(()) })
392    };
393    Ok(quote!(Box::pin(async move { #result })))
394}
395
396fn returns_result(output: &syn::ReturnType) -> bool {
397    let syn::ReturnType::Type(_, ty) = output else {
398        return false;
399    };
400    let Type::Path(path) = ty.as_ref() else {
401        return false;
402    };
403    path.path
404        .segments
405        .last()
406        .is_some_and(|segment| segment.ident == "Result")
407}
408
409fn expand_authoring_item(attributes: TokenStream, item: TokenStream) -> TokenStream {
410    let attributes = match syn::parse::<PluginAttributes>(attributes) {
411        Ok(attributes) => attributes,
412        Err(error) => return authoring_error(&error),
413    };
414    let item = match syn::parse::<Item>(item) {
415        Ok(item) => item,
416        Err(error) => return authoring_error(&error),
417    };
418    let expanded = match item {
419        Item::Fn(function) => expand_plugin_function(&attributes, &function),
420        Item::Struct(plugin) => expand_plugin_struct(&attributes, plugin),
421        other => Err(syn::Error::new_spanned(
422            other,
423            "a native Plugin must be declared by a factory function or a named-field struct",
424        )),
425    };
426    match expanded {
427        Ok(tokens) => tokens.into(),
428        Err(error) => authoring_error(&error),
429    }
430}
431
432fn authoring_error(error: &syn::Error) -> TokenStream {
433    syn::Error::new(error.span(), error.to_string())
434        .into_compile_error()
435        .into()
436}
437
438/// Derives the locked JSON Schema fragment consumed by a struct-level Plugin.
439#[proc_macro_derive(PluginConfig, attributes(lenso, serde))]
440pub fn plugin_config(item: TokenStream) -> TokenStream {
441    let input = match syn::parse::<DeriveInput>(item) {
442        Ok(input) => input,
443        Err(error) => return authoring_error(&error),
444    };
445    match expand_plugin_config(&input) {
446        Ok(tokens) => tokens.into(),
447        Err(error) => authoring_error(&error),
448    }
449}
450
451fn expand_plugin_config(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
452    let Data::Struct(data) = &input.data else {
453        return Err(syn::Error::new_spanned(
454            input,
455            "Plugin configuration must be a named-field struct",
456        ));
457    };
458    let Fields::Named(fields) = &data.fields else {
459        return Err(syn::Error::new_spanned(
460            &data.fields,
461            "Plugin configuration must use named fields",
462        ));
463    };
464    let mut properties = Map::new();
465    let mut defaults = Map::new();
466    let mut required = Vec::new();
467    for field in &fields.named {
468        let ident = field.ident.as_ref().expect("named fields have identifiers");
469        let name = serde_field_name(&field.attrs, ident)?;
470        let (schema, optional) = configuration_type_schema(&field.ty)?;
471        if let Some(default) = configuration_field_default(&field.attrs)? {
472            if !configuration_value_matches_schema(&default, &schema) {
473                return Err(syn::Error::new_spanned(
474                    field,
475                    "Plugin configuration default does not match the field type",
476                ));
477            }
478            defaults.insert(name.clone(), default);
479        }
480        properties.insert(name.clone(), schema);
481        if !optional {
482            required.push(Value::String(name));
483        }
484    }
485    let schema = canonical_json(&json!({
486        "$schema": "https://json-schema.org/draft/2020-12/schema",
487        "type": "object",
488        "additionalProperties": false,
489        "required": required,
490        "properties": properties,
491    }));
492    let defaults = canonical_json(&Value::Object(defaults));
493    let macro_name = format_ident!("__lenso_config_schema_{}", snake(&input.ident.to_string()));
494    let defaults_macro_name = format_ident!(
495        "__lenso_config_defaults_{}",
496        snake(&input.ident.to_string())
497    );
498    Ok(quote! {
499        #[doc(hidden)]
500        #[macro_export]
501        macro_rules! #macro_name {
502            () => { #schema };
503        }
504        #[doc(hidden)]
505        #[macro_export]
506        macro_rules! #defaults_macro_name {
507            () => { #defaults };
508        }
509    })
510}
511
512fn configuration_field_default(attributes: &[Attribute]) -> syn::Result<Option<Value>> {
513    let mut default = None;
514    for attribute in attributes {
515        if !attribute.path().is_ident("lenso") {
516            continue;
517        }
518        attribute.parse_nested_meta(|meta| {
519            if !meta.path.is_ident("default") {
520                return Err(meta.error("expected `default = <JSON literal>`"));
521            }
522            if default.is_some() {
523                return Err(meta.error("duplicate Plugin configuration default"));
524            }
525            let expression = meta.value()?.parse::<Expr>()?;
526            let encoded = quote!(#expression).to_string();
527            default = Some(serde_json::from_str(&encoded).map_err(|error| {
528                meta.error(format!(
529                    "Plugin configuration default must be a JSON literal: {error}"
530                ))
531            })?);
532            Ok(())
533        })?;
534    }
535    Ok(default)
536}
537
538fn configuration_value_matches_schema(value: &Value, schema: &Value) -> bool {
539    match schema.get("type").and_then(Value::as_str) {
540        Some("array") => value.as_array().is_some_and(|items| {
541            schema.get("items").is_some_and(|schema| {
542                items
543                    .iter()
544                    .all(|item| configuration_value_matches_schema(item, schema))
545            })
546        }),
547        Some("boolean") => value.is_boolean(),
548        Some("integer") => value
549            .as_number()
550            .is_some_and(|number| number.is_i64() || number.is_u64()),
551        Some("number") => value.is_number(),
552        Some("string") => value.is_string(),
553        _ => false,
554    }
555}
556
557fn serde_field_name(attributes: &[Attribute], ident: &syn::Ident) -> syn::Result<String> {
558    let mut name = ident.to_string();
559    for attribute in attributes {
560        if !attribute.path().is_ident("serde") {
561            continue;
562        }
563        attribute.parse_nested_meta(|meta| {
564            if meta.path.is_ident("rename") {
565                name = meta.value()?.parse::<LitStr>()?.value();
566            }
567            Ok(())
568        })?;
569    }
570    Ok(name)
571}
572
573fn configuration_type_schema(ty: &Type) -> syn::Result<(Value, bool)> {
574    let Type::Path(path) = ty else {
575        return Err(syn::Error::new_spanned(
576            ty,
577            "Plugin configuration fields must use portable named types",
578        ));
579    };
580    let segment = path.path.segments.last().expect("type paths are non-empty");
581    let name = segment.ident.to_string();
582    if name == "Option" {
583        return Ok((configuration_inner_schema(segment, ty)?, true));
584    }
585    if name == "Vec" {
586        return Ok((
587            json!({"type": "array", "items": configuration_inner_schema(segment, ty)?}),
588            false,
589        ));
590    }
591    let schema = match name.as_str() {
592        "String" => json!({"type": "string"}),
593        "bool" => json!({"type": "boolean"}),
594        "f32" | "f64" => json!({"type": "number"}),
595        "i8" | "i16" | "i32" | "i64" | "i128" | "isize" | "u8" | "u16" | "u32" | "u64" | "u128"
596        | "usize" => json!({"type": "integer"}),
597        _ => {
598            return Err(syn::Error::new_spanned(
599                ty,
600                "unsupported Plugin configuration field type; use String, bool, a number, Option<T>, or Vec<T>",
601            ));
602        }
603    };
604    Ok((schema, false))
605}
606
607fn configuration_inner_schema(segment: &syn::PathSegment, ty: &Type) -> syn::Result<Value> {
608    let PathArguments::AngleBracketed(arguments) = &segment.arguments else {
609        return Err(syn::Error::new_spanned(
610            ty,
611            "configuration container requires one type",
612        ));
613    };
614    let [GenericArgument::Type(inner)] = arguments.args.iter().collect::<Vec<_>>().as_slice()
615    else {
616        return Err(syn::Error::new_spanned(
617            ty,
618            "configuration container requires one type",
619        ));
620    };
621    configuration_type_schema(inner).map(|(schema, _)| schema)
622}
623
624#[allow(clippy::too_many_lines)]
625fn expand_plugin_function(
626    attributes: &PluginAttributes,
627    function: &ItemFn,
628) -> syn::Result<proc_macro2::TokenStream> {
629    let sdk = authoring_crate();
630    if attributes.descriptor.is_none()
631        && (attributes.configuration_schema.is_some()
632            || attributes.configuration_defaults.is_some())
633    {
634        return Err(syn::Error::new_spanned(
635            function,
636            "`configuration_schema` and `configuration_defaults` require `descriptor` on a factory function",
637        ));
638    }
639    if attributes.validate.is_some()
640        || attributes.prepare.is_some()
641        || attributes.activate.is_some()
642        || attributes.deactivate.is_some()
643        || attributes.lifecycle
644        || attributes.consumer
645    {
646        return Err(syn::Error::new_spanned(
647            function,
648            "struct-level Plugin attributes are unavailable on factory functions",
649        ));
650    }
651    let (plugin_id, root_slot) = plugin_metadata()?;
652    let descriptor_json = attributes
653        .descriptor
654        .as_ref()
655        .map(|descriptor| {
656            plugin_descriptor(
657                &plugin_id,
658                &root_slot,
659                descriptor,
660                attributes.configuration_schema.as_ref(),
661                attributes.configuration_defaults.as_ref(),
662            )
663        })
664        .transpose()?;
665    let function_name = &function.sig.ident;
666    let generated_plugin = format_ident!("__lenso_plugin_{function_name}");
667    let link_function = format_ident!("__lenso_link_{function_name}");
668    let descriptor_constant = descriptor_json.map(|descriptor| {
669        let artifact =
670            format!("LENSO_PLUGIN_DESCRIPTOR_V1\0{descriptor}\0END_LENSO_PLUGIN_DESCRIPTOR_V1");
671        let artifact_length = artifact.len();
672        let artifact = proc_macro2::Literal::byte_string(artifact.as_bytes());
673        let package_file_tracking = package_file_tracking([
674            attributes.configuration_schema.as_ref(),
675            attributes.configuration_defaults.as_ref(),
676        ]);
677        quote! {
678            /// Generated package-owned Plugin Descriptor bytes.
679            pub const PLUGIN_DESCRIPTOR_JSON: &str = #descriptor;
680            /// Linker-retained descriptor artifact consumed without executing package code.
681            #[doc(hidden)]
682            #[used]
683            pub static __LENSO_PLUGIN_DESCRIPTOR_ARTIFACT: [u8; #artifact_length] = *#artifact;
684            #(#package_file_tracking)*
685        }
686    });
687
688    Ok(quote! {
689        /// Runtime package identity derived from Cargo package metadata.
690        pub const PACKAGE_ID: &str = #plugin_id;
691        /// Exact linked Cargo package version.
692        pub const PACKAGE_VERSION: &str = env!("CARGO_PKG_VERSION");
693        /// Exact Host Build identity for this linked package.
694        pub const FACTORY_IDENTITY: &str = concat!(#plugin_id, "@", env!("CARGO_PKG_VERSION"));
695        #descriptor_constant
696
697        #function
698
699        #[doc(hidden)]
700        mod #generated_plugin {
701            #[derive(Clone, Copy, Debug, Default)]
702            struct Factory;
703
704            impl #sdk::__private::NativePluginFactory for Factory {
705                fn package_id(&self) -> &'static str {
706                    #plugin_id
707                }
708
709                fn package_version(&self) -> &'static str {
710                    env!("CARGO_PKG_VERSION")
711                }
712
713                fn instantiate(
714                    &self,
715                    context: #sdk::__private::NativePluginFactoryContext<'_>,
716                ) -> Result<
717                    #sdk::__private::NativePluginInstance,
718                    #sdk::__private::RuntimeFailure,
719                > {
720                    super::#function_name(context)
721                }
722            }
723
724            pub(super) fn factory() -> std::rc::Rc<dyn #sdk::__private::NativePluginFactory> {
725                std::rc::Rc::new(Factory)
726            }
727
728            #sdk::__private::__inventory::submit! {
729                #sdk::__private::LinkedNativePluginFactory::new(
730                    factory,
731                    super::PLUGIN_DESCRIPTOR_JSON,
732                )
733            }
734
735            // Make Cargo track the manifest that supplied the generated identity.
736            const _: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/Cargo.toml"));
737        }
738
739        /// Explicit Host linkage anchor generated for this Plugin factory.
740        #[doc(hidden)]
741        pub fn #link_function() {
742            #sdk::__private::link_native_plugin(
743                #sdk::__private::LinkedNativePluginFactory::new(
744                    #generated_plugin::factory,
745                    PLUGIN_DESCRIPTOR_JSON,
746                ),
747            );
748        }
749    })
750}
751
752/// Derives one or more provided Capability endpoints, one native factory, and static registration.
753///
754/// Apply this to one inherent implementation containing the Capabilities' domain methods
755/// for a struct already annotated with [`plugin`]. Generated bindings lower the
756/// methods into the Adapter-facing Provider trait. Existing explicit Provider
757/// trait implementations remain supported as a single-Capability compatibility
758/// escape hatch. Multi-Capability Plugins must use one inherent implementation.
759#[proc_macro_attribute]
760pub fn provides(attributes: TokenStream, item: TokenStream) -> TokenStream {
761    let capabilities =
762        parse_macro_input!(attributes with Punctuated::<Path, Token![,]>::parse_terminated);
763    let implementation = parse_macro_input!(item as ItemImpl);
764    expand_provides(
765        &capabilities.into_iter().collect::<Vec<_>>(),
766        &implementation,
767    )
768    .unwrap_or_else(syn::Error::into_compile_error)
769    .into()
770}
771
772struct CapabilityContribution {
773    namespace: Path,
774    descriptor: syn::Ident,
775    endpoints: syn::Ident,
776    lower: syn::Ident,
777    object_lower: syn::Ident,
778    trait_object_lower: syn::Ident,
779    provider_wrapper: syn::Ident,
780    projection_module: syn::Ident,
781}
782
783fn capability_contributions(capabilities: &[Path]) -> syn::Result<Vec<CapabilityContribution>> {
784    let mut seen = BTreeSet::new();
785    capabilities
786        .iter()
787        .enumerate()
788        .map(|(index, capability)| {
789            let path = quote!(#capability).to_string();
790            if !seen.insert(path) {
791                return Err(syn::Error::new_spanned(
792                    capability,
793                    "a Plugin cannot provide the same Capability more than once",
794                ));
795            }
796            let mut namespace = capability.clone();
797            let capability_ident = namespace
798                .segments
799                .pop()
800                .ok_or_else(|| syn::Error::new_spanned(capability, "Capability path is empty"))?
801                .into_value()
802                .ident;
803            namespace.segments.pop_punct();
804            if namespace.segments.is_empty() {
805                return Err(syn::Error::new_spanned(
806                    capability,
807                    "Capability must be namespace-qualified, for example `agent::Agent`",
808                ));
809            }
810            let capability_snake = snake(&capability_ident.to_string());
811            Ok(CapabilityContribution {
812                namespace,
813                descriptor: format_ident!("__lenso_provided_{capability_snake}"),
814                endpoints: format_ident!("__lenso_native_endpoints_{capability_snake}"),
815                lower: format_ident!("__lenso_native_lower_{capability_snake}"),
816                object_lower: format_ident!("__lenso_native_lower_object_{capability_snake}"),
817                trait_object_lower: format_ident!(
818                    "__lenso_native_lower_trait_object_{capability_snake}"
819                ),
820                provider_wrapper: format_ident!("Provider"),
821                projection_module: format_ident!("projection_{index}"),
822            })
823        })
824        .collect()
825}
826
827fn provided_module(
828    capabilities: &[Path],
829    implementation: &ItemImpl,
830) -> syn::Result<(syn::Ident, bool)> {
831    if capabilities.is_empty() {
832        return Err(syn::Error::new_spanned(
833            implementation,
834            "`provides` requires at least one namespace-qualified Capability",
835        ));
836    }
837    if capabilities.len() > 1 && implementation.trait_.is_some() {
838        return Err(syn::Error::new_spanned(
839            implementation,
840            "multiple Capabilities require one inherent impl containing their domain methods",
841        ));
842    }
843    let Type::Path(plugin_type) = implementation.self_ty.as_ref() else {
844        return Err(syn::Error::new_spanned(
845            &implementation.self_ty,
846            "the Plugin provider type must be a path",
847        ));
848    };
849    let plugin_ident = plugin_type
850        .path
851        .segments
852        .last()
853        .ok_or_else(|| {
854            syn::Error::new_spanned(&plugin_type.path, "the Plugin provider type is empty")
855        })?
856        .ident
857        .clone();
858    Ok((plugin_ident, implementation.trait_.is_none()))
859}
860
861#[allow(clippy::too_many_lines)]
862fn expand_provides(
863    capabilities: &[Path],
864    implementation: &ItemImpl,
865) -> syn::Result<proc_macro2::TokenStream> {
866    let sdk = authoring_crate();
867    let (plugin_ident, lowers_domain_methods) = provided_module(capabilities, implementation)?;
868    let contributions = capability_contributions(capabilities)?;
869    let provided_descriptors = contributions
870        .iter()
871        .map(|contribution| {
872            let namespace = &contribution.namespace;
873            let descriptor = &contribution.descriptor;
874            quote!(#namespace::#descriptor!())
875        })
876        .collect::<Vec<_>>();
877    let plugin_descriptor = format_ident!(
878        "__lenso_plugin_descriptor_{}",
879        snake(&plugin_ident.to_string())
880    );
881    let generated_plugin = format_ident!("__lenso_provider_{}", snake(&plugin_ident.to_string()));
882    let link_function = format_ident!("__lenso_link_{}", snake(&plugin_ident.to_string()));
883    let lifecycle = format_ident!("__LensoLifecycle{plugin_ident}");
884    let artifact = format_ident!("__LENSO_PLUGIN_DESCRIPTOR_ARTIFACT_{plugin_ident}");
885    let provider_implementations = contributions
886        .iter()
887        .filter_map(|contribution| {
888            let namespace = &contribution.namespace;
889            let lower = &contribution.lower;
890            lowers_domain_methods.then(|| {
891                quote! {
892                    #namespace::#lower!(#plugin_ident, #sdk::__private);
893                }
894            })
895        })
896        .collect::<Vec<_>>();
897    let object_provider_implementations = contributions
898        .iter()
899        .map(|contribution| {
900            let namespace = &contribution.namespace;
901            let provider_wrapper = &contribution.provider_wrapper;
902            let projection_module = &contribution.projection_module;
903            let object_lower = if lowers_domain_methods {
904                &contribution.object_lower
905            } else {
906                &contribution.trait_object_lower
907            };
908            quote! {
909                mod #projection_module {
910                    #[derive(Clone, Debug)]
911                    pub(super) struct #provider_wrapper(
912                        pub(super) #sdk::__private::PluginObject<super::super::#plugin_ident>
913                    );
914
915                    impl #provider_wrapper {
916                        fn get(
917                            &self,
918                        ) -> Result<
919                            ::std::rc::Rc<super::super::#plugin_ident>,
920                            #sdk::__private::RuntimeFailure,
921                        > {
922                            self.0.get()
923                        }
924                    }
925
926                    super::super::#namespace::#object_lower!(
927                        #provider_wrapper,
928                        super::super::#plugin_ident,
929                        #sdk::__private
930                    );
931                }
932            }
933        })
934        .collect::<Vec<_>>();
935    let endpoint_contributions = contributions
936        .iter()
937        .map(|contribution| {
938            let namespace = &contribution.namespace;
939            let endpoints = &contribution.endpoints;
940            let provider_wrapper = &contribution.provider_wrapper;
941            let projection_module = &contribution.projection_module;
942            quote! {
943                let (provided_requests, provided_streams, provided_events) =
944                    super::#namespace::#endpoints!(
945                        #projection_module::#provider_wrapper(plugin.clone()),
946                        #sdk::__private
947                    );
948                request_endpoints.extend(provided_requests);
949                stream_endpoints.extend(provided_streams);
950                event_endpoints.extend(provided_events);
951            }
952        })
953        .collect::<Vec<_>>();
954    let v2_endpoint_contributions = endpoint_contributions.clone();
955
956    let mut implementation = implementation.clone();
957    implementation
958        .attrs
959        .push(syn::parse_quote!(#[allow(clippy::unused_async, clippy::unused_async_trait_impl)]));
960
961    Ok(quote! {
962        #implementation
963        #(#provider_implementations)*
964
965        /// Generated package-owned Plugin Descriptor bytes.
966        pub const PLUGIN_DESCRIPTOR_JSON: &str = #plugin_descriptor!(
967            #(#provided_descriptors),*
968        );
969        #[doc(hidden)]
970        const __LENSO_PLUGIN_DESCRIPTOR_ARTIFACT_TEXT: &str = concat!(
971            "LENSO_PLUGIN_DESCRIPTOR_V1\0",
972            #plugin_descriptor!(#(#provided_descriptors),*),
973            "\0END_LENSO_PLUGIN_DESCRIPTOR_V1",
974        );
975        /// Linker-retained descriptor artifact consumed without executing package code.
976        #[doc(hidden)]
977        #[used]
978        pub static #artifact: &[u8] = __LENSO_PLUGIN_DESCRIPTOR_ARTIFACT_TEXT.as_bytes();
979
980        #[doc(hidden)]
981        mod #generated_plugin {
982            #(#object_provider_implementations)*
983
984            #[derive(Clone, Copy, Debug, Default)]
985            struct Factory;
986
987            impl #sdk::__private::NativePluginFactory for Factory {
988                fn package_id(&self) -> &'static str { super::PACKAGE_ID }
989                fn package_version(&self) -> &'static str { super::PACKAGE_VERSION }
990                fn runtime_profile(&self) -> &'static str {
991                    super::#plugin_ident::__LENSO_RUNTIME_PROFILE
992                }
993
994                fn instantiate(
995                    &self,
996                    context: #sdk::__private::NativePluginFactoryContext<'_>,
997                ) -> Result<
998                    #sdk::__private::NativePluginInstance,
999                    #sdk::__private::RuntimeFailure,
1000                > {
1001                    if super::#plugin_ident::__LENSO_AUTHORING_VERSION == 2 {
1002                        let plugin = #sdk::__private::PluginObject::<super::#plugin_ident>::empty();
1003                        let lifecycle = #sdk::__private::CompleteObjectLifecycle::linked(
1004                            plugin.clone(),
1005                            context.configuration(),
1006                        )?;
1007                        let mut request_endpoints = Vec::new();
1008                        let mut stream_endpoints = Vec::new();
1009                        let mut event_endpoints = Vec::new();
1010                        #(#v2_endpoint_contributions)*
1011                        return Ok(#sdk::__private::NativePluginInstance::with_all_endpoints(
1012                            request_endpoints,
1013                            stream_endpoints,
1014                            event_endpoints,
1015                            lifecycle,
1016                        ));
1017                    }
1018                    let plugin = ::std::rc::Rc::new(
1019                        super::#plugin_ident::__lenso_construct(context)?,
1020                    );
1021                    let lifecycle = super::#lifecycle { plugin: plugin.clone() };
1022                    let plugin = #sdk::__private::PluginObject::from_value(plugin);
1023                    let mut request_endpoints = Vec::new();
1024                    let mut stream_endpoints = Vec::new();
1025                    let mut event_endpoints = Vec::new();
1026                    #(#endpoint_contributions)*
1027                    Ok(#sdk::__private::NativePluginInstance::with_all_endpoints(
1028                        request_endpoints,
1029                        stream_endpoints,
1030                        event_endpoints,
1031                        lifecycle,
1032                    ))
1033                }
1034            }
1035
1036            pub(super) fn factory() -> ::std::rc::Rc<dyn #sdk::__private::NativePluginFactory> {
1037                ::std::rc::Rc::new(Factory)
1038            }
1039
1040            #sdk::__private::__inventory::submit! {
1041                #sdk::__private::LinkedNativePluginFactory::new(
1042                    factory,
1043                    super::PLUGIN_DESCRIPTOR_JSON,
1044                )
1045            }
1046        }
1047
1048        /// Explicit Host linkage anchor generated for this Plugin factory.
1049        #[doc(hidden)]
1050        pub fn #link_function() {
1051            #sdk::__private::link_native_plugin(
1052                #sdk::__private::LinkedNativePluginFactory::new(
1053                    #generated_plugin::factory,
1054                    PLUGIN_DESCRIPTOR_JSON,
1055                ),
1056            );
1057        }
1058    })
1059}
1060
1061#[allow(clippy::too_many_lines)]
1062fn expand_plugin_struct(
1063    attributes: &PluginAttributes,
1064    mut plugin: ItemStruct,
1065) -> syn::Result<proc_macro2::TokenStream> {
1066    let sdk = authoring_crate();
1067    if attributes.descriptor.is_some() {
1068        return Err(syn::Error::new_spanned(
1069            &plugin.ident,
1070            "struct-level Plugins derive their Descriptor; remove `descriptor`",
1071        ));
1072    }
1073    let (plugin_id, root_slot) = plugin_metadata()?;
1074    let package_version = env::var("CARGO_PKG_VERSION").map_err(|_| {
1075        syn::Error::new_spanned(
1076            &plugin.ident,
1077            "CARGO_PKG_VERSION is unavailable while deriving Plugin Descriptor",
1078        )
1079    })?;
1080    let StructFields {
1081        config_type,
1082        ports,
1083        tasks,
1084        initializers,
1085        construction_fields,
1086    } = analyze_struct_fields(&mut plugin, &sdk)?;
1087    let schema = configuration_schema_tokens(
1088        attributes.configuration_schema.as_ref(),
1089        config_type.as_ref(),
1090    )?;
1091    let configuration_defaults = configuration_defaults_tokens(
1092        attributes.configuration_schema.as_ref(),
1093        attributes.configuration_defaults.as_ref(),
1094        config_type.as_ref(),
1095    )?;
1096    let name = &plugin.ident;
1097    let inputs_name = format_ident!("__LensoInputs{name}");
1098    let input_fields = construction_fields
1099        .iter()
1100        .filter_map(|field| match field.kind {
1101            ConstructionFieldKind::Config | ConstructionFieldKind::Dependency { .. } => {
1102                let name = &field.name;
1103                let ty = &field.ty;
1104                Some(quote!(#name: #ty))
1105            }
1106            ConstructionFieldKind::Private | ConstructionFieldKind::Legacy => None,
1107        });
1108    let input_initializers = construction_fields
1109        .iter()
1110        .filter_map(|field| v2_input_initializer(field, &sdk));
1111    let construction_module = format_ident!("__lenso_construction_{}", snake(&name.to_string()));
1112    let v2_configuration = construct_v2_configuration(
1113        &plugin_id,
1114        config_type.as_ref(),
1115        attributes.validate.as_ref(),
1116        &sdk,
1117    );
1118    let v2_initializers = construction_fields
1119        .iter()
1120        .map(|field| v2_field_initializer(field, &sdk))
1121        .collect::<Vec<_>>();
1122    let uses_legacy_authoring = construction_fields
1123        .iter()
1124        .any(|field| matches!(field.kind, ConstructionFieldKind::Legacy))
1125        || attributes.lifecycle
1126        || attributes.prepare.is_some()
1127        || attributes.activate.is_some()
1128        || attributes.deactivate.is_some();
1129    let authoring_version = if uses_legacy_authoring { 1_u32 } else { 2_u32 };
1130    let runtime_profile = if uses_legacy_authoring {
1131        "lenso.native-authoring@1"
1132    } else {
1133        "lenso.native-authoring@2"
1134    };
1135    let v2_construct = if uses_legacy_authoring {
1136        quote! {
1137            Err(#sdk::__private::RuntimeFailure::InvalidResolvedPlan {
1138                detail: "legacy Plugin fields cannot use authoring version 2".to_owned(),
1139            })
1140        }
1141    } else {
1142        quote! {
1143            #v2_configuration
1144            let plugin = Self { #(#v2_initializers),* };
1145            Ok(::std::rc::Rc::new(plugin) as ::std::rc::Rc<dyn ::std::any::Any>)
1146        }
1147    };
1148    let lifecycle_name = format_ident!("__LensoLifecycle{name}");
1149    let descriptor_macro = format_ident!("__lenso_plugin_descriptor_{}", snake(&name.to_string()));
1150    let requirement_macros = ports
1151        .iter()
1152        .map(|(_, client, cardinality)| requirement_macro(client, *cardinality))
1153        .collect::<syn::Result<Vec<_>>>()?;
1154    let dependency_requirement_macros = construction_fields
1155        .iter()
1156        .filter_map(|field| match &field.kind {
1157            ConstructionFieldKind::Dependency {
1158                id,
1159                client,
1160                cardinality,
1161            } => Some(named_requirement_macro(client, *cardinality, id)),
1162            ConstructionFieldKind::Config
1163            | ConstructionFieldKind::Private
1164            | ConstructionFieldKind::Legacy => None,
1165        })
1166        .collect::<syn::Result<Vec<_>>>()?;
1167    let connect_ports = ports.iter().map(|(field, _, _)| {
1168        quote! { self.plugin.#field.connect(context.dependencies())?; }
1169    });
1170    let connect_tasks = task_connectors(&tasks);
1171    let requirement_parts = intersperse_commas(
1172        requirement_macros
1173            .into_iter()
1174            .chain(dependency_requirement_macros)
1175            .collect(),
1176    );
1177    let (prefix, after_schema, suffix, defaults) = descriptor_affixes(
1178        &plugin_id,
1179        &package_version,
1180        &root_slot,
1181        authoring_version,
1182        runtime_profile,
1183    );
1184    let construct_configuration = if let Some(config_type) = &config_type {
1185        let validate = attributes
1186            .validate
1187            .as_ref()
1188            .map(|path| quote!(#path(&configuration)?;));
1189        quote! {
1190            let configuration = #sdk::__private::serde_json::from_str::<#config_type>(context.configuration())
1191                .map_err(|error| #sdk::__private::RuntimeFailure::InvalidResolvedPlan {
1192                    detail: format!("invalid {} configuration: {error}", #plugin_id),
1193                })?;
1194            #validate
1195        }
1196    } else {
1197        if attributes.configuration_schema.is_some() {
1198            return Err(syn::Error::new_spanned(
1199                &plugin.ident,
1200                "`configuration_schema` requires a `#[config]` field",
1201            ));
1202        }
1203        if attributes.validate.is_some() {
1204            return Err(syn::Error::new_spanned(
1205                &plugin.ident,
1206                "`validate` requires a `#[config]` field",
1207            ));
1208        }
1209        if attributes.configuration_defaults.is_some() {
1210            return Err(syn::Error::new_spanned(
1211                &plugin.ident,
1212                "`configuration_defaults` requires a `#[config]` field",
1213            ));
1214        }
1215        quote! {
1216            let configuration = #sdk::__private::serde_json::from_str::<#sdk::__private::serde_json::Value>(context.configuration())
1217                .map_err(|error| #sdk::__private::RuntimeFailure::InvalidResolvedPlan {
1218                    detail: format!("invalid {} configuration: {error}", #plugin_id),
1219                })?;
1220            if !configuration.as_object().is_some_and(|object| object.is_empty()) {
1221                return Err(#sdk::__private::RuntimeFailure::InvalidResolvedPlan {
1222                    detail: format!("{} does not accept configuration", #plugin_id),
1223                });
1224            }
1225        }
1226    };
1227    if attributes.lifecycle
1228        && (attributes.prepare.is_some()
1229            || attributes.activate.is_some()
1230            || attributes.deactivate.is_some())
1231    {
1232        return Err(syn::Error::new_spanned(
1233            &plugin.ident,
1234            "`lifecycle` replaces the `prepare`, `activate`, and `deactivate` function attributes",
1235        ));
1236    }
1237    let prepare = if attributes.lifecycle {
1238        quote! {
1239            let plugin = self.plugin.clone();
1240            Box::pin(async move { #sdk::Lifecycle::prepare(plugin.as_ref(), context).await })
1241        }
1242    } else {
1243        hook(attributes.prepare.as_ref(), &sdk)
1244    };
1245    let activate_hook = if attributes.lifecycle {
1246        quote! {
1247            let plugin = self.plugin.clone();
1248            Box::pin(async move { #sdk::Lifecycle::activate(plugin.as_ref(), context).await })
1249        }
1250    } else {
1251        hook(attributes.activate.as_ref(), &sdk)
1252    };
1253    let deactivate_hook = if attributes.lifecycle {
1254        quote! {
1255            let plugin = self.plugin.clone();
1256            Box::pin(async move { #sdk::Lifecycle::deactivate(plugin.as_ref(), context).await })
1257        }
1258    } else {
1259        hook(attributes.deactivate.as_ref(), &sdk)
1260    };
1261    let disconnect_tasks = task_disconnectors(&tasks);
1262    let activate = if tasks.is_empty() {
1263        activate_hook
1264    } else {
1265        quote! {
1266            let plugin = self.plugin.clone();
1267            let activation = { #activate_hook };
1268            Box::pin(async move {
1269                let result = activation.await;
1270                if result.is_err() {
1271                    #(#disconnect_tasks)*
1272                }
1273                result
1274            })
1275        }
1276    };
1277    let disconnect_tasks = task_disconnectors(&tasks);
1278    let deactivate = if tasks.is_empty() {
1279        deactivate_hook
1280    } else {
1281        quote! {
1282            let plugin = self.plugin.clone();
1283            #(#disconnect_tasks)*
1284            let deactivation = { #deactivate_hook };
1285            deactivation
1286        }
1287    };
1288    let package_file_tracking = package_file_tracking([
1289        attributes.configuration_schema.as_ref(),
1290        attributes.configuration_defaults.as_ref(),
1291    ]);
1292    let consumer_finalizer = if attributes.consumer {
1293        let generated_plugin = format_ident!("__lenso_consumer_{}", snake(&name.to_string()));
1294        let link_function = format_ident!("__lenso_link_{}", snake(&name.to_string()));
1295        let artifact = format_ident!("__LENSO_PLUGIN_DESCRIPTOR_ARTIFACT_{name}");
1296        Some(quote! {
1297            /// Generated package-owned Plugin Descriptor bytes.
1298            pub const PLUGIN_DESCRIPTOR_JSON: &str = #descriptor_macro!();
1299            #[doc(hidden)]
1300            const __LENSO_PLUGIN_DESCRIPTOR_ARTIFACT_TEXT: &str = concat!(
1301                "LENSO_PLUGIN_DESCRIPTOR_V1\0",
1302                #descriptor_macro!(),
1303                "\0END_LENSO_PLUGIN_DESCRIPTOR_V1",
1304            );
1305            /// Linker-retained descriptor artifact consumed without executing package code.
1306            #[doc(hidden)]
1307            #[used]
1308            pub static #artifact: &[u8] = __LENSO_PLUGIN_DESCRIPTOR_ARTIFACT_TEXT.as_bytes();
1309
1310            #[doc(hidden)]
1311            mod #generated_plugin {
1312                #[derive(Clone, Copy, Debug, Default)]
1313                struct Factory;
1314
1315                impl #sdk::__private::NativePluginFactory for Factory {
1316                    fn package_id(&self) -> &'static str { super::PACKAGE_ID }
1317                    fn package_version(&self) -> &'static str { super::PACKAGE_VERSION }
1318                    fn runtime_profile(&self) -> &'static str {
1319                        super::#name::__LENSO_RUNTIME_PROFILE
1320                    }
1321
1322                    fn instantiate(
1323                        &self,
1324                        context: #sdk::__private::NativePluginFactoryContext<'_>,
1325                    ) -> Result<
1326                        #sdk::__private::NativePluginInstance,
1327                        #sdk::__private::RuntimeFailure,
1328                    > {
1329                        if super::#name::__LENSO_AUTHORING_VERSION == 2 {
1330                            let object = #sdk::__private::PluginObject::<super::#name>::empty();
1331                            let lifecycle = #sdk::__private::CompleteObjectLifecycle::linked(
1332                                object,
1333                                context.configuration(),
1334                            )?;
1335                            return Ok(#sdk::__private::NativePluginInstance::with_lifecycle(
1336                                Vec::new(),
1337                                lifecycle,
1338                            ));
1339                        }
1340                        let plugin = ::std::rc::Rc::new(super::#name::__lenso_construct(context)?);
1341                        let lifecycle = super::#lifecycle_name { plugin };
1342                        Ok(#sdk::__private::NativePluginInstance::with_lifecycle(
1343                            Vec::new(),
1344                            lifecycle,
1345                        ))
1346                    }
1347                }
1348
1349                pub(super) fn factory() -> ::std::rc::Rc<dyn #sdk::__private::NativePluginFactory> {
1350                    ::std::rc::Rc::new(Factory)
1351                }
1352
1353                #sdk::__private::__inventory::submit! {
1354                    #sdk::__private::LinkedNativePluginFactory::new(
1355                        factory,
1356                        super::PLUGIN_DESCRIPTOR_JSON,
1357                    )
1358                }
1359            }
1360
1361            /// Explicit Host linkage anchor generated for this Plugin factory.
1362            #[doc(hidden)]
1363            pub fn #link_function() {
1364                #sdk::__private::link_native_plugin(
1365                    #sdk::__private::LinkedNativePluginFactory::new(
1366                        #generated_plugin::factory,
1367                        PLUGIN_DESCRIPTOR_JSON,
1368                    ),
1369                );
1370            }
1371        })
1372    } else {
1373        None
1374    };
1375
1376    Ok(quote! {
1377        /// Runtime package identity derived from Cargo package metadata.
1378        pub const PACKAGE_ID: &str = #plugin_id;
1379        /// Exact linked Cargo package version.
1380        pub const PACKAGE_VERSION: &str = env!("CARGO_PKG_VERSION");
1381        /// Exact Host Build identity for this linked package.
1382        pub const FACTORY_IDENTITY: &str = concat!(#plugin_id, "@", env!("CARGO_PKG_VERSION"));
1383
1384        #plugin
1385
1386        #[doc(hidden)]
1387        struct #inputs_name {
1388            #(#input_fields),*
1389        }
1390
1391        #[doc(hidden)]
1392        macro_rules! #descriptor_macro {
1393            () => {
1394                concat!(#prefix, #schema, ",\"configuration_defaults\":", #configuration_defaults, #after_schema, #suffix #(, #requirement_parts)*, #defaults)
1395            };
1396            ($first:expr $(, $rest:expr)*) => {
1397                concat!(#prefix, #schema, ",\"configuration_defaults\":", #configuration_defaults, #after_schema, $first $(, ",", $rest)*, #suffix #(, #requirement_parts)*, #defaults)
1398            };
1399        }
1400
1401        impl #name {
1402            #[doc(hidden)]
1403            const __LENSO_AUTHORING_VERSION: u32 = #authoring_version;
1404            #[doc(hidden)]
1405            const __LENSO_RUNTIME_PROFILE: &'static str = #runtime_profile;
1406
1407            #[doc(hidden)]
1408            fn __lenso_inputs(
1409                context: &#sdk::__private::ConstructionContext,
1410            ) -> Result<#inputs_name, #sdk::__private::RuntimeFailure> {
1411                #v2_configuration
1412                Ok(#inputs_name { #(#input_initializers),* })
1413            }
1414
1415            #[doc(hidden)]
1416            #[allow(unreachable_code)]
1417            fn __lenso_construct(
1418                context: #sdk::__private::NativePluginFactoryContext<'_>,
1419            ) -> Result<Self, #sdk::__private::RuntimeFailure> {
1420                if context.entrypoint() != "default" {
1421                    return Err(#sdk::__private::RuntimeFailure::InvalidResolvedPlan {
1422                        detail: format!("unsupported {} entrypoint {}", #plugin_id, context.entrypoint()),
1423                    });
1424                }
1425                #construct_configuration
1426                Ok(Self { #(#initializers),* })
1427            }
1428
1429            #[doc(hidden)]
1430            fn __lenso_auto_construct(
1431                context: #sdk::__private::ConstructionContext,
1432            ) -> #sdk::__private::ErasedConstructionFuture {
1433                Box::pin(async move {
1434                    let _ = context;
1435                    #v2_construct
1436                })
1437            }
1438        }
1439
1440        #[doc(hidden)]
1441        mod #construction_module {
1442            fn plugin_type() -> ::std::any::TypeId {
1443                ::std::any::TypeId::of::<super::#name>()
1444            }
1445
1446            #sdk::__private::__inventory::submit! {
1447                #sdk::__private::LinkedPluginConstruction::new(
1448                    plugin_type,
1449                    false,
1450                    super::#name::__lenso_auto_construct,
1451                    None,
1452                )
1453            }
1454        }
1455
1456        #[doc(hidden)]
1457        #[derive(Clone, Debug)]
1458        struct #lifecycle_name {
1459            plugin: ::std::rc::Rc<#name>,
1460        }
1461
1462        impl #sdk::__private::PluginLifecycle for #lifecycle_name {
1463            fn prepare(&self, context: #sdk::__private::PrepareContext) -> #sdk::__private::PluginFuture {
1464                #prepare
1465            }
1466
1467            fn activate(&self, context: #sdk::__private::ActivateContext) -> #sdk::__private::PluginFuture {
1468                let connected = (|| -> Result<(), #sdk::__private::RuntimeFailure> {
1469                    #(#connect_ports)*
1470                    #(#connect_tasks)*
1471                    Ok(())
1472                })();
1473                if let Err(error) = connected {
1474                    return Box::pin(#sdk::__private::futures::future::ready(Err(error)));
1475                }
1476                #activate
1477            }
1478
1479            fn deactivate(&self, context: #sdk::__private::DeactivateContext) -> #sdk::__private::PluginFuture {
1480                #deactivate
1481            }
1482        }
1483
1484        const _: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/Cargo.toml"));
1485        #(#package_file_tracking)*
1486
1487        #consumer_finalizer
1488    })
1489}
1490
1491fn descriptor_affixes(
1492    plugin_id: &str,
1493    package_version: &str,
1494    root_slot: &str,
1495    authoring_version: u32,
1496    runtime_profile: &str,
1497) -> (String, &'static str, &'static str, &'static str) {
1498    let prefix = format!(
1499        "{{\"authoring_version\":{authoring_version},\"runtime_profile\":{},\"plugin_id\":{},\"release_version\":{},\"root_slot\":{},\"runtime_package_id\":{},\"runtime_package_revision\":{},\"entrypoint\":\"default\",\"configuration_schema\":",
1500        serde_json::to_string(runtime_profile).expect("runtime profile serializes"),
1501        serde_json::to_string(plugin_id).expect("Plugin ID serializes"),
1502        serde_json::to_string(package_version).expect("package version serializes"),
1503        serde_json::to_string(root_slot).expect("root Slot serializes"),
1504        serde_json::to_string(plugin_id).expect("runtime package ID serializes"),
1505        serde_json::to_string(package_version).expect("package version serializes"),
1506    );
1507    let after_schema = ",\"provided_capabilities\":[";
1508    let suffix = "],\"required_capabilities\":[";
1509    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\"}";
1510    (prefix, after_schema, suffix, defaults)
1511}
1512
1513fn package_file_tracking<'a>(
1514    paths: impl IntoIterator<Item = Option<&'a LitStr>>,
1515) -> Vec<proc_macro2::TokenStream> {
1516    paths
1517        .into_iter()
1518        .flatten()
1519        .map(|path| {
1520            quote!(
1521                const _: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/", #path));
1522            )
1523        })
1524        .collect()
1525}
1526
1527fn configuration_schema_tokens(
1528    schema_path: Option<&LitStr>,
1529    config_type: Option<&Type>,
1530) -> syn::Result<proc_macro2::TokenStream> {
1531    if let Some(path) = schema_path {
1532        let schema = canonical_json(&read_configuration_schema(path)?);
1533        return Ok(quote!(#schema));
1534    }
1535    let Some(config_type) = config_type else {
1536        let schema = canonical_json(&json!({
1537            "$schema": "https://json-schema.org/draft/2020-12/schema",
1538            "type": "object",
1539            "additionalProperties": false,
1540            "required": [],
1541            "properties": {},
1542        }));
1543        return Ok(quote!(#schema));
1544    };
1545    let Type::Path(config) = config_type else {
1546        return Err(syn::Error::new_spanned(
1547            config_type,
1548            "the `#[config]` field type must be a path",
1549        ));
1550    };
1551    let mut namespace = config.path.clone();
1552    let config_name = namespace
1553        .segments
1554        .pop()
1555        .expect("type paths are non-empty")
1556        .into_value()
1557        .ident;
1558    namespace.segments.pop_punct();
1559    let macro_name = format_ident!("__lenso_config_schema_{}", snake(&config_name.to_string()));
1560    if namespace.segments.is_empty() {
1561        Ok(quote!(#macro_name!()))
1562    } else {
1563        Ok(quote!(#namespace::#macro_name!()))
1564    }
1565}
1566
1567fn configuration_defaults_tokens(
1568    schema_path: Option<&LitStr>,
1569    defaults_path: Option<&LitStr>,
1570    config_type: Option<&Type>,
1571) -> syn::Result<proc_macro2::TokenStream> {
1572    if let Some(path) = defaults_path {
1573        if schema_path.is_none() {
1574            return Err(syn::Error::new(
1575                path.span(),
1576                "`configuration_defaults` requires an explicit `configuration_schema`",
1577            ));
1578        }
1579        let defaults = read_configuration_defaults(path)?;
1580        let schema = read_configuration_schema(schema_path.expect("checked above"))?;
1581        validate_configuration_defaults(&defaults, &schema).map_err(|detail| {
1582            syn::Error::new(
1583                path.span(),
1584                format!("invalid package configuration defaults: {detail}"),
1585            )
1586        })?;
1587        let defaults = canonical_json(&defaults);
1588        return Ok(quote!(#defaults));
1589    }
1590    if schema_path.is_some() || config_type.is_none() {
1591        let defaults = canonical_json(&json!({}));
1592        return Ok(quote!(#defaults));
1593    }
1594    let config_type = config_type.expect("checked above");
1595    let Type::Path(config) = config_type else {
1596        return Err(syn::Error::new_spanned(
1597            config_type,
1598            "the `#[config]` field type must be a path",
1599        ));
1600    };
1601    let mut namespace = config.path.clone();
1602    let config_name = namespace
1603        .segments
1604        .pop()
1605        .expect("type paths are non-empty")
1606        .into_value()
1607        .ident;
1608    namespace.segments.pop_punct();
1609    let macro_name = format_ident!(
1610        "__lenso_config_defaults_{}",
1611        snake(&config_name.to_string())
1612    );
1613    if namespace.segments.is_empty() {
1614        Ok(quote!(#macro_name!()))
1615    } else {
1616        Ok(quote!(#namespace::#macro_name!()))
1617    }
1618}
1619
1620struct StructFields {
1621    config_type: Option<Type>,
1622    ports: Vec<(syn::Ident, Path, PortCardinality)>,
1623    tasks: Vec<syn::Ident>,
1624    initializers: Vec<proc_macro2::TokenStream>,
1625    construction_fields: Vec<ConstructionField>,
1626}
1627
1628struct ConstructionField {
1629    name: syn::Ident,
1630    ty: Type,
1631    kind: ConstructionFieldKind,
1632}
1633
1634enum ConstructionFieldKind {
1635    Config,
1636    Dependency {
1637        id: LitStr,
1638        client: Box<Type>,
1639        cardinality: DependencyCardinality,
1640    },
1641    Private,
1642    Legacy,
1643}
1644
1645#[derive(Clone, Copy)]
1646enum DependencyCardinality {
1647    One,
1648    Optional,
1649    Many,
1650}
1651
1652#[derive(Clone, Copy)]
1653enum PortCardinality {
1654    One,
1655    Many,
1656}
1657
1658#[allow(clippy::too_many_lines)]
1659fn analyze_struct_fields(
1660    plugin: &mut ItemStruct,
1661    sdk: &proc_macro2::TokenStream,
1662) -> syn::Result<StructFields> {
1663    let Fields::Named(fields) = &mut plugin.fields else {
1664        return Err(syn::Error::new_spanned(
1665            &plugin.fields,
1666            "a struct-level Plugin requires named fields",
1667        ));
1668    };
1669    let mut config = None;
1670    let mut ports = Vec::new();
1671    let mut tasks = Vec::new();
1672    let mut resources = None;
1673    let mut initializers = Vec::new();
1674    let mut construction_fields = Vec::new();
1675    for field in &mut fields.named {
1676        let name = field.ident.as_ref().expect("named fields have identifiers");
1677        let is_config = take_marker(&mut field.attrs, "config");
1678        let is_tasks = take_marker(&mut field.attrs, "tasks");
1679        let is_resources = take_marker(&mut field.attrs, "resources");
1680        let dependency = take_dependency(&mut field.attrs)?;
1681        if usize::from(is_config)
1682            + usize::from(is_tasks)
1683            + usize::from(is_resources)
1684            + usize::from(dependency.is_some())
1685            > 1
1686        {
1687            return Err(syn::Error::new_spanned(
1688                field,
1689                "a Plugin field can have only one construction marker",
1690            ));
1691        }
1692        if is_config {
1693            if config.replace(field.ty.clone()).is_some() {
1694                return Err(syn::Error::new_spanned(
1695                    field,
1696                    "a Plugin has exactly one `#[config]` field",
1697                ));
1698            }
1699            initializers.push(quote!(#name: configuration));
1700            construction_fields.push(ConstructionField {
1701                name: name.clone(),
1702                ty: field.ty.clone(),
1703                kind: ConstructionFieldKind::Config,
1704            });
1705        } else if let Some(id) = dependency {
1706            let (client, cardinality) = dependency_client(&field.ty)?;
1707            initializers.push(quote! {
1708                #name: return Err(#sdk::__private::RuntimeFailure::InvalidResolvedPlan {
1709                    detail: concat!("dependency field `", stringify!(#name), "` requires authoring version 2").to_owned(),
1710                })
1711            });
1712            construction_fields.push(ConstructionField {
1713                name: name.clone(),
1714                ty: field.ty.clone(),
1715                kind: ConstructionFieldKind::Dependency {
1716                    id,
1717                    client: Box::new(client),
1718                    cardinality,
1719                },
1720            });
1721        } else if is_tasks {
1722            if !is_named_type(&field.ty, "ManagedTasks") {
1723                return Err(syn::Error::new_spanned(
1724                    &field.ty,
1725                    "a `#[tasks]` field must have type `ManagedTasks`",
1726                ));
1727            }
1728            if !tasks.is_empty() {
1729                return Err(syn::Error::new_spanned(
1730                    field,
1731                    "a Plugin has at most one `#[tasks]` field",
1732                ));
1733            }
1734            tasks.push(name.clone());
1735            initializers.push(quote!(#name: ::core::default::Default::default()));
1736            construction_fields.push(ConstructionField {
1737                name: name.clone(),
1738                ty: field.ty.clone(),
1739                kind: ConstructionFieldKind::Legacy,
1740            });
1741        } else if is_resources {
1742            if !is_named_type(&field.ty, "InstanceResources") {
1743                return Err(syn::Error::new_spanned(
1744                    &field.ty,
1745                    "a `#[resources]` field must have type `InstanceResources`",
1746                ));
1747            }
1748            if resources.replace(name.clone()).is_some() {
1749                return Err(syn::Error::new_spanned(
1750                    field,
1751                    "a Plugin has at most one `#[resources]` field",
1752                ));
1753            }
1754            initializers.push(quote!(#name: context.resources().clone()));
1755            construction_fields.push(ConstructionField {
1756                name: name.clone(),
1757                ty: field.ty.clone(),
1758                kind: ConstructionFieldKind::Legacy,
1759            });
1760        } else if let Some((client, cardinality)) = port_client(&field.ty)? {
1761            ports.push((name.clone(), client, cardinality));
1762            initializers.push(quote!(#name: ::core::default::Default::default()));
1763            construction_fields.push(ConstructionField {
1764                name: name.clone(),
1765                ty: field.ty.clone(),
1766                kind: ConstructionFieldKind::Legacy,
1767            });
1768        } else {
1769            initializers.push(legacy_default_initializer(name, &field.ty, sdk));
1770            construction_fields.push(ConstructionField {
1771                name: name.clone(),
1772                ty: field.ty.clone(),
1773                kind: ConstructionFieldKind::Private,
1774            });
1775        }
1776    }
1777    Ok(StructFields {
1778        config_type: config,
1779        ports,
1780        tasks,
1781        initializers,
1782        construction_fields,
1783    })
1784}
1785
1786fn take_dependency(attributes: &mut Vec<Attribute>) -> syn::Result<Option<LitStr>> {
1787    let mut id = None;
1788    let mut seen = false;
1789    let mut retained = Vec::with_capacity(attributes.len());
1790    for attribute in attributes.drain(..) {
1791        if !attribute.path().is_ident("dependency") {
1792            retained.push(attribute);
1793            continue;
1794        }
1795        if seen {
1796            return Err(syn::Error::new_spanned(
1797                attribute,
1798                "duplicate `dependency` marker",
1799            ));
1800        }
1801        seen = true;
1802        attribute.parse_nested_meta(|meta| {
1803            if !meta.path.is_ident("id") {
1804                return Err(meta.error("expected `id = \"public_requirement_id\"`"));
1805            }
1806            id = Some(meta.value()?.parse()?);
1807            Ok(())
1808        })?;
1809    }
1810    *attributes = retained;
1811    if seen {
1812        id.map(Some).ok_or_else(|| {
1813            syn::Error::new(proc_macro2::Span::call_site(), "dependency id is required")
1814        })
1815    } else {
1816        Ok(None)
1817    }
1818}
1819
1820fn dependency_client(ty: &Type) -> syn::Result<(Type, DependencyCardinality)> {
1821    let Type::Path(path) = ty else {
1822        return Err(syn::Error::new_spanned(
1823            ty,
1824            "dependency type must be a generated client",
1825        ));
1826    };
1827    let segment = path.path.segments.last().expect("type paths are non-empty");
1828    if segment.ident == "Option" {
1829        return Ok((
1830            single_type_argument(segment, ty)?.clone(),
1831            DependencyCardinality::Optional,
1832        ));
1833    }
1834    if segment.ident == "Vec" {
1835        let bound = single_type_argument(segment, ty)?;
1836        let Type::Path(bound_path) = bound else {
1837            return Err(syn::Error::new_spanned(
1838                bound,
1839                "many dependency must contain a generated client",
1840            ));
1841        };
1842        let bound_segment = bound_path
1843            .path
1844            .segments
1845            .last()
1846            .expect("type paths are non-empty");
1847        if bound_segment.ident != "BoundCapabilityClient" {
1848            return Err(syn::Error::new_spanned(
1849                bound,
1850                "many dependency must be `Vec<BoundCapabilityClient<Client>>`",
1851            ));
1852        }
1853        return Ok((
1854            single_type_argument(bound_segment, bound)?.clone(),
1855            DependencyCardinality::Many,
1856        ));
1857    }
1858    Ok((ty.clone(), DependencyCardinality::One))
1859}
1860
1861fn single_type_argument<'a>(segment: &'a syn::PathSegment, ty: &Type) -> syn::Result<&'a Type> {
1862    let PathArguments::AngleBracketed(arguments) = &segment.arguments else {
1863        return Err(syn::Error::new_spanned(
1864            ty,
1865            "dependency wrapper requires one type",
1866        ));
1867    };
1868    let [GenericArgument::Type(inner)] = arguments.args.iter().collect::<Vec<_>>().as_slice()
1869    else {
1870        return Err(syn::Error::new_spanned(
1871            ty,
1872            "dependency wrapper requires one type",
1873        ));
1874    };
1875    Ok(inner)
1876}
1877
1878fn legacy_default_initializer(
1879    name: &syn::Ident,
1880    ty: &Type,
1881    sdk: &proc_macro2::TokenStream,
1882) -> proc_macro2::TokenStream {
1883    quote! {
1884        #name: {
1885            trait __LensoMaybeDefault<T> {
1886                fn __lenso_default(self) -> Option<T>;
1887            }
1888            impl<T: Default> __LensoMaybeDefault<T> for &&::std::marker::PhantomData<T> {
1889                fn __lenso_default(self) -> Option<T> {
1890                    Some(T::default())
1891                }
1892            }
1893            impl<T> __LensoMaybeDefault<T> for &::std::marker::PhantomData<T> {
1894                fn __lenso_default(self) -> Option<T> {
1895                    None
1896                }
1897            }
1898            let marker = ::std::marker::PhantomData::<#ty>;
1899            (&&marker).__lenso_default().ok_or_else(|| {
1900                #sdk::__private::RuntimeFailure::InvalidResolvedPlan {
1901                    detail: concat!(
1902                        "Plugin field `",
1903                        stringify!(#name),
1904                        "` has no default; use authoring version 2 with #[create]",
1905                    )
1906                    .to_owned(),
1907                }
1908            })?
1909        }
1910    }
1911}
1912
1913fn construct_v2_configuration(
1914    plugin_id: &str,
1915    config_type: Option<&Type>,
1916    validate: Option<&Path>,
1917    sdk: &proc_macro2::TokenStream,
1918) -> proc_macro2::TokenStream {
1919    if let Some(config_type) = config_type {
1920        let validate = validate.map(|path| quote!(#path(&configuration)?;));
1921        quote! {
1922            let configuration = #sdk::__private::serde_json::from_str::<#config_type>(
1923                context.configuration(),
1924            )
1925            .map_err(|error| #sdk::__private::RuntimeFailure::InvalidResolvedPlan {
1926                detail: format!("invalid {} configuration: {error}", #plugin_id),
1927            })?;
1928            #validate
1929        }
1930    } else {
1931        quote! {
1932            let configuration = #sdk::__private::serde_json::from_str::<
1933                #sdk::__private::serde_json::Value,
1934            >(context.configuration())
1935            .map_err(|error| #sdk::__private::RuntimeFailure::InvalidResolvedPlan {
1936                detail: format!("invalid {} configuration: {error}", #plugin_id),
1937            })?;
1938            if !configuration.as_object().is_some_and(|object| object.is_empty()) {
1939                return Err(#sdk::__private::RuntimeFailure::InvalidResolvedPlan {
1940                    detail: format!("{} does not accept configuration", #plugin_id),
1941                });
1942            }
1943        }
1944    }
1945}
1946
1947fn v2_field_initializer(
1948    field: &ConstructionField,
1949    sdk: &proc_macro2::TokenStream,
1950) -> proc_macro2::TokenStream {
1951    let name = &field.name;
1952    let ty = &field.ty;
1953    match &field.kind {
1954        ConstructionFieldKind::Config => quote!(#name: configuration),
1955        ConstructionFieldKind::Dependency {
1956            id,
1957            client,
1958            cardinality: DependencyCardinality::One,
1959        } => quote! {
1960            #name: {
1961                let dependency = context.dependencies().requirement(#id)?;
1962                <#client as #sdk::__private::CapabilityClient>::from_dependencies(&dependency)?
1963            }
1964        },
1965        ConstructionFieldKind::Dependency {
1966            id,
1967            client,
1968            cardinality: DependencyCardinality::Optional,
1969        } => quote! {
1970            #name: {
1971                let dependency = context.dependencies().requirement(#id)?;
1972                if dependency.bindings().is_empty() {
1973                    None
1974                } else {
1975                    Some(<#client as #sdk::__private::CapabilityClient>::from_dependencies(
1976                        &dependency,
1977                    )?)
1978                }
1979            }
1980        },
1981        ConstructionFieldKind::Dependency {
1982            id,
1983            client,
1984            cardinality: DependencyCardinality::Many,
1985        } => quote! {
1986            #name: {
1987                let dependency = context.dependencies().requirement(#id)?;
1988                <#client as #sdk::__private::CapabilityClientMany>::many_from_dependencies(
1989                    &dependency,
1990                )?
1991            }
1992        },
1993        ConstructionFieldKind::Private => quote! {
1994            #name: {
1995                trait __LensoMaybeDefault<T> {
1996                    fn __lenso_default(self) -> Option<T>;
1997                }
1998                impl<T: Default> __LensoMaybeDefault<T> for &&::std::marker::PhantomData<T> {
1999                    fn __lenso_default(self) -> Option<T> {
2000                        Some(T::default())
2001                    }
2002                }
2003                impl<T> __LensoMaybeDefault<T> for &::std::marker::PhantomData<T> {
2004                    fn __lenso_default(self) -> Option<T> {
2005                        None
2006                    }
2007                }
2008                let marker = ::std::marker::PhantomData::<#ty>;
2009                (&&marker).__lenso_default().ok_or_else(|| {
2010                    #sdk::__private::RuntimeFailure::InvalidResolvedPlan {
2011                        detail: concat!(
2012                            "Plugin field `",
2013                            stringify!(#name),
2014                            "` has no default; add a #[create] constructor",
2015                        )
2016                        .to_owned(),
2017                    }
2018                })?
2019            }
2020        },
2021        ConstructionFieldKind::Legacy => quote! {
2022            #name: return Err(#sdk::__private::RuntimeFailure::InvalidResolvedPlan {
2023                detail: concat!(
2024                    "legacy Plugin field `",
2025                    stringify!(#name),
2026                    "` cannot use authoring version 2",
2027                )
2028                .to_owned(),
2029            })
2030        },
2031    }
2032}
2033
2034fn v2_input_initializer(
2035    field: &ConstructionField,
2036    sdk: &proc_macro2::TokenStream,
2037) -> Option<proc_macro2::TokenStream> {
2038    let name = &field.name;
2039    match &field.kind {
2040        ConstructionFieldKind::Config => Some(quote!(#name: configuration)),
2041        ConstructionFieldKind::Dependency {
2042            id,
2043            client,
2044            cardinality: DependencyCardinality::One,
2045        } => Some(quote! {
2046            #name: {
2047                let dependency = context.dependencies().requirement(#id)?;
2048                <#client as #sdk::__private::CapabilityClient>::from_dependencies(&dependency)?
2049            }
2050        }),
2051        ConstructionFieldKind::Dependency {
2052            id,
2053            client,
2054            cardinality: DependencyCardinality::Optional,
2055        } => Some(quote! {
2056            #name: {
2057                let dependency = context.dependencies().requirement(#id)?;
2058                if dependency.bindings().is_empty() {
2059                    None
2060                } else {
2061                    Some(<#client as #sdk::__private::CapabilityClient>::from_dependencies(
2062                        &dependency,
2063                    )?)
2064                }
2065            }
2066        }),
2067        ConstructionFieldKind::Dependency {
2068            id,
2069            client,
2070            cardinality: DependencyCardinality::Many,
2071        } => Some(quote! {
2072            #name: {
2073                let dependency = context.dependencies().requirement(#id)?;
2074                <#client as #sdk::__private::CapabilityClientMany>::many_from_dependencies(
2075                    &dependency,
2076                )?
2077            }
2078        }),
2079        ConstructionFieldKind::Private | ConstructionFieldKind::Legacy => None,
2080    }
2081}
2082
2083fn is_named_type(ty: &Type, expected: &str) -> bool {
2084    let Type::Path(path) = ty else {
2085        return false;
2086    };
2087    path.path
2088        .segments
2089        .last()
2090        .is_some_and(|segment| segment.ident == expected && segment.arguments.is_empty())
2091}
2092
2093fn take_marker(attributes: &mut Vec<Attribute>, name: &str) -> bool {
2094    let present = attributes
2095        .iter()
2096        .any(|attribute| attribute.path().is_ident(name));
2097    attributes.retain(|attribute| !attribute.path().is_ident(name));
2098    present
2099}
2100
2101fn task_connectors(tasks: &[syn::Ident]) -> Vec<proc_macro2::TokenStream> {
2102    tasks
2103        .iter()
2104        .map(|field| {
2105            quote! { self.plugin.#field.__lenso_connect(context.tasks().clone())?; }
2106        })
2107        .collect()
2108}
2109
2110fn task_disconnectors(tasks: &[syn::Ident]) -> Vec<proc_macro2::TokenStream> {
2111    tasks
2112        .iter()
2113        .map(|field| quote! { plugin.#field.__lenso_disconnect(); })
2114        .collect()
2115}
2116
2117fn port_client(ty: &Type) -> syn::Result<Option<(Path, PortCardinality)>> {
2118    let Type::Path(path) = ty else {
2119        return Ok(None);
2120    };
2121    let Some(segment) = path.path.segments.last() else {
2122        return Ok(None);
2123    };
2124    let cardinality = if segment.ident == "Port" {
2125        PortCardinality::One
2126    } else if segment.ident == "ManyPort" {
2127        PortCardinality::Many
2128    } else {
2129        return Ok(None);
2130    };
2131    let syn::PathArguments::AngleBracketed(arguments) = &segment.arguments else {
2132        return Err(syn::Error::new_spanned(
2133            ty,
2134            "Port or ManyPort requires one Capability client type",
2135        ));
2136    };
2137    let Some(syn::GenericArgument::Type(Type::Path(client))) = arguments.args.first() else {
2138        return Err(syn::Error::new_spanned(
2139            ty,
2140            "Port or ManyPort requires one Capability client type",
2141        ));
2142    };
2143    if arguments.args.len() != 1 {
2144        return Err(syn::Error::new_spanned(
2145            ty,
2146            "Port or ManyPort requires one Capability client type",
2147        ));
2148    }
2149    Ok(Some((client.path.clone(), cardinality)))
2150}
2151
2152fn requirement_macro(
2153    client: &Path,
2154    cardinality: PortCardinality,
2155) -> syn::Result<proc_macro2::TokenStream> {
2156    let prefix = match cardinality {
2157        PortCardinality::One => "__lenso_required_",
2158        PortCardinality::Many => "__lenso_required_many_",
2159    };
2160    requirement_macro_path(client, prefix, None)
2161}
2162
2163fn named_requirement_macro(
2164    client: &Type,
2165    cardinality: DependencyCardinality,
2166    requirement_id: &LitStr,
2167) -> syn::Result<proc_macro2::TokenStream> {
2168    let Type::Path(client) = client else {
2169        return Err(syn::Error::new_spanned(
2170            client,
2171            "dependency client must be a namespace-qualified generated client",
2172        ));
2173    };
2174    let prefix = match cardinality {
2175        DependencyCardinality::One => "__lenso_required_",
2176        DependencyCardinality::Optional => "__lenso_required_optional_",
2177        DependencyCardinality::Many => "__lenso_required_many_",
2178    };
2179    requirement_macro_path(&client.path, prefix, Some(requirement_id))
2180}
2181
2182fn requirement_macro_path(
2183    client: &Path,
2184    prefix: &str,
2185    requirement_id: Option<&LitStr>,
2186) -> syn::Result<proc_macro2::TokenStream> {
2187    if client.segments.len() < 2 {
2188        return Err(syn::Error::new_spanned(
2189            client,
2190            "a Capability client must be namespace-qualified, for example `model::ModelClient`",
2191        ));
2192    }
2193    let mut namespace = client.clone();
2194    let client_name = namespace
2195        .segments
2196        .pop()
2197        .expect("checked length")
2198        .into_value()
2199        .ident;
2200    namespace.segments.pop_punct();
2201    let macro_name = format_ident!("{}{}", prefix, snake(&client_name.to_string()));
2202    Ok(requirement_id.map_or_else(
2203        || quote!(#namespace::#macro_name!()),
2204        |requirement_id| quote!(#namespace::#macro_name!(#requirement_id)),
2205    ))
2206}
2207
2208fn intersperse_commas(values: Vec<proc_macro2::TokenStream>) -> Vec<proc_macro2::TokenStream> {
2209    values
2210        .into_iter()
2211        .enumerate()
2212        .flat_map(|(index, value)| {
2213            if index == 0 {
2214                vec![value]
2215            } else {
2216                vec![quote!(","), value]
2217            }
2218        })
2219        .collect()
2220}
2221
2222fn hook(path: Option<&Path>, sdk: &proc_macro2::TokenStream) -> proc_macro2::TokenStream {
2223    path.map_or_else(
2224        || quote!(Box::pin(#sdk::__private::futures::future::ready(Ok(())))),
2225        |path| quote!(#path(&self.plugin, &context)),
2226    )
2227}
2228
2229fn canonical_json(value: &Value) -> String {
2230    serde_json::to_string(value).expect("JSON values serialize")
2231}
2232
2233fn authoring_crate() -> proc_macro2::TokenStream {
2234    for package in ["lenso", "lenso-native-adapter"] {
2235        match crate_name(package) {
2236            Ok(FoundCrate::Itself) => {
2237                let ident = format_ident!("{}", package.replace('-', "_"));
2238                return quote!(::#ident);
2239            }
2240            Ok(FoundCrate::Name(name)) => {
2241                let ident = format_ident!("{name}");
2242                return quote!(::#ident);
2243            }
2244            Err(_) => {}
2245        }
2246    }
2247    quote!(::lenso_native_adapter)
2248}
2249
2250fn snake(value: &str) -> String {
2251    let mut output = String::new();
2252    for (index, character) in value.chars().enumerate() {
2253        if character.is_ascii_uppercase() && index > 0 {
2254            output.push('_');
2255        }
2256        output.push(character.to_ascii_lowercase());
2257    }
2258    output
2259}
2260
2261fn plugin_descriptor(
2262    plugin_id: &str,
2263    root_slot: &str,
2264    descriptor: &LitStr,
2265    configuration_schema: Option<&LitStr>,
2266    configuration_defaults: Option<&LitStr>,
2267) -> syn::Result<String> {
2268    let supplied: Value = serde_json::from_str(&descriptor.value()).map_err(|error| {
2269        syn::Error::new(
2270            descriptor.span(),
2271            format!("Plugin Descriptor input is not valid JSON: {error}"),
2272        )
2273    })?;
2274    let mut supplied = supplied.as_object().cloned().ok_or_else(|| {
2275        syn::Error::new(
2276            descriptor.span(),
2277            "Plugin Descriptor input must be an object",
2278        )
2279    })?;
2280    if supplied.contains_key("configuration_schema") {
2281        return Err(syn::Error::new(
2282            descriptor.span(),
2283            "Plugin Descriptor input cannot contain `configuration_schema`; use the package-owned schema path attribute",
2284        ));
2285    }
2286    if supplied.contains_key("configuration_defaults") {
2287        return Err(syn::Error::new(
2288            descriptor.span(),
2289            "Plugin Descriptor input cannot contain `configuration_defaults`; use the package-owned defaults path attribute",
2290        ));
2291    }
2292    if let Some(schema_path) = configuration_schema {
2293        supplied.insert(
2294            "configuration_schema".to_owned(),
2295            read_configuration_schema(schema_path)?,
2296        );
2297    }
2298    if let Some(defaults_path) = configuration_defaults {
2299        if configuration_schema.is_none() {
2300            return Err(syn::Error::new(
2301                defaults_path.span(),
2302                "`configuration_defaults` requires `configuration_schema`",
2303            ));
2304        }
2305        let defaults = read_configuration_defaults(defaults_path)?;
2306        let schema = supplied
2307            .get("configuration_schema")
2308            .expect("explicit configuration Schema was inserted above");
2309        validate_configuration_defaults(&defaults, schema).map_err(|detail| {
2310            syn::Error::new(
2311                defaults_path.span(),
2312                format!("invalid package configuration defaults: {detail}"),
2313            )
2314        })?;
2315        supplied.insert("configuration_defaults".to_owned(), defaults);
2316    }
2317    for owned in [
2318        "plugin_id",
2319        "release_version",
2320        "root_slot",
2321        "runtime_package_id",
2322        "runtime_package_revision",
2323        "entrypoint",
2324        "execution_class",
2325        "restart_policy",
2326        "criticality",
2327    ] {
2328        if supplied.contains_key(owned) {
2329            return Err(syn::Error::new(
2330                descriptor.span(),
2331                format!("Plugin Descriptor input cannot override generated field `{owned}`"),
2332            ));
2333        }
2334    }
2335    let package_version = env::var("CARGO_PKG_VERSION").map_err(|_| {
2336        syn::Error::new(
2337            descriptor.span(),
2338            "CARGO_PKG_VERSION is unavailable while deriving Plugin Descriptor",
2339        )
2340    })?;
2341    Ok(complete_plugin_descriptor(
2342        plugin_id,
2343        &package_version,
2344        root_slot,
2345        supplied,
2346    ))
2347}
2348
2349fn read_configuration_schema(schema_path: &LitStr) -> syn::Result<Value> {
2350    let schema = read_package_json(schema_path, "configuration Schema")?;
2351    if !schema.is_object() {
2352        return Err(syn::Error::new(
2353            schema_path.span(),
2354            "configuration Schema must be a JSON object",
2355        ));
2356    }
2357    Ok(schema)
2358}
2359
2360fn read_configuration_defaults(defaults_path: &LitStr) -> syn::Result<Value> {
2361    let defaults = read_package_json(defaults_path, "configuration defaults")?;
2362    if !defaults.is_object() {
2363        return Err(syn::Error::new(
2364            defaults_path.span(),
2365            "configuration defaults must be a JSON object",
2366        ));
2367    }
2368    Ok(defaults)
2369}
2370
2371fn read_package_json(path: &LitStr, label: &str) -> syn::Result<Value> {
2372    let relative = PathBuf::from(path.value());
2373    if relative.is_absolute()
2374        || relative
2375            .components()
2376            .any(|component| !matches!(component, std::path::Component::Normal(_)))
2377    {
2378        return Err(syn::Error::new(
2379            path.span(),
2380            format!("{label} path must stay inside the Plugin package"),
2381        ));
2382    }
2383    let manifest_dir = env::var_os("CARGO_MANIFEST_DIR").ok_or_else(|| {
2384        syn::Error::new(
2385            path.span(),
2386            format!("CARGO_MANIFEST_DIR is unavailable while deriving {label}"),
2387        )
2388    })?;
2389    let full_path = PathBuf::from(manifest_dir).join(relative);
2390    let bytes = fs::read(&full_path).map_err(|error| {
2391        syn::Error::new(
2392            path.span(),
2393            format!("failed to read {label} {}: {error}", full_path.display()),
2394        )
2395    })?;
2396    serde_json::from_slice(&bytes).map_err(|error| {
2397        syn::Error::new(
2398            path.span(),
2399            format!("{label} {} is invalid JSON: {error}", full_path.display()),
2400        )
2401    })
2402}
2403
2404fn validate_configuration_defaults(defaults: &Value, schema: &Value) -> Result<(), String> {
2405    if !defaults.is_object() {
2406        return Err("$: defaults must be an object".to_owned());
2407    }
2408    validate_default_value(defaults, schema, "$")
2409}
2410
2411fn validate_default_value(value: &Value, schema: &Value, path: &str) -> Result<(), String> {
2412    let schema = schema
2413        .as_object()
2414        .ok_or_else(|| format!("{path}: configuration Schema must be an object"))?;
2415    if schema
2416        .get("x-lenso-sensitive")
2417        .and_then(Value::as_bool)
2418        .unwrap_or(false)
2419    {
2420        return Err(format!(
2421            "{path}: sensitive configuration cannot have a package default"
2422        ));
2423    }
2424    if let Some(expected) = schema.get("type").and_then(Value::as_str) {
2425        let valid = match expected {
2426            "array" => value.is_array(),
2427            "boolean" => value.is_boolean(),
2428            "integer" => value
2429                .as_number()
2430                .is_some_and(|number| number.is_i64() || number.is_u64()),
2431            "null" => value.is_null(),
2432            "number" => value.is_number(),
2433            "object" => value.is_object(),
2434            "string" => value.is_string(),
2435            _ => false,
2436        };
2437        if !valid {
2438            return Err(format!(
2439                "{path}: default does not match Schema type `{expected}`"
2440            ));
2441        }
2442    }
2443    if let (Some(minimum), Some(number)) = (schema.get("minimum"), value.as_f64()) {
2444        let minimum = minimum
2445            .as_f64()
2446            .ok_or_else(|| format!("{path}: Schema minimum must be a number"))?;
2447        if number < minimum {
2448            return Err(format!(
2449                "{path}: default must be greater than or equal to {minimum}"
2450            ));
2451        }
2452    }
2453    if let Some(expected) = schema.get("const")
2454        && value != expected
2455    {
2456        return Err(format!("{path}: default does not match Schema const"));
2457    }
2458    if let Some(allowed) = schema.get("enum") {
2459        let allowed = allowed
2460            .as_array()
2461            .ok_or_else(|| format!("{path}: Schema enum must be an array"))?;
2462        if !allowed.contains(value) {
2463            return Err(format!("{path}: default is not in Schema enum"));
2464        }
2465    }
2466    validate_default_object(value, schema, path)?;
2467    validate_default_array(value, schema, path)
2468}
2469
2470fn validate_default_object(
2471    value: &Value,
2472    schema: &Map<String, Value>,
2473    path: &str,
2474) -> Result<(), String> {
2475    let Some(object) = value.as_object() else {
2476        return Ok(());
2477    };
2478    let empty = Map::new();
2479    let properties = schema.get("properties").map_or(Ok(&empty), |properties| {
2480        properties
2481            .as_object()
2482            .ok_or_else(|| format!("{path}: Schema properties must be an object"))
2483    })?;
2484    for (name, child) in object {
2485        if let Some(child_schema) = properties.get(name) {
2486            validate_default_value(child, child_schema, &format!("{path}.{name}"))?;
2487            continue;
2488        }
2489        match schema.get("additionalProperties") {
2490            Some(Value::Bool(false)) => {
2491                return Err(format!("{path}.{name}: additional property is not allowed"));
2492            }
2493            Some(Value::Object(additional_schema)) => validate_default_value(
2494                child,
2495                &Value::Object(additional_schema.clone()),
2496                &format!("{path}.{name}"),
2497            )?,
2498            _ => {}
2499        }
2500    }
2501    Ok(())
2502}
2503
2504fn validate_default_array(
2505    value: &Value,
2506    schema: &Map<String, Value>,
2507    path: &str,
2508) -> Result<(), String> {
2509    let (Some(items), Some(item_schema)) = (value.as_array(), schema.get("items")) else {
2510        return Ok(());
2511    };
2512    for (index, item) in items.iter().enumerate() {
2513        validate_default_value(item, item_schema, &format!("{path}[{index}]"))?;
2514    }
2515    Ok(())
2516}
2517
2518fn complete_plugin_descriptor(
2519    plugin_id: &str,
2520    package_version: &str,
2521    root_slot: &str,
2522    mut supplied: Map<String, Value>,
2523) -> String {
2524    let mut generated = Map::new();
2525    generated.insert("plugin_id".to_owned(), json!(plugin_id));
2526    generated.insert("release_version".to_owned(), json!(package_version));
2527    generated.insert("root_slot".to_owned(), json!(root_slot));
2528    generated.insert("runtime_package_id".to_owned(), json!(plugin_id));
2529    generated.insert(
2530        "runtime_package_revision".to_owned(),
2531        json!(package_version),
2532    );
2533    generated.insert("entrypoint".to_owned(), json!("default"));
2534    for (key, value) in std::mem::take(&mut supplied) {
2535        generated.insert(key, value);
2536    }
2537    generated.insert("execution_class".to_owned(), json!("lenso.native-rust@1"));
2538    generated.insert(
2539        "restart_policy".to_owned(),
2540        json!({
2541            "mode": "never",
2542            "max_attempts": 0,
2543            "window": {"secs": 0, "nanos": 0},
2544            "backoff": {"secs": 0, "nanos": 0},
2545            "stability": {"secs": 0, "nanos": 0},
2546            "jitter": {"secs": 0, "nanos": 0}
2547        }),
2548    );
2549    generated.insert("criticality".to_owned(), json!("non_critical"));
2550    serde_json::to_string(&Value::Object(generated))
2551        .expect("generated Plugin Descriptor values must serialize")
2552}
2553
2554fn plugin_metadata() -> syn::Result<(String, String)> {
2555    let manifest_dir = env::var_os("CARGO_MANIFEST_DIR").ok_or_else(|| {
2556        syn::Error::new(
2557            proc_macro2::Span::call_site(),
2558            "CARGO_MANIFEST_DIR is unavailable",
2559        )
2560    })?;
2561    let manifest_path = PathBuf::from(manifest_dir).join("Cargo.toml");
2562    let manifest = fs::read_to_string(&manifest_path).map_err(|error| {
2563        syn::Error::new(
2564            proc_macro2::Span::call_site(),
2565            format!("failed to read {}: {error}", manifest_path.display()),
2566        )
2567    })?;
2568    let manifest: toml::Value = toml::from_str(&manifest).map_err(|error| {
2569        syn::Error::new(
2570            proc_macro2::Span::call_site(),
2571            format!("failed to parse {}: {error}", manifest_path.display()),
2572        )
2573    })?;
2574    let lenso = manifest
2575        .get("package")
2576        .and_then(|package| package.get("metadata"))
2577        .and_then(|metadata| metadata.get("lenso"))
2578        .and_then(toml::Value::as_table)
2579        .ok_or_else(|| metadata_error("missing `[package.metadata.lenso]` in Cargo.toml"))?;
2580    let plugin_id = lenso
2581        .get("plugin-id")
2582        .and_then(toml::Value::as_str)
2583        .ok_or_else(|| {
2584            metadata_error("missing `plugin-id = \"...\"` in `[package.metadata.lenso]`")
2585        })?;
2586    let root_slot = lenso
2587        .get("root-slot")
2588        .and_then(toml::Value::as_str)
2589        .ok_or_else(|| {
2590            metadata_error("missing `root-slot = \"...\"` in `[package.metadata.lenso]`")
2591        })?;
2592    Ok((plugin_id.to_owned(), root_slot.to_owned()))
2593}
2594
2595fn metadata_error(detail: &str) -> syn::Error {
2596    syn::Error::new(proc_macro2::Span::call_site(), detail)
2597}
2598
2599#[cfg(test)]
2600mod tests {
2601    use super::*;
2602    use syn::parse_quote;
2603
2604    #[test]
2605    fn generated_descriptor_owns_identity_and_execution_defaults() {
2606        let supplied = serde_json::from_value::<Map<String, Value>>(json!({
2607            "provided_capabilities": [],
2608            "required_capabilities": []
2609        }))
2610        .unwrap();
2611        let descriptor = complete_plugin_descriptor("example.tool", "1.2.3", "tools", supplied);
2612        let descriptor: Value = serde_json::from_str(&descriptor).unwrap();
2613
2614        assert_eq!(descriptor["plugin_id"], "example.tool");
2615        assert_eq!(descriptor["release_version"], "1.2.3");
2616        assert_eq!(descriptor["runtime_package_id"], "example.tool");
2617        assert_eq!(descriptor["runtime_package_revision"], "1.2.3");
2618        assert_eq!(descriptor["entrypoint"], "default");
2619        assert_eq!(descriptor["execution_class"], "lenso.native-rust@1");
2620        assert_eq!(descriptor["restart_policy"]["mode"], "never");
2621        assert_eq!(descriptor["criticality"], "non_critical");
2622    }
2623
2624    #[test]
2625    fn package_schema_is_embedded_as_descriptor_data() {
2626        let path = LitStr::new(
2627            "tests/fixtures/config.schema.json",
2628            proc_macro2::Span::call_site(),
2629        );
2630        let schema = read_configuration_schema(&path).unwrap();
2631
2632        assert_eq!(schema["type"], "object");
2633        assert_eq!(schema["required"], json!(["name", "retries"]));
2634    }
2635
2636    #[test]
2637    fn package_defaults_are_embedded_as_descriptor_data() {
2638        let path = LitStr::new(
2639            "tests/fixtures/config.defaults.json",
2640            proc_macro2::Span::call_site(),
2641        );
2642        let defaults = read_configuration_defaults(&path).unwrap();
2643
2644        assert_eq!(defaults, json!({"name": "fixture", "retries": 3}));
2645    }
2646
2647    #[test]
2648    fn factory_function_descriptor_embeds_package_defaults() {
2649        let descriptor = LitStr::new(
2650            r#"{"provided_capabilities":[],"required_capabilities":[]}"#,
2651            proc_macro2::Span::call_site(),
2652        );
2653        let schema = LitStr::new(
2654            "tests/fixtures/config.schema.json",
2655            proc_macro2::Span::call_site(),
2656        );
2657        let defaults = LitStr::new(
2658            "tests/fixtures/config.defaults.json",
2659            proc_macro2::Span::call_site(),
2660        );
2661
2662        let generated = plugin_descriptor(
2663            "example.tool",
2664            "tools",
2665            &descriptor,
2666            Some(&schema),
2667            Some(&defaults),
2668        )
2669        .unwrap();
2670        let generated: Value = serde_json::from_str(&generated).unwrap();
2671        assert_eq!(
2672            generated["configuration_defaults"],
2673            json!({"name": "fixture", "retries": 3})
2674        );
2675    }
2676
2677    #[test]
2678    fn typed_configuration_defaults_must_match_the_field_type() {
2679        let input: DeriveInput = parse_quote! {
2680            struct InvalidConfig {
2681                #[lenso(default = 3)]
2682                name: String,
2683            }
2684        };
2685
2686        let error = expand_plugin_config(&input).unwrap_err();
2687        assert!(error.to_string().contains("does not match the field type"));
2688    }
2689
2690    #[test]
2691    fn package_defaults_fail_closed_against_schema_constraints() {
2692        let schema = json!({
2693            "type": "object",
2694            "properties": {
2695                "retries": {"type": "integer", "minimum": 1},
2696                "token": {"x-lenso-sensitive": true}
2697            },
2698            "additionalProperties": false
2699        });
2700
2701        assert_eq!(
2702            validate_configuration_defaults(&json!({"retries": 0}), &schema),
2703            Err("$.retries: default must be greater than or equal to 1".to_owned())
2704        );
2705        assert_eq!(
2706            validate_configuration_defaults(&json!({"token": {"secret_ref": "TOKEN"}}), &schema),
2707            Err("$.token: sensitive configuration cannot have a package default".to_owned())
2708        );
2709    }
2710
2711    #[test]
2712    fn typed_ports_preserve_client_paths_and_cardinality() {
2713        let one: Type = parse_quote!(Port<secrets::SecretsClient>);
2714        let many: Type = parse_quote!(ManyPort<auth::AuthClient>);
2715
2716        let (one_client, one_cardinality) = port_client(&one).unwrap().unwrap();
2717        let (many_client, many_cardinality) = port_client(&many).unwrap().unwrap();
2718
2719        assert_eq!(quote!(#one_client).to_string(), "secrets :: SecretsClient");
2720        assert!(matches!(one_cardinality, PortCardinality::One));
2721        assert_eq!(quote!(#many_client).to_string(), "auth :: AuthClient");
2722        assert!(matches!(many_cardinality, PortCardinality::Many));
2723    }
2724
2725    #[test]
2726    fn named_dependency_fields_determine_cardinality_without_type_only_matching() {
2727        let mut plugin: ItemStruct = parse_quote! {
2728            struct Consumer {
2729                #[dependency(id = "source")]
2730                source: store::StoreClient,
2731                #[dependency(id = "fallback")]
2732                fallback: Option<store::StoreClient>,
2733                #[dependency(id = "replicas")]
2734                replicas: Vec<BoundCapabilityClient<store::StoreClient>>,
2735            }
2736        };
2737        let fields = analyze_struct_fields(&mut plugin, &quote!(::lenso)).unwrap();
2738
2739        assert_eq!(fields.construction_fields.len(), 3);
2740        let ids = fields
2741            .construction_fields
2742            .iter()
2743            .map(|field| match &field.kind {
2744                ConstructionFieldKind::Dependency {
2745                    id, cardinality, ..
2746                } => (
2747                    id.value(),
2748                    match cardinality {
2749                        DependencyCardinality::One => "one",
2750                        DependencyCardinality::Optional => "optional",
2751                        DependencyCardinality::Many => "many",
2752                    },
2753                ),
2754                _ => panic!("expected dependency field"),
2755            })
2756            .collect::<Vec<_>>();
2757        assert_eq!(
2758            ids,
2759            vec![
2760                ("source".to_owned(), "one"),
2761                ("fallback".to_owned(), "optional"),
2762                ("replicas".to_owned(), "many"),
2763            ]
2764        );
2765        assert!(plugin.fields.iter().all(|field| field.attrs.is_empty()));
2766    }
2767
2768    #[test]
2769    fn managed_tasks_fields_are_initialized_and_connected_on_activate() {
2770        let mut plugin: ItemStruct = parse_quote! {
2771            struct Worker {
2772                #[tasks]
2773                tasks: ManagedTasks,
2774            }
2775        };
2776        let fields = analyze_struct_fields(&mut plugin, &quote!(::lenso)).unwrap();
2777
2778        let task_field: syn::Ident = parse_quote!(tasks);
2779        assert_eq!(fields.tasks, vec![task_field]);
2780        assert_eq!(
2781            fields.initializers[0].to_string(),
2782            "tasks : :: core :: default :: Default :: default ()"
2783        );
2784        assert_eq!(
2785            task_connectors(&fields.tasks)[0].to_string(),
2786            "self . plugin . tasks . __lenso_connect (context . tasks () . clone ()) ? ;"
2787        );
2788        assert_eq!(
2789            task_disconnectors(&fields.tasks)[0].to_string(),
2790            "plugin . tasks . __lenso_disconnect () ;"
2791        );
2792        assert!(plugin.fields.iter().next().unwrap().attrs.is_empty());
2793    }
2794
2795    #[test]
2796    fn multiple_capabilities_reject_trait_impls() {
2797        let implementation: ItemImpl = parse_quote! {
2798            impl fixture::Provider for ExamplePlugin {}
2799        };
2800        let error = expand_provides(
2801            &[parse_quote!(fixture::One), parse_quote!(fixture::Two)],
2802            &implementation,
2803        )
2804        .expect_err("multi-Capability authoring must have one inherent impl");
2805
2806        assert!(
2807            error
2808                .to_string()
2809                .contains("multiple Capabilities require one inherent impl")
2810        );
2811    }
2812
2813    #[test]
2814    fn duplicate_capabilities_are_rejected() {
2815        let implementation: ItemImpl = parse_quote! { impl ExamplePlugin {} };
2816        let error = expand_provides(
2817            &[parse_quote!(fixture::One), parse_quote!(fixture::One)],
2818            &implementation,
2819        )
2820        .expect_err("one Capability cannot be contributed twice");
2821
2822        assert!(error.to_string().contains("same Capability more than once"));
2823    }
2824
2825    #[test]
2826    fn capability_paths_must_be_namespace_qualified() {
2827        let implementation: ItemImpl = parse_quote! { impl ExamplePlugin {} };
2828        let error = expand_provides(&[parse_quote!(One)], &implementation)
2829            .expect_err("generated Capability macros live in their namespace");
2830
2831        assert!(error.to_string().contains("namespace-qualified"));
2832    }
2833}