Skip to main content

temporalio_sdk/
plugins.rs

1//! Experimental plugin APIs for configuring workers and clients from reusable values.
2
3#[cfg(feature = "wasm-workflows")]
4use crate::WasmWorkflowComponent;
5pub use crate::workflow_registry::WorkflowDefinitions;
6use crate::{
7    WorkerOptions,
8    activities::ActivityDefinitions,
9    interceptors::{ActivityInboundInterceptor, WorkerInterceptor},
10    workflow_interceptors::WorkflowInterceptorConstructor,
11    workflow_replayer::WorkflowReplayerOptions,
12};
13use std::{any::Any, sync::Arc};
14use temporalio_client::{
15    ClientInterceptor, ClientOptions, ClientPlugin, ConnectionOptions, ErasedClientPlugin,
16    PluginApplyError, PluginError, PluginTarget, WorkerPluginData,
17};
18use temporalio_common::data_converters::DataConverter;
19
20/// Configures worker options before the worker is created.
21///
22/// Use [`ClientAndWorkerPlugin`] for plugins that target both clients and workers.
23///
24/// **Experimental:** This API may change or be removed.
25pub trait WorkerPlugin: Send + Sync + 'static {
26    /// Stable, unique name used to identify this plugin.
27    fn name(&self) -> &str;
28
29    /// Configure worker options.
30    ///
31    /// Worker plugin registrations are captured before this method is called. Altering plugins in
32    /// this method does not change which plugins are applied.
33    fn configure_worker_options(&self, _options: &mut WorkerOptions) -> Result<(), PluginError> {
34        Ok(())
35    }
36
37    /// Configure workflow replayer options.
38    ///
39    /// Replay plugin registrations are captured before this method is called. Altering plugins in
40    /// this method does not change which plugins are applied.
41    fn configure_workflow_replayer_options(
42        &self,
43        _options: &mut WorkflowReplayerOptions,
44    ) -> Result<(), PluginError> {
45        Ok(())
46    }
47}
48
49#[derive(Clone)]
50struct PropagatedWorkerPlugin(Arc<dyn WorkerPlugin>);
51
52impl WorkerPluginData for PropagatedWorkerPlugin {}
53
54/// A container for a plugin that implements both [`ClientPlugin`] and
55/// [`WorkerPlugin`].
56///
57/// This wrapper is only needed when the same plugin configures both a client and workers.
58/// One-sided plugins can implement [`ClientPlugin`] or [`WorkerPlugin`] directly.
59///
60/// When registered on [`ClientOptions`], the resulting client carries the worker plugin and
61/// automatically applies it to workers created from that client. Do not also register the plugin
62/// on [`WorkerOptions`], or its worker configuration will be applied twice.
63///
64/// ```
65/// # use temporalio_client::{ClientOptions, ClientPlugin};
66/// # use temporalio_sdk::{ClientAndWorkerPlugin, WorkerPlugin};
67/// # struct MyPlugin;
68/// # impl ClientPlugin for MyPlugin {
69/// #     fn name(&self) -> &str { "my-plugin" }
70/// # }
71/// # impl WorkerPlugin for MyPlugin {
72/// #     fn name(&self) -> &str { "my-plugin" }
73/// # }
74/// let plugin = ClientAndWorkerPlugin::new(MyPlugin);
75/// let client_options = ClientOptions::new("default").plugin(plugin).build();
76/// # let _ = client_options;
77/// ```
78///
79/// **Experimental:** This API may change or be removed.
80#[derive(Clone)]
81pub struct ClientAndWorkerPlugin {
82    client: ErasedClientPlugin,
83    worker: Arc<dyn WorkerPlugin>,
84}
85
86impl ClientAndWorkerPlugin {
87    /// Type-erase one plugin value for client registration, automatic worker propagation, and
88    /// optional explicit worker registration.
89    pub fn new<P>(plugin: P) -> Self
90    where
91        P: ClientPlugin + WorkerPlugin,
92    {
93        let plugin = Arc::new(plugin);
94        let mut client = ErasedClientPlugin::new(SharedClientPlugin(plugin.clone()));
95        let worker: Arc<dyn WorkerPlugin> = Arc::new(SharedWorkerPlugin(plugin));
96        client = client.with_worker_plugin(PropagatedWorkerPlugin(worker.clone()));
97        Self { client, worker }
98    }
99}
100
101impl From<ClientAndWorkerPlugin> for ErasedClientPlugin {
102    fn from(plugin: ClientAndWorkerPlugin) -> Self {
103        plugin.client
104    }
105}
106
107impl WorkerPlugin for ClientAndWorkerPlugin {
108    fn name(&self) -> &str {
109        self.worker.name()
110    }
111
112    fn configure_worker_options(&self, options: &mut WorkerOptions) -> Result<(), PluginError> {
113        self.worker.configure_worker_options(options)
114    }
115
116    fn configure_workflow_replayer_options(
117        &self,
118        options: &mut WorkflowReplayerOptions,
119    ) -> Result<(), PluginError> {
120        self.worker.configure_workflow_replayer_options(options)
121    }
122}
123
124/// A simple plugin field that either supplies a value or customizes the existing value.
125///
126/// Builder methods on [`SimplePluginBuilder`] automatically convert supported values and
127/// functions into this type.
128///
129/// **Experimental:** This API may change or be removed.
130#[derive(Clone)]
131pub enum SimplePluginOption<T> {
132    /// A value that replaces a scalar field or appends to a collection field.
133    Value(T),
134    /// A function that receives the existing field and returns a configured value.
135    Function(Arc<dyn Fn(Option<T>) -> T + Send + Sync>),
136}
137
138macro_rules! impl_simple_plugin_option_conversions {
139    ($type:ty) => {
140        impl From<$type> for SimplePluginOption<$type> {
141            fn from(value: $type) -> Self {
142                Self::Value(value)
143            }
144        }
145
146        impl<F> From<F> for SimplePluginOption<$type>
147        where
148            F: Fn(Option<$type>) -> $type + Send + Sync + 'static,
149        {
150            fn from(function: F) -> Self {
151                Self::Function(Arc::new(function))
152            }
153        }
154    };
155}
156
157impl_simple_plugin_option_conversions!(DataConverter);
158impl_simple_plugin_option_conversions!(Vec<Arc<dyn ClientInterceptor>>);
159impl_simple_plugin_option_conversions!(Vec<Arc<dyn WorkerInterceptor>>);
160impl_simple_plugin_option_conversions!(Vec<Arc<dyn ActivityInboundInterceptor>>);
161impl_simple_plugin_option_conversions!(Vec<WorkflowInterceptorConstructor>);
162impl_simple_plugin_option_conversions!(ActivityDefinitions);
163impl_simple_plugin_option_conversions!(WorkflowDefinitions);
164#[cfg(feature = "wasm-workflows")]
165impl_simple_plugin_option_conversions!(Vec<WasmWorkflowComponent>);
166
167fn apply_replacing<T: Clone>(target: &mut T, option: Option<&SimplePluginOption<T>>) {
168    let Some(option) = option else {
169        return;
170    };
171    *target = match option {
172        SimplePluginOption::Value(value) => value.clone(),
173        SimplePluginOption::Function(function) => function(Some(target.clone())),
174    };
175}
176
177fn apply_appending<T: Clone>(
178    target: &mut T,
179    option: Option<&SimplePluginOption<T>>,
180    append: impl Fn(&mut T, &T),
181) {
182    let Some(option) = option else {
183        return;
184    };
185    match option {
186        SimplePluginOption::Value(value) => append(target, value),
187        SimplePluginOption::Function(function) => *target = function(Some(target.clone())),
188    }
189}
190
191/// A plugin assembled from declarative values or functions of existing values.
192///
193/// Scalar values replace the corresponding option, while collection values append. Functions
194/// receive the existing field and replace it with their return value, except that returned
195/// workflow definitions are merged into the existing definitions.
196///
197/// ```
198/// use temporalio_client::ClientOptions;
199/// use temporalio_common::data_converters::DataConverter;
200/// use temporalio_sdk::SimplePlugin;
201///
202/// let plugin = SimplePlugin::builder("my-plugin")
203///     .data_converter(|existing: Option<DataConverter>| existing.unwrap_or_default())
204///     .build();
205/// let client_options = ClientOptions::new("default").plugin(plugin).build();
206/// # let _ = client_options;
207/// ```
208///
209/// Worker configuration declared by this combined plugin is automatically propagated through the
210/// configured client when a worker is created.
211///
212/// **Experimental:** This API may change or be removed.
213#[derive(Clone, bon::Builder)]
214#[builder(state_mod(vis = "pub"))]
215pub struct SimplePlugin {
216    #[builder(start_fn, into)]
217    name: String,
218    #[builder(into)]
219    data_converter: Option<SimplePluginOption<DataConverter>>,
220    #[builder(into)]
221    client_interceptors: Option<SimplePluginOption<Vec<Arc<dyn ClientInterceptor>>>>,
222    #[builder(into)]
223    worker_interceptors: Option<SimplePluginOption<Vec<Arc<dyn WorkerInterceptor>>>>,
224    #[builder(into)]
225    activity_inbound_interceptors:
226        Option<SimplePluginOption<Vec<Arc<dyn ActivityInboundInterceptor>>>>,
227    #[builder(into)]
228    workflow_interceptors: Option<SimplePluginOption<Vec<WorkflowInterceptorConstructor>>>,
229    #[builder(into)]
230    activities: Option<SimplePluginOption<ActivityDefinitions>>,
231    #[builder(into)]
232    workflows: Option<SimplePluginOption<WorkflowDefinitions>>,
233    #[cfg(feature = "wasm-workflows")]
234    #[builder(into)]
235    wasm_workflow_components: Option<SimplePluginOption<Vec<WasmWorkflowComponent>>>,
236}
237
238impl From<SimplePlugin> for ErasedClientPlugin {
239    fn from(plugin: SimplePlugin) -> Self {
240        ClientAndWorkerPlugin::new(plugin).into()
241    }
242}
243
244impl ClientPlugin for SimplePlugin {
245    fn name(&self) -> &str {
246        &self.name
247    }
248
249    fn configure_client_options(&self, options: &mut ClientOptions) -> Result<(), PluginError> {
250        apply_replacing(&mut options.data_converter, self.data_converter.as_ref());
251        apply_appending(
252            &mut options.client_interceptors,
253            self.client_interceptors.as_ref(),
254            |existing, value| existing.extend(value.iter().cloned()),
255        );
256        Ok(())
257    }
258}
259
260impl WorkerPlugin for SimplePlugin {
261    fn name(&self) -> &str {
262        &self.name
263    }
264
265    fn configure_worker_options(&self, options: &mut WorkerOptions) -> Result<(), PluginError> {
266        apply_appending(
267            &mut options.activities,
268            self.activities.as_ref(),
269            |existing, value| existing.extend(value),
270        );
271        if let Some(workflows) = &self.workflows {
272            match workflows {
273                SimplePluginOption::Value(value) => options.workflows.extend(value),
274                SimplePluginOption::Function(function) => {
275                    let workflows = function(Some(options.workflows.clone()));
276                    options.workflows.extend(&workflows)
277                }
278            }
279            .map_err(PluginError::new)?;
280        }
281        apply_appending(
282            &mut options.worker_interceptors,
283            self.worker_interceptors.as_ref(),
284            |existing, value| existing.extend(value.iter().cloned()),
285        );
286        apply_appending(
287            &mut options.activity_inbound_interceptors,
288            self.activity_inbound_interceptors.as_ref(),
289            |existing, value| existing.extend(value.iter().cloned()),
290        );
291        apply_appending(
292            &mut options.workflow_interceptor_constructors,
293            self.workflow_interceptors.as_ref(),
294            |existing, value| existing.extend(value.iter().cloned()),
295        );
296        #[cfg(feature = "wasm-workflows")]
297        apply_appending(
298            &mut options.wasm_workflow_components,
299            self.wasm_workflow_components.as_ref(),
300            |existing, value| existing.extend(value.iter().cloned()),
301        );
302        Ok(())
303    }
304
305    fn configure_workflow_replayer_options(
306        &self,
307        options: &mut WorkflowReplayerOptions,
308    ) -> Result<(), PluginError> {
309        apply_replacing(&mut options.data_converter, self.data_converter.as_ref());
310        if let Some(workflows) = &self.workflows {
311            match workflows {
312                SimplePluginOption::Value(value) => options.workflows.extend(value),
313                SimplePluginOption::Function(function) => {
314                    let workflows = function(Some(options.workflows.clone()));
315                    options.workflows.extend(&workflows)
316                }
317            }
318            .map_err(PluginError::new)?;
319        }
320        apply_appending(
321            &mut options.worker_interceptors,
322            self.worker_interceptors.as_ref(),
323            |existing, value| existing.extend(value.iter().cloned()),
324        );
325        apply_appending(
326            &mut options.workflow_interceptor_constructors,
327            self.workflow_interceptors.as_ref(),
328            |existing, value| existing.extend(value.iter().cloned()),
329        );
330        #[cfg(feature = "wasm-workflows")]
331        apply_appending(
332            &mut options.wasm_workflow_components,
333            self.wasm_workflow_components.as_ref(),
334            |existing, value| existing.extend(value.iter().cloned()),
335        );
336        Ok(())
337    }
338}
339
340struct SharedClientPlugin<P>(Arc<P>);
341
342impl<P> ClientPlugin for SharedClientPlugin<P>
343where
344    P: ClientPlugin,
345{
346    fn name(&self) -> &str {
347        ClientPlugin::name(self.0.as_ref())
348    }
349
350    fn configure_connection_options(
351        &self,
352        options: &mut ConnectionOptions,
353    ) -> Result<(), PluginError> {
354        self.0.configure_connection_options(options)
355    }
356
357    fn configure_client_options(&self, options: &mut ClientOptions) -> Result<(), PluginError> {
358        self.0.configure_client_options(options)
359    }
360}
361
362struct SharedWorkerPlugin<P>(Arc<P>);
363
364impl<P> WorkerPlugin for SharedWorkerPlugin<P>
365where
366    P: ClientPlugin + WorkerPlugin,
367{
368    fn name(&self) -> &str {
369        ClientPlugin::name(self.0.as_ref())
370    }
371
372    fn configure_worker_options(&self, options: &mut WorkerOptions) -> Result<(), PluginError> {
373        self.0.configure_worker_options(options)
374    }
375
376    fn configure_workflow_replayer_options(
377        &self,
378        options: &mut WorkflowReplayerOptions,
379    ) -> Result<(), PluginError> {
380        self.0.configure_workflow_replayer_options(options)
381    }
382}
383
384#[derive(Debug, Eq, PartialEq)]
385struct WorkerPluginWarning<'a> {
386    plugin_name: &'a str,
387    message: &'static str,
388}
389
390fn worker_plugin_warnings(
391    plugins: &[Arc<dyn WorkerPlugin>],
392) -> impl Iterator<Item = WorkerPluginWarning<'_>> {
393    plugins.iter().enumerate().filter_map(|(index, plugin)| {
394        plugins[index + 1..]
395            .iter()
396            .any(|other| plugin.name() == other.name())
397            .then_some(WorkerPluginWarning {
398                plugin_name: plugin.name(),
399                message: "Multiple worker plugins with the same name were registered",
400            })
401    })
402}
403
404pub(crate) fn apply_worker_plugins(
405    client_options: &ClientOptions,
406    options: &mut WorkerOptions,
407) -> Result<(), PluginApplyError> {
408    options.client_plugin_names = client_options
409        .plugins()
410        .iter()
411        .map(|plugin| plugin.name().to_owned())
412        .collect();
413    let mut plugins = client_options
414        .plugins()
415        .iter()
416        .flat_map(ErasedClientPlugin::worker_plugins)
417        .filter_map(|plugin| (plugin as &dyn Any).downcast_ref::<PropagatedWorkerPlugin>())
418        .map(|plugin| plugin.0.clone())
419        .collect::<Vec<_>>();
420    plugins.append(&mut options.worker_plugins);
421
422    for warning in worker_plugin_warnings(&plugins) {
423        warn!(plugin = warning.plugin_name, "{}", warning.message);
424    }
425
426    for registration in &plugins {
427        registration
428            .configure_worker_options(options)
429            .map_err(|source| {
430                PluginApplyError::new(registration.name(), PluginTarget::Worker, source)
431            })?;
432    }
433    options.worker_plugins = plugins;
434    Ok(())
435}
436
437pub(crate) fn apply_workflow_replayer_plugins(
438    options: &mut WorkflowReplayerOptions,
439) -> Result<(), PluginApplyError> {
440    let plugins = std::mem::take(&mut options.worker_plugins);
441
442    for warning in worker_plugin_warnings(&plugins) {
443        warn!(plugin = warning.plugin_name, "{}", warning.message);
444    }
445
446    for registration in &plugins {
447        registration
448            .configure_workflow_replayer_options(options)
449            .map_err(|source| {
450                PluginApplyError::new(registration.name(), PluginTarget::WorkflowReplayer, source)
451            })?;
452    }
453    options.worker_plugins = plugins;
454    Ok(())
455}
456
457#[cfg(test)]
458mod tests {
459    use super::*;
460    use std::{
461        collections::HashSet,
462        sync::{
463            Mutex,
464            atomic::{AtomicU8, Ordering},
465        },
466    };
467    use temporalio_client::ClientOptions;
468    use temporalio_common::protos::temporal::api::worker::v1::PluginInfo;
469
470    #[temporalio_macros::workflow]
471    #[derive(Default)]
472    struct PluginTestWorkflow;
473
474    #[temporalio_macros::workflow_methods]
475    impl PluginTestWorkflow {
476        #[run]
477        async fn run(_ctx: &mut crate::WorkflowContext<Self>) -> crate::WorkflowResult<()> {
478            Ok(())
479        }
480    }
481
482    struct RecordingCombinedPlugin {
483        order: Arc<Mutex<Vec<&'static str>>>,
484    }
485
486    impl ClientPlugin for RecordingCombinedPlugin {
487        fn name(&self) -> &str {
488            "combined"
489        }
490    }
491
492    impl WorkerPlugin for RecordingCombinedPlugin {
493        fn name(&self) -> &str {
494            "combined"
495        }
496
497        fn configure_worker_options(
498            &self,
499            _options: &mut WorkerOptions,
500        ) -> Result<(), PluginError> {
501            self.order.lock().unwrap().push("propagated");
502            Ok(())
503        }
504    }
505
506    struct RecordingWorkerPlugin {
507        name: &'static str,
508        value: &'static str,
509        order: Arc<Mutex<Vec<&'static str>>>,
510    }
511
512    impl WorkerPlugin for RecordingWorkerPlugin {
513        fn name(&self) -> &str {
514            self.name
515        }
516
517        fn configure_worker_options(
518            &self,
519            _options: &mut WorkerOptions,
520        ) -> Result<(), PluginError> {
521            self.order.lock().unwrap().push(self.value);
522            Ok(())
523        }
524
525        fn configure_workflow_replayer_options(
526            &self,
527            _options: &mut WorkflowReplayerOptions,
528        ) -> Result<(), PluginError> {
529            self.order.lock().unwrap().push(self.value);
530            Ok(())
531        }
532    }
533
534    #[test]
535    fn propagated_plugins_run_before_local_plugins() {
536        let order = Arc::new(Mutex::new(Vec::new()));
537        let combined = ClientAndWorkerPlugin::new(RecordingCombinedPlugin {
538            order: order.clone(),
539        });
540        let client_options = ClientOptions::new("namespace").plugin(combined).build();
541        let mut worker_options = WorkerOptions::new("queue")
542            .worker_plugin(RecordingWorkerPlugin {
543                name: "local",
544                value: "local",
545                order: order.clone(),
546            })
547            .build();
548
549        apply_worker_plugins(&client_options, &mut worker_options).unwrap();
550
551        assert_eq!(*order.lock().unwrap(), ["propagated", "local"]);
552    }
553
554    #[test]
555    fn replay_plugins_configure_in_registration_order() {
556        let order = Arc::new(Mutex::new(Vec::new()));
557        let mut options = WorkflowReplayerOptions::new()
558            .worker_plugin(RecordingWorkerPlugin {
559                name: "first",
560                value: "first",
561                order: order.clone(),
562            })
563            .worker_plugin(RecordingWorkerPlugin {
564                name: "second",
565                value: "second",
566                order: order.clone(),
567            })
568            .build();
569
570        apply_workflow_replayer_plugins(&mut options).unwrap();
571
572        assert_eq!(*order.lock().unwrap(), ["first", "second"]);
573        assert_eq!(options.worker_plugins.len(), 2);
574    }
575
576    #[test]
577    fn explicitly_reusing_a_combined_registration_warns_and_applies_both() {
578        let order = Arc::new(Mutex::new(Vec::new()));
579        let combined = ClientAndWorkerPlugin::new(RecordingCombinedPlugin {
580            order: order.clone(),
581        });
582        let client_options = ClientOptions::new("namespace")
583            .plugin(combined.clone())
584            .build();
585        let mut worker_options = WorkerOptions::new("queue").worker_plugin(combined).build();
586
587        apply_worker_plugins(&client_options, &mut worker_options).unwrap();
588
589        assert_eq!(
590            worker_plugin_warnings(&worker_options.worker_plugins).collect::<Vec<_>>(),
591            [WorkerPluginWarning {
592                plugin_name: "combined",
593                message: "Multiple worker plugins with the same name were registered",
594            }]
595        );
596        assert_eq!(*order.lock().unwrap(), ["propagated", "propagated"]);
597    }
598
599    struct ClientOnlyPlugin;
600
601    impl ClientPlugin for ClientOnlyPlugin {
602        fn name(&self) -> &str {
603            "client-only"
604        }
605    }
606
607    struct SameNameClientPlugin;
608
609    impl ClientPlugin for SameNameClientPlugin {
610        fn name(&self) -> &str {
611            "same-name"
612        }
613    }
614
615    struct UnrecognizedWorkerPluginExtension {
616        _registration: Arc<dyn WorkerPlugin>,
617    }
618
619    impl WorkerPluginData for UnrecognizedWorkerPluginExtension {}
620
621    #[test]
622    fn arbitrary_opaque_data_cannot_impersonate_propagated_plugin() {
623        let order = Arc::new(Mutex::new(Vec::new()));
624        let client_registration = ErasedClientPlugin::new(ClientOnlyPlugin).with_worker_plugin(
625            UnrecognizedWorkerPluginExtension {
626                _registration: Arc::new(RecordingWorkerPlugin {
627                    name: "unrecognized",
628                    value: "should-not-run",
629                    order: order.clone(),
630                }),
631            },
632        );
633        let client_options = ClientOptions::new("namespace")
634            .plugin(client_registration)
635            .build();
636        let mut worker_options = WorkerOptions::new("queue").build();
637
638        apply_worker_plugins(&client_options, &mut worker_options).unwrap();
639
640        assert!(order.lock().unwrap().is_empty());
641    }
642
643    #[test]
644    fn duplicate_worker_plugin_names_warn_and_heartbeat_names_are_deduplicated() {
645        let order = Arc::new(Mutex::new(Vec::new()));
646        let client_options = ClientOptions::new("namespace")
647            .client_plugin(ClientOnlyPlugin)
648            .client_plugin(SameNameClientPlugin)
649            .build();
650        let mut worker_options = WorkerOptions::new("queue")
651            .register_workflow::<PluginTestWorkflow>()
652            .unwrap()
653            .worker_plugin(RecordingWorkerPlugin {
654                name: "same-name",
655                value: "first",
656                order: order.clone(),
657            })
658            .worker_plugin(RecordingWorkerPlugin {
659                name: "same-name",
660                value: "second",
661                order,
662            })
663            .build();
664        apply_worker_plugins(&client_options, &mut worker_options).unwrap();
665
666        assert_eq!(
667            worker_plugin_warnings(&worker_options.worker_plugins).collect::<Vec<_>>(),
668            [WorkerPluginWarning {
669                plugin_name: "same-name",
670                message: "Multiple worker plugins with the same name were registered",
671            }]
672        );
673        let core_options = worker_options
674            .to_core_options("namespace".to_owned(), "identity".to_owned())
675            .unwrap();
676
677        let expected_plugins: HashSet<_> = vec![
678            PluginInfo {
679                name: "client-only".into(),
680                version: "".into(),
681            },
682            PluginInfo {
683                name: "same-name".into(),
684                version: "".into(),
685            },
686        ]
687        .into_iter()
688        .collect();
689        assert_eq!(core_options.plugins, expected_plugins);
690    }
691
692    struct EmptyClientInterceptor;
693
694    impl ClientInterceptor for EmptyClientInterceptor {}
695
696    struct EmptyWorkerInterceptor;
697
698    #[async_trait::async_trait(?Send)]
699    impl WorkerInterceptor for EmptyWorkerInterceptor {}
700
701    #[test]
702    fn simple_plugin_applies_declarative_values() {
703        let plugin = SimplePlugin::builder("simple")
704            .client_interceptors(vec![
705                Arc::new(EmptyClientInterceptor) as Arc<dyn ClientInterceptor>
706            ])
707            .build();
708        let mut client_options = ClientOptions::new("namespace").build();
709        client_options
710            .client_interceptors
711            .push(Arc::new(EmptyClientInterceptor));
712        plugin
713            .configure_client_options(&mut client_options)
714            .unwrap();
715        assert_eq!(client_options.client_interceptors.len(), 2);
716
717        let plugin = SimplePlugin::builder("simple")
718            .worker_interceptors(vec![
719                Arc::new(EmptyWorkerInterceptor) as Arc<dyn WorkerInterceptor>
720            ])
721            .build();
722        let client_options = ClientOptions::new("namespace").build();
723        let mut worker_options = WorkerOptions::new("queue")
724            .worker_interceptor(EmptyWorkerInterceptor)
725            .worker_plugin(plugin)
726            .build();
727        apply_worker_plugins(&client_options, &mut worker_options).unwrap();
728
729        assert_eq!(worker_options.worker_interceptors.len(), 2);
730    }
731
732    #[test]
733    fn simple_plugin_options_accept_values_and_functions() {
734        let calls = Arc::new(AtomicU8::new(0));
735        let plugin = SimplePlugin::builder("simple")
736            .data_converter({
737                let calls = calls.clone();
738                move |existing: Option<DataConverter>| {
739                    calls.fetch_add(1, Ordering::SeqCst);
740                    existing.unwrap()
741                }
742            })
743            .client_interceptors({
744                let calls = calls.clone();
745                move |existing: Option<Vec<Arc<dyn ClientInterceptor>>>| {
746                    calls.fetch_add(1, Ordering::SeqCst);
747                    assert_eq!(existing.unwrap().len(), 1);
748                    Vec::new()
749                }
750            })
751            .worker_interceptors({
752                let calls = calls.clone();
753                move |existing: Option<Vec<Arc<dyn WorkerInterceptor>>>| {
754                    calls.fetch_add(1, Ordering::SeqCst);
755                    assert_eq!(existing.unwrap().len(), 1);
756                    Vec::new()
757                }
758            })
759            .activity_inbound_interceptors({
760                let calls = calls.clone();
761                move |existing: Option<Vec<Arc<dyn ActivityInboundInterceptor>>>| {
762                    calls.fetch_add(1, Ordering::SeqCst);
763                    existing.unwrap()
764                }
765            })
766            .workflow_interceptors({
767                let calls = calls.clone();
768                move |existing: Option<Vec<WorkflowInterceptorConstructor>>| {
769                    calls.fetch_add(1, Ordering::SeqCst);
770                    existing.unwrap()
771                }
772            })
773            .activities({
774                let calls = calls.clone();
775                move |existing: Option<ActivityDefinitions>| {
776                    calls.fetch_add(1, Ordering::SeqCst);
777                    existing.unwrap()
778                }
779            })
780            .workflows({
781                let calls = calls.clone();
782                move |existing: Option<WorkflowDefinitions>| {
783                    calls.fetch_add(1, Ordering::SeqCst);
784                    existing.unwrap()
785                }
786            })
787            .build();
788
789        let mut client_options = ClientOptions::new("namespace").build();
790        client_options
791            .client_interceptors
792            .push(Arc::new(EmptyClientInterceptor));
793        plugin
794            .configure_client_options(&mut client_options)
795            .unwrap();
796        assert!(client_options.client_interceptors.is_empty());
797
798        let mut worker_options = WorkerOptions::new("queue")
799            .worker_interceptor(EmptyWorkerInterceptor)
800            .worker_plugin(plugin)
801            .build();
802        apply_worker_plugins(
803            &ClientOptions::new("namespace").build(),
804            &mut worker_options,
805        )
806        .unwrap();
807        assert!(worker_options.worker_interceptors.is_empty());
808        assert_eq!(calls.load(Ordering::SeqCst), 7);
809    }
810
811    struct RecursivePlugin(Arc<AtomicU8>);
812
813    impl WorkerPlugin for RecursivePlugin {
814        fn name(&self) -> &str {
815            "recursive"
816        }
817
818        fn configure_worker_options(&self, options: &mut WorkerOptions) -> Result<(), PluginError> {
819            self.0.fetch_add(1, Ordering::SeqCst);
820            *options = WorkerOptions::new(options.task_queue.clone())
821                .worker_plugin(RecursivePlugin(self.0.clone()))
822                .worker_plugin(RecursivePlugin(self.0.clone()))
823                .build();
824            Ok(())
825        }
826    }
827
828    #[test]
829    fn test_plugins_cannot_recurse() {
830        let count = Arc::new(AtomicU8::new(0));
831        let mut worker_opts = WorkerOptions::new("my-task-queue")
832            .worker_plugin(RecursivePlugin(count.clone()))
833            .build();
834        apply_worker_plugins(&ClientOptions::new("my-ns").build(), &mut worker_opts).unwrap();
835        assert_eq!(count.load(Ordering::SeqCst), 1);
836    }
837}