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