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