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