Skip to main content

floopy/resources/
experiments.rs

1use std::sync::Arc;
2
3use futures::Stream;
4use reqwest::Method;
5
6use crate::constants::{
7    experiment_results, experiment_rollback, CONFIRM_EXPERIMENTS, ENDPOINT_EXPERIMENTS,
8    HEADER_CONFIRM,
9};
10use crate::error::Result;
11use crate::http::HttpTransport;
12use crate::options::RequestOptions;
13use crate::types::{
14    Experiment, ExperimentCreateParams, ExperimentListPage, ExperimentListParams, ExperimentResults,
15};
16
17use super::require;
18
19/// Manages A/B routing experiments.
20pub struct Experiments {
21    t: Arc<HttpTransport>,
22}
23
24/// Inject `X-Floopy-Confirm: experiments` (gateway SEC-009 requires it on
25/// create/rollback) without dropping the caller's overrides.
26fn with_confirm(req: Option<RequestOptions>) -> RequestOptions {
27    let mut req = req.unwrap_or_default();
28    req = req.header(HEADER_CONFIRM, CONFIRM_EXPERIMENTS);
29    req
30}
31
32impl Experiments {
33    pub(crate) fn new(t: Arc<HttpTransport>) -> Self {
34        Self { t }
35    }
36
37    /// Fetch a single page of experiments.
38    ///
39    /// # Errors
40    /// Returns an [`Error`](crate::Error) on a non-2xx response or transport
41    /// failure.
42    pub async fn list(
43        &self,
44        params: &ExperimentListParams,
45        req: impl Into<Option<RequestOptions>>,
46    ) -> Result<ExperimentListPage> {
47        let (data, _) = self
48            .t
49            .request(
50                Method::GET,
51                ENDPOINT_EXPERIMENTS,
52                None,
53                &params.query(),
54                req.into().as_ref(),
55            )
56            .await?;
57        require(data)
58    }
59
60    /// Stream one [`ExperimentListPage`] per round-trip until exhausted.
61    pub fn pages(
62        &self,
63        params: ExperimentListParams,
64        req: Option<RequestOptions>,
65    ) -> impl Stream<Item = Result<ExperimentListPage>> + Send + 'static {
66        let t = self.t.clone();
67        async_stream::try_stream! {
68            let mut params = params;
69            loop {
70                let (data, _) = t
71                    .request::<ExperimentListPage>(
72                        Method::GET,
73                        ENDPOINT_EXPERIMENTS,
74                        None,
75                        &params.query(),
76                        req.as_ref(),
77                    )
78                    .await?;
79                let page = require(data)?;
80                let next = page.next_cursor.clone();
81                let has_more = page.has_more;
82                yield page;
83                match next {
84                    Some(cursor) if has_more && !cursor.is_empty() => {
85                        params.cursor = Some(cursor);
86                    }
87                    _ => break,
88                }
89            }
90        }
91    }
92
93    /// Create an experiment. The `X-Floopy-Confirm: experiments` header is
94    /// injected automatically.
95    ///
96    /// # Errors
97    /// Returns an [`Error`](crate::Error) on a non-2xx response or transport
98    /// failure.
99    pub async fn create(
100        &self,
101        params: ExperimentCreateParams,
102        req: Option<RequestOptions>,
103    ) -> Result<Experiment> {
104        let body =
105            serde_json::to_value(&params).map_err(|e| crate::Error::Decode(e.to_string()))?;
106        let (data, _) = self
107            .t
108            .request(
109                Method::POST,
110                ENDPOINT_EXPERIMENTS,
111                Some(&body),
112                &[],
113                Some(&with_confirm(req)),
114            )
115            .await?;
116        require(data)
117    }
118
119    /// Roll back an experiment. The `X-Floopy-Confirm: experiments` header
120    /// is injected automatically.
121    ///
122    /// # Errors
123    /// Returns an [`Error`](crate::Error) on a non-2xx response or transport
124    /// failure.
125    pub async fn rollback(
126        &self,
127        experiment_id: &str,
128        req: Option<RequestOptions>,
129    ) -> Result<Experiment> {
130        let (data, _) = self
131            .t
132            .request(
133                Method::POST,
134                &experiment_rollback(experiment_id),
135                None,
136                &[],
137                Some(&with_confirm(req)),
138            )
139            .await?;
140        require(data)
141    }
142
143    /// Fetch the computed outcome of an experiment.
144    ///
145    /// # Errors
146    /// Returns an [`Error`](crate::Error) on a non-2xx response or transport
147    /// failure.
148    pub async fn results(
149        &self,
150        experiment_id: &str,
151        req: impl Into<Option<RequestOptions>>,
152    ) -> Result<ExperimentResults> {
153        let (data, _) = self
154            .t
155            .request(
156                Method::GET,
157                &experiment_results(experiment_id),
158                None,
159                &[],
160                req.into().as_ref(),
161            )
162            .await?;
163        require(data)
164    }
165}