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