Skip to main content

lenso_native_adapter_macros/
lib.rs

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