Skip to main content

golem_rust/
retry.rs

1use crate::bindings::golem::api::retry as retry_api;
2
3pub use builder::{NamedPolicy, Policy, Predicate, Props, RetryBuilderError, Value};
4pub use retry_api::{NamedRetryPolicy, PredicateValue, RetryPolicy, RetryPredicate};
5
6/// Get all retry policies active for this agent.
7pub fn get_retry_policies() -> Vec<NamedRetryPolicy> {
8    retry_api::get_retry_policies()
9}
10
11/// Get a specific retry policy by name.
12pub fn get_retry_policy_by_name(name: &str) -> Option<NamedRetryPolicy> {
13    retry_api::get_retry_policy_by_name(name)
14}
15
16/// Resolve the matching retry policy for a given operation context.
17/// Evaluates named policies in descending priority order; returns the
18/// policy from the first rule whose predicate matches, or none.
19pub fn resolve_retry_policy(
20    verb: &str,
21    noun_uri: &str,
22    properties: &[(String, PredicateValue)],
23) -> Option<RetryPolicy> {
24    let props: Vec<(String, PredicateValue)> = properties.to_vec();
25    retry_api::resolve_retry_policy(verb, noun_uri, &props)
26}
27
28/// Add or overwrite a named retry policy (persisted to oplog).
29/// If a policy with the same name exists, it is replaced.
30pub fn set_retry_policy(policy: &NamedRetryPolicy) {
31    retry_api::set_retry_policy(policy);
32}
33
34/// Add or overwrite a high-level named retry policy after validating and
35/// flattening it into the raw WIT representation.
36pub fn set_named_policy(policy: &NamedPolicy) -> Result<(), RetryBuilderError> {
37    let raw = policy.try_to_raw()?;
38    set_retry_policy(&raw);
39    Ok(())
40}
41
42/// Remove a named retry policy by name (persisted to oplog).
43pub fn remove_retry_policy(name: &str) {
44    retry_api::remove_retry_policy(name);
45}
46
47/// Guard that restores the previous state of a named retry policy on drop.
48/// If the policy existed before, it is restored; if it was newly added, it is removed.
49pub struct RetryPolicyGuard {
50    previous: Option<NamedRetryPolicy>,
51    name: String,
52}
53
54impl Drop for RetryPolicyGuard {
55    fn drop(&mut self) {
56        match self.previous.take() {
57            Some(original) => set_retry_policy(&original),
58            None => remove_retry_policy(&self.name),
59        }
60    }
61}
62
63/// Temporarily sets a named retry policy. When the returned guard is dropped,
64/// the previous policy with the same name is restored (or removed if it didn't exist).
65#[must_use]
66pub fn use_retry_policy(policy: NamedRetryPolicy) -> RetryPolicyGuard {
67    let previous = get_retry_policy_by_name(&policy.name);
68    let name = policy.name.clone();
69    set_retry_policy(&policy);
70    RetryPolicyGuard { previous, name }
71}
72
73/// Temporarily sets a high-level named retry policy after validating and
74/// flattening it into the raw WIT representation.
75pub fn use_named_policy(policy: &NamedPolicy) -> Result<RetryPolicyGuard, RetryBuilderError> {
76    let raw = policy.try_to_raw()?;
77    Ok(use_retry_policy(raw))
78}
79
80/// Executes the given function with a named retry policy temporarily set.
81pub fn with_retry_policy<R>(policy: NamedRetryPolicy, f: impl FnOnce() -> R) -> R {
82    let _guard = use_retry_policy(policy);
83    f()
84}
85
86/// Executes the given async function with a named retry policy temporarily set.
87pub async fn with_retry_policy_async<R, F: std::future::Future<Output = R>>(
88    policy: NamedRetryPolicy,
89    f: impl FnOnce() -> F,
90) -> R {
91    let _guard = use_retry_policy(policy);
92    f().await
93}
94
95/// Executes the given function with a high-level named retry policy temporarily set.
96pub fn with_named_policy<R>(
97    policy: &NamedPolicy,
98    f: impl FnOnce() -> R,
99) -> Result<R, RetryBuilderError> {
100    let _guard = use_named_policy(policy)?;
101    Ok(f())
102}
103
104/// Executes the given async function with a high-level named retry policy temporarily set.
105///
106/// This mutates the agent's active named retry policies for the lifetime of the future.
107/// If the future yields, other interleaved work in the same agent may observe the
108/// temporary policy until the guard is dropped.
109pub async fn with_named_policy_async<R, F: std::future::Future<Output = R>>(
110    policy: &NamedPolicy,
111    f: impl FnOnce() -> F,
112) -> Result<R, RetryBuilderError> {
113    let _guard = use_named_policy(policy)?;
114    Ok(f().await)
115}
116
117pub mod builder {
118    use super::retry_api;
119    use std::error::Error;
120    use std::fmt;
121    use std::time::Duration;
122
123    /// Standard retry-property keys populated by the platform retry context.
124    pub struct Props;
125
126    impl Props {
127        pub const VERB: &str = "verb";
128        pub const NOUN_URI: &str = "noun-uri";
129        pub const URI_SCHEME: &str = "uri-scheme";
130        pub const URI_HOST: &str = "uri-host";
131        pub const URI_PORT: &str = "uri-port";
132        pub const URI_PATH: &str = "uri-path";
133        pub const STATUS_CODE: &str = "status-code";
134        pub const ERROR_TYPE: &str = "error-type";
135        pub const FUNCTION: &str = "function";
136        pub const TARGET_COMPONENT_ID: &str = "target-component-id";
137        pub const TARGET_AGENT_TYPE: &str = "target-agent-type";
138        pub const DB_TYPE: &str = "db-type";
139        pub const TRAP_TYPE: &str = "trap-type";
140    }
141
142    /// High-level predicate value accepted by the retry builder layer.
143    #[derive(Clone, Debug, PartialEq, Eq)]
144    pub enum Value {
145        Text(String),
146        Integer(i128),
147        Boolean(bool),
148    }
149
150    impl From<String> for Value {
151        fn from(value: String) -> Self {
152            Self::Text(value)
153        }
154    }
155
156    impl From<&str> for Value {
157        fn from(value: &str) -> Self {
158            Self::Text(value.to_string())
159        }
160    }
161
162    impl From<bool> for Value {
163        fn from(value: bool) -> Self {
164            Self::Boolean(value)
165        }
166    }
167
168    impl From<i8> for Value {
169        fn from(value: i8) -> Self {
170            Self::Integer(value.into())
171        }
172    }
173
174    impl From<i16> for Value {
175        fn from(value: i16) -> Self {
176            Self::Integer(value.into())
177        }
178    }
179
180    impl From<i32> for Value {
181        fn from(value: i32) -> Self {
182            Self::Integer(value.into())
183        }
184    }
185
186    impl From<i64> for Value {
187        fn from(value: i64) -> Self {
188            Self::Integer(value.into())
189        }
190    }
191
192    impl From<isize> for Value {
193        fn from(value: isize) -> Self {
194            Self::Integer(value as i128)
195        }
196    }
197
198    impl From<u8> for Value {
199        fn from(value: u8) -> Self {
200            Self::Integer(value.into())
201        }
202    }
203
204    impl From<u16> for Value {
205        fn from(value: u16) -> Self {
206            Self::Integer(value.into())
207        }
208    }
209
210    impl From<u32> for Value {
211        fn from(value: u32) -> Self {
212            Self::Integer(value.into())
213        }
214    }
215
216    impl From<u64> for Value {
217        fn from(value: u64) -> Self {
218            Self::Integer(value.into())
219        }
220    }
221
222    impl From<usize> for Value {
223        fn from(value: usize) -> Self {
224            Self::Integer(value as i128)
225        }
226    }
227
228    /// High-level retry predicate that keeps tree structure until conversion.
229    #[derive(Clone, Debug, PartialEq)]
230    pub enum Predicate {
231        Eq {
232            property: String,
233            value: Value,
234        },
235        Neq {
236            property: String,
237            value: Value,
238        },
239        Gt {
240            property: String,
241            value: Value,
242        },
243        Gte {
244            property: String,
245            value: Value,
246        },
247        Lt {
248            property: String,
249            value: Value,
250        },
251        Lte {
252            property: String,
253            value: Value,
254        },
255        Exists(String),
256        OneOf {
257            property: String,
258            values: Vec<Value>,
259        },
260        MatchesGlob {
261            property: String,
262            pattern: String,
263        },
264        StartsWith {
265            property: String,
266            prefix: String,
267        },
268        Contains {
269            property: String,
270            substring: String,
271        },
272        And(Box<Predicate>, Box<Predicate>),
273        Or(Box<Predicate>, Box<Predicate>),
274        Not(Box<Predicate>),
275        Always,
276        Never,
277    }
278
279    impl Predicate {
280        pub fn always() -> Self {
281            Self::Always
282        }
283
284        pub fn never() -> Self {
285            Self::Never
286        }
287
288        pub fn eq(property: impl Into<String>, value: impl Into<Value>) -> Self {
289            Self::Eq {
290                property: property.into(),
291                value: value.into(),
292            }
293        }
294
295        pub fn neq(property: impl Into<String>, value: impl Into<Value>) -> Self {
296            Self::Neq {
297                property: property.into(),
298                value: value.into(),
299            }
300        }
301
302        pub fn gt(property: impl Into<String>, value: impl Into<Value>) -> Self {
303            Self::Gt {
304                property: property.into(),
305                value: value.into(),
306            }
307        }
308
309        pub fn gte(property: impl Into<String>, value: impl Into<Value>) -> Self {
310            Self::Gte {
311                property: property.into(),
312                value: value.into(),
313            }
314        }
315
316        pub fn lt(property: impl Into<String>, value: impl Into<Value>) -> Self {
317            Self::Lt {
318                property: property.into(),
319                value: value.into(),
320            }
321        }
322
323        pub fn lte(property: impl Into<String>, value: impl Into<Value>) -> Self {
324            Self::Lte {
325                property: property.into(),
326                value: value.into(),
327            }
328        }
329
330        pub fn exists(property: impl Into<String>) -> Self {
331            Self::Exists(property.into())
332        }
333
334        pub fn one_of<I, V>(property: impl Into<String>, values: I) -> Self
335        where
336            I: IntoIterator<Item = V>,
337            V: Into<Value>,
338        {
339            Self::OneOf {
340                property: property.into(),
341                values: values.into_iter().map(Into::into).collect(),
342            }
343        }
344
345        pub fn matches_glob(property: impl Into<String>, pattern: impl Into<String>) -> Self {
346            Self::MatchesGlob {
347                property: property.into(),
348                pattern: pattern.into(),
349            }
350        }
351
352        pub fn starts_with(property: impl Into<String>, prefix: impl Into<String>) -> Self {
353            Self::StartsWith {
354                property: property.into(),
355                prefix: prefix.into(),
356            }
357        }
358
359        pub fn contains(property: impl Into<String>, substring: impl Into<String>) -> Self {
360            Self::Contains {
361                property: property.into(),
362                substring: substring.into(),
363            }
364        }
365
366        pub fn and(left: Predicate, right: Predicate) -> Self {
367            Self::And(Box::new(left), Box::new(right))
368        }
369
370        pub fn or(left: Predicate, right: Predicate) -> Self {
371            Self::Or(Box::new(left), Box::new(right))
372        }
373
374        #[allow(clippy::should_implement_trait)]
375        pub fn not(inner: Predicate) -> Self {
376            Self::Not(Box::new(inner))
377        }
378
379        pub fn try_to_raw(&self) -> Result<retry_api::RetryPredicate, RetryBuilderError> {
380            self.clone().try_into()
381        }
382    }
383
384    /// High-level retry policy that keeps tree structure until conversion.
385    #[derive(Clone, Debug, PartialEq)]
386    pub enum Policy {
387        Periodic(Duration),
388        Exponential {
389            base_delay: Duration,
390            factor: f64,
391        },
392        Fibonacci {
393            first: Duration,
394            second: Duration,
395        },
396        Immediate,
397        Never,
398        CountBox {
399            max_retries: u32,
400            inner: Box<Policy>,
401        },
402        TimeBox {
403            limit: Duration,
404            inner: Box<Policy>,
405        },
406        Clamp {
407            min_delay: Duration,
408            max_delay: Duration,
409            inner: Box<Policy>,
410        },
411        AddDelay {
412            delay: Duration,
413            inner: Box<Policy>,
414        },
415        Jitter {
416            factor: f64,
417            inner: Box<Policy>,
418        },
419        OnlyWhen {
420            predicate: Predicate,
421            inner: Box<Policy>,
422        },
423        AndThen(Box<Policy>, Box<Policy>),
424        Union(Box<Policy>, Box<Policy>),
425        Intersect(Box<Policy>, Box<Policy>),
426    }
427
428    impl Policy {
429        pub fn immediate() -> Self {
430            Self::Immediate
431        }
432
433        pub fn never() -> Self {
434            Self::Never
435        }
436
437        pub fn periodic(delay: Duration) -> Self {
438            Self::Periodic(delay)
439        }
440
441        pub fn exponential(base_delay: Duration, factor: f64) -> Self {
442            Self::Exponential { base_delay, factor }
443        }
444
445        pub fn fibonacci(first: Duration, second: Duration) -> Self {
446            Self::Fibonacci { first, second }
447        }
448
449        pub fn max_retries(self, max_retries: u32) -> Self {
450            Self::CountBox {
451                max_retries,
452                inner: Box::new(self),
453            }
454        }
455
456        pub fn within(self, limit: Duration) -> Self {
457            Self::TimeBox {
458                limit,
459                inner: Box::new(self),
460            }
461        }
462
463        pub fn clamp(self, min_delay: Duration, max_delay: Duration) -> Self {
464            Self::Clamp {
465                min_delay,
466                max_delay,
467                inner: Box::new(self),
468            }
469        }
470
471        pub fn add_delay(self, delay: Duration) -> Self {
472            Self::AddDelay {
473                delay,
474                inner: Box::new(self),
475            }
476        }
477
478        pub fn with_jitter(self, factor: f64) -> Self {
479            Self::Jitter {
480                factor,
481                inner: Box::new(self),
482            }
483        }
484
485        pub fn only_when(self, predicate: Predicate) -> Self {
486            Self::OnlyWhen {
487                predicate,
488                inner: Box::new(self),
489            }
490        }
491
492        pub fn and_then(self, other: Policy) -> Self {
493            Self::AndThen(Box::new(self), Box::new(other))
494        }
495
496        pub fn union(self, other: Policy) -> Self {
497            Self::Union(Box::new(self), Box::new(other))
498        }
499
500        pub fn intersect(self, other: Policy) -> Self {
501            Self::Intersect(Box::new(self), Box::new(other))
502        }
503
504        pub fn try_to_raw(&self) -> Result<retry_api::RetryPolicy, RetryBuilderError> {
505            self.clone().try_into()
506        }
507    }
508
509    /// High-level named retry rule with an outer applicability predicate.
510    #[derive(Clone, Debug, PartialEq)]
511    pub struct NamedPolicy {
512        name: String,
513        priority: u32,
514        predicate: Predicate,
515        policy: Policy,
516    }
517
518    impl NamedPolicy {
519        pub fn named(name: impl Into<String>, policy: Policy) -> Self {
520            Self {
521                name: name.into(),
522                priority: 0,
523                predicate: Predicate::always(),
524                policy,
525            }
526        }
527
528        pub fn priority(mut self, priority: u32) -> Self {
529            self.priority = priority;
530            self
531        }
532
533        pub fn applies_when(mut self, predicate: Predicate) -> Self {
534            self.predicate = predicate;
535            self
536        }
537
538        pub fn try_to_raw(&self) -> Result<retry_api::NamedRetryPolicy, RetryBuilderError> {
539            self.clone().try_into()
540        }
541    }
542
543    /// Validation errors produced when flattening high-level retry builders into raw WIT types.
544    #[derive(Clone, Debug, PartialEq)]
545    pub enum RetryBuilderError {
546        IntegerOutOfRange {
547            value: i128,
548        },
549        DurationOutOfRange {
550            field: &'static str,
551            duration: Duration,
552        },
553        InvalidExponentialFactor {
554            factor: f64,
555        },
556        InvalidJitterFactor {
557            factor: f64,
558        },
559        InvalidClampRange {
560            min_delay: Duration,
561            max_delay: Duration,
562        },
563    }
564
565    impl fmt::Display for RetryBuilderError {
566        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
567            match self {
568                Self::IntegerOutOfRange { value } => {
569                    write!(
570                        f,
571                        "predicate integer value {value} does not fit into WIT s64"
572                    )
573                }
574                Self::DurationOutOfRange { field, duration } => write!(
575                    f,
576                    "{field} duration {duration:?} does not fit into WIT duration nanoseconds"
577                ),
578                Self::InvalidExponentialFactor { factor } => write!(
579                    f,
580                    "exponential factor must be finite and greater than 0, got {factor}"
581                ),
582                Self::InvalidJitterFactor { factor } => write!(
583                    f,
584                    "jitter factor must be finite and greater than or equal to 0, got {factor}"
585                ),
586                Self::InvalidClampRange {
587                    min_delay,
588                    max_delay,
589                } => write!(
590                    f,
591                    "clamp min delay {min_delay:?} must be less than or equal to max delay {max_delay:?}"
592                ),
593            }
594        }
595    }
596
597    impl Error for RetryBuilderError {}
598
599    impl TryFrom<Value> for retry_api::PredicateValue {
600        type Error = RetryBuilderError;
601
602        fn try_from(value: Value) -> Result<Self, Self::Error> {
603            match value {
604                Value::Text(value) => Ok(Self::Text(value)),
605                Value::Boolean(value) => Ok(Self::Boolean(value)),
606                Value::Integer(value) => i64::try_from(value)
607                    .map(Self::Integer)
608                    .map_err(|_| RetryBuilderError::IntegerOutOfRange { value }),
609            }
610        }
611    }
612
613    impl TryFrom<Predicate> for retry_api::RetryPredicate {
614        type Error = RetryBuilderError;
615
616        fn try_from(predicate: Predicate) -> Result<Self, Self::Error> {
617            let mut nodes = Vec::new();
618            push_predicate_node(predicate, &mut nodes)?;
619            Ok(Self { nodes })
620        }
621    }
622
623    impl TryFrom<Policy> for retry_api::RetryPolicy {
624        type Error = RetryBuilderError;
625
626        fn try_from(policy: Policy) -> Result<Self, Self::Error> {
627            let mut nodes = Vec::new();
628            push_policy_node(policy, &mut nodes)?;
629            Ok(Self { nodes })
630        }
631    }
632
633    impl TryFrom<NamedPolicy> for retry_api::NamedRetryPolicy {
634        type Error = RetryBuilderError;
635
636        fn try_from(policy: NamedPolicy) -> Result<Self, Self::Error> {
637            Ok(Self {
638                name: policy.name,
639                priority: policy.priority,
640                predicate: policy.predicate.try_to_raw()?,
641                policy: policy.policy.try_to_raw()?,
642            })
643        }
644    }
645
646    fn push_predicate_node(
647        predicate: Predicate,
648        nodes: &mut Vec<retry_api::PredicateNode>,
649    ) -> Result<i32, RetryBuilderError> {
650        let index = nodes.len() as i32;
651        nodes.push(retry_api::PredicateNode::PredFalse);
652
653        let node = match predicate {
654            Predicate::Eq { property, value } => {
655                retry_api::PredicateNode::PropEq(retry_api::PropertyComparison {
656                    property_name: property,
657                    value: value.try_into()?,
658                })
659            }
660            Predicate::Neq { property, value } => {
661                retry_api::PredicateNode::PropNeq(retry_api::PropertyComparison {
662                    property_name: property,
663                    value: value.try_into()?,
664                })
665            }
666            Predicate::Gt { property, value } => {
667                retry_api::PredicateNode::PropGt(retry_api::PropertyComparison {
668                    property_name: property,
669                    value: value.try_into()?,
670                })
671            }
672            Predicate::Gte { property, value } => {
673                retry_api::PredicateNode::PropGte(retry_api::PropertyComparison {
674                    property_name: property,
675                    value: value.try_into()?,
676                })
677            }
678            Predicate::Lt { property, value } => {
679                retry_api::PredicateNode::PropLt(retry_api::PropertyComparison {
680                    property_name: property,
681                    value: value.try_into()?,
682                })
683            }
684            Predicate::Lte { property, value } => {
685                retry_api::PredicateNode::PropLte(retry_api::PropertyComparison {
686                    property_name: property,
687                    value: value.try_into()?,
688                })
689            }
690            Predicate::Exists(property) => retry_api::PredicateNode::PropExists(property),
691            Predicate::OneOf { property, values } => {
692                retry_api::PredicateNode::PropIn(retry_api::PropertySetCheck {
693                    property_name: property,
694                    values: values
695                        .into_iter()
696                        .map(TryInto::try_into)
697                        .collect::<Result<Vec<_>, _>>()?,
698                })
699            }
700            Predicate::MatchesGlob { property, pattern } => {
701                retry_api::PredicateNode::PropMatches(retry_api::PropertyPattern {
702                    property_name: property,
703                    pattern,
704                })
705            }
706            Predicate::StartsWith { property, prefix } => {
707                retry_api::PredicateNode::PropStartsWith(retry_api::PropertyPattern {
708                    property_name: property,
709                    pattern: prefix,
710                })
711            }
712            Predicate::Contains {
713                property,
714                substring,
715            } => retry_api::PredicateNode::PropContains(retry_api::PropertyPattern {
716                property_name: property,
717                pattern: substring,
718            }),
719            Predicate::And(left, right) => {
720                let left_index = push_predicate_node(*left, nodes)?;
721                let right_index = push_predicate_node(*right, nodes)?;
722                retry_api::PredicateNode::PredAnd((left_index, right_index))
723            }
724            Predicate::Or(left, right) => {
725                let left_index = push_predicate_node(*left, nodes)?;
726                let right_index = push_predicate_node(*right, nodes)?;
727                retry_api::PredicateNode::PredOr((left_index, right_index))
728            }
729            Predicate::Not(inner) => {
730                let inner_index = push_predicate_node(*inner, nodes)?;
731                retry_api::PredicateNode::PredNot(inner_index)
732            }
733            Predicate::Always => retry_api::PredicateNode::PredTrue,
734            Predicate::Never => retry_api::PredicateNode::PredFalse,
735        };
736
737        nodes[index as usize] = node;
738        Ok(index)
739    }
740
741    fn push_policy_node(
742        policy: Policy,
743        nodes: &mut Vec<retry_api::PolicyNode>,
744    ) -> Result<i32, RetryBuilderError> {
745        let index = nodes.len() as i32;
746        nodes.push(retry_api::PolicyNode::Never);
747
748        let node = match policy {
749            Policy::Periodic(delay) => {
750                retry_api::PolicyNode::Periodic(duration_to_wit(delay, "periodic delay")?)
751            }
752            Policy::Exponential { base_delay, factor } => {
753                if !factor.is_finite() || factor <= 0.0 {
754                    return Err(RetryBuilderError::InvalidExponentialFactor { factor });
755                }
756
757                retry_api::PolicyNode::Exponential(retry_api::ExponentialConfig {
758                    base_delay: duration_to_wit(base_delay, "exponential base delay")?,
759                    factor,
760                })
761            }
762            Policy::Fibonacci { first, second } => {
763                retry_api::PolicyNode::Fibonacci(retry_api::FibonacciConfig {
764                    first: duration_to_wit(first, "fibonacci first delay")?,
765                    second: duration_to_wit(second, "fibonacci second delay")?,
766                })
767            }
768            Policy::Immediate => retry_api::PolicyNode::Immediate,
769            Policy::Never => retry_api::PolicyNode::Never,
770            Policy::CountBox { max_retries, inner } => {
771                let inner_index = push_policy_node(*inner, nodes)?;
772                retry_api::PolicyNode::CountBox(retry_api::CountBoxConfig {
773                    max_retries,
774                    inner: inner_index,
775                })
776            }
777            Policy::TimeBox { limit, inner } => {
778                let inner_index = push_policy_node(*inner, nodes)?;
779                retry_api::PolicyNode::TimeBox(retry_api::TimeBoxConfig {
780                    limit: duration_to_wit(limit, "time-box limit")?,
781                    inner: inner_index,
782                })
783            }
784            Policy::Clamp {
785                min_delay,
786                max_delay,
787                inner,
788            } => {
789                if min_delay > max_delay {
790                    return Err(RetryBuilderError::InvalidClampRange {
791                        min_delay,
792                        max_delay,
793                    });
794                }
795
796                let inner_index = push_policy_node(*inner, nodes)?;
797                retry_api::PolicyNode::ClampDelay(retry_api::ClampConfig {
798                    min_delay: duration_to_wit(min_delay, "clamp min delay")?,
799                    max_delay: duration_to_wit(max_delay, "clamp max delay")?,
800                    inner: inner_index,
801                })
802            }
803            Policy::AddDelay { delay, inner } => {
804                let inner_index = push_policy_node(*inner, nodes)?;
805                retry_api::PolicyNode::AddDelay(retry_api::AddDelayConfig {
806                    delay: duration_to_wit(delay, "added delay")?,
807                    inner: inner_index,
808                })
809            }
810            Policy::Jitter { factor, inner } => {
811                if !factor.is_finite() || factor < 0.0 {
812                    return Err(RetryBuilderError::InvalidJitterFactor { factor });
813                }
814
815                let inner_index = push_policy_node(*inner, nodes)?;
816                retry_api::PolicyNode::Jitter(retry_api::JitterConfig {
817                    factor,
818                    inner: inner_index,
819                })
820            }
821            Policy::OnlyWhen { predicate, inner } => {
822                let inner_index = push_policy_node(*inner, nodes)?;
823                retry_api::PolicyNode::FilteredOn(retry_api::FilteredConfig {
824                    predicate: predicate.try_to_raw()?,
825                    inner: inner_index,
826                })
827            }
828            Policy::AndThen(left, right) => {
829                let left_index = push_policy_node(*left, nodes)?;
830                let right_index = push_policy_node(*right, nodes)?;
831                retry_api::PolicyNode::AndThen((left_index, right_index))
832            }
833            Policy::Union(left, right) => {
834                let left_index = push_policy_node(*left, nodes)?;
835                let right_index = push_policy_node(*right, nodes)?;
836                retry_api::PolicyNode::PolicyUnion((left_index, right_index))
837            }
838            Policy::Intersect(left, right) => {
839                let left_index = push_policy_node(*left, nodes)?;
840                let right_index = push_policy_node(*right, nodes)?;
841                retry_api::PolicyNode::PolicyIntersect((left_index, right_index))
842            }
843        };
844
845        nodes[index as usize] = node;
846        Ok(index)
847    }
848
849    fn duration_to_wit(duration: Duration, field: &'static str) -> Result<u64, RetryBuilderError> {
850        let nanos = duration.as_nanos();
851        if nanos > u64::MAX as u128 {
852            return Err(RetryBuilderError::DurationOutOfRange { field, duration });
853        }
854
855        Ok(nanos as u64)
856    }
857}
858
859#[cfg(test)]
860mod tests {
861    use std::time::Duration;
862
863    use test_r::test;
864
865    use super::builder::{NamedPolicy, Policy, Predicate, Props, RetryBuilderError};
866    use super::retry_api;
867
868    #[test]
869    fn named_policy_builder_flattens_to_raw_wit_types() {
870        let named = NamedPolicy::named(
871            "transient-http",
872            Policy::exponential(Duration::from_millis(200), 2.0)
873                .clamp(Duration::from_millis(100), Duration::from_secs(5))
874                .only_when(Predicate::one_of(Props::STATUS_CODE, [502_u16, 503, 504]))
875                .max_retries(3),
876        )
877        .priority(10)
878        .applies_when(Predicate::and(
879            Predicate::eq(Props::VERB, "GET"),
880            Predicate::matches_glob(Props::URI_HOST, "*.example.com"),
881        ));
882
883        let raw = named
884            .try_to_raw()
885            .expect("builder should convert to raw WIT types");
886
887        assert_eq!(raw.name, "transient-http");
888        assert_eq!(raw.priority, 10);
889        assert_eq!(raw.predicate.nodes.len(), 3);
890        match &raw.predicate.nodes[0] {
891            retry_api::PredicateNode::PredAnd((left, right)) => {
892                assert_eq!((*left, *right), (1, 2));
893            }
894            node => panic!("unexpected root predicate node: {node:?}"),
895        }
896        match &raw.predicate.nodes[1] {
897            retry_api::PredicateNode::PropEq(retry_api::PropertyComparison {
898                property_name,
899                value: retry_api::PredicateValue::Text(value),
900            }) => {
901                assert_eq!(property_name, Props::VERB);
902                assert_eq!(value, "GET");
903            }
904            node => panic!("unexpected left predicate node: {node:?}"),
905        }
906        match &raw.predicate.nodes[2] {
907            retry_api::PredicateNode::PropMatches(retry_api::PropertyPattern {
908                property_name,
909                pattern,
910            }) => {
911                assert_eq!(property_name, Props::URI_HOST);
912                assert_eq!(pattern, "*.example.com");
913            }
914            node => panic!("unexpected right predicate node: {node:?}"),
915        }
916
917        assert_eq!(raw.policy.nodes.len(), 4);
918        match &raw.policy.nodes[0] {
919            retry_api::PolicyNode::CountBox(retry_api::CountBoxConfig { max_retries, inner }) => {
920                assert_eq!((*max_retries, *inner), (3, 1));
921            }
922            node => panic!("unexpected root policy node: {node:?}"),
923        }
924        match &raw.policy.nodes[1] {
925            retry_api::PolicyNode::FilteredOn(retry_api::FilteredConfig { predicate, inner }) => {
926                assert_eq!(*inner, 2);
927                assert_eq!(predicate.nodes.len(), 1);
928                match &predicate.nodes[0] {
929                    retry_api::PredicateNode::PropIn(retry_api::PropertySetCheck {
930                        property_name,
931                        values,
932                    }) => {
933                        assert_eq!(property_name, Props::STATUS_CODE);
934                        assert_eq!(values.len(), 3);
935                        match &values[..] {
936                            [
937                                retry_api::PredicateValue::Integer(502),
938                                retry_api::PredicateValue::Integer(503),
939                                retry_api::PredicateValue::Integer(504),
940                            ] => {}
941                            other => panic!("unexpected predicate values: {other:?}"),
942                        }
943                    }
944                    node => panic!("unexpected filtered predicate node: {node:?}"),
945                }
946            }
947            node => panic!("unexpected filtered policy node: {node:?}"),
948        }
949        match &raw.policy.nodes[2] {
950            retry_api::PolicyNode::ClampDelay(retry_api::ClampConfig {
951                min_delay,
952                max_delay,
953                inner,
954            }) => {
955                assert_eq!(
956                    (*min_delay, *max_delay, *inner),
957                    (100_000_000, 5_000_000_000, 3)
958                );
959            }
960            node => panic!("unexpected clamp policy node: {node:?}"),
961        }
962        match &raw.policy.nodes[3] {
963            retry_api::PolicyNode::Exponential(retry_api::ExponentialConfig {
964                base_delay,
965                factor,
966            }) => {
967                assert_eq!(*base_delay, 200_000_000);
968                assert_eq!(*factor, 2.0);
969            }
970            node => panic!("unexpected exponential policy node: {node:?}"),
971        }
972    }
973
974    #[test]
975    fn predicate_builder_rejects_integer_values_that_do_not_fit_wit() {
976        let predicate = Predicate::eq(Props::STATUS_CODE, u64::try_from(i64::MAX).unwrap() + 1);
977
978        assert_eq!(
979            predicate.try_to_raw().unwrap_err(),
980            RetryBuilderError::IntegerOutOfRange {
981                value: i128::from(i64::MAX) + 1,
982            }
983        );
984    }
985
986    #[test]
987    fn policy_builder_rejects_invalid_exponential_factors() {
988        let policy = Policy::exponential(Duration::from_millis(100), 0.0);
989
990        assert_eq!(
991            policy.try_to_raw().unwrap_err(),
992            RetryBuilderError::InvalidExponentialFactor { factor: 0.0 }
993        );
994
995        assert!(matches!(
996            Policy::exponential(Duration::from_millis(100), f64::NAN)
997                .try_to_raw()
998                .unwrap_err(),
999            RetryBuilderError::InvalidExponentialFactor { factor } if factor.is_nan()
1000        ));
1001    }
1002
1003    #[test]
1004    fn policy_builder_rejects_invalid_jitter_factors() {
1005        let policy = Policy::periodic(Duration::from_millis(100)).with_jitter(-0.1);
1006
1007        assert_eq!(
1008            policy.try_to_raw().unwrap_err(),
1009            RetryBuilderError::InvalidJitterFactor { factor: -0.1 }
1010        );
1011    }
1012
1013    #[test]
1014    fn policy_builder_rejects_invalid_clamp_ranges() {
1015        let policy = Policy::periodic(Duration::from_millis(100))
1016            .clamp(Duration::from_secs(2), Duration::from_secs(1));
1017
1018        assert_eq!(
1019            policy.try_to_raw().unwrap_err(),
1020            RetryBuilderError::InvalidClampRange {
1021                min_delay: Duration::from_secs(2),
1022                max_delay: Duration::from_secs(1),
1023            }
1024        );
1025    }
1026
1027    #[test]
1028    fn policy_builder_rejects_durations_that_do_not_fit_wit() {
1029        let policy = Policy::periodic(Duration::MAX);
1030
1031        assert_eq!(
1032            policy.try_to_raw().unwrap_err(),
1033            RetryBuilderError::DurationOutOfRange {
1034                field: "periodic delay",
1035                duration: Duration::MAX,
1036            }
1037        );
1038    }
1039}