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