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