fundaia 0.55.0

Command line for the Fundaia deployment platform: projects, services, variables, deployments, logs and metrics
use super::*;

#[derive(Debug, Default, Clone, Copy)]
pub struct RequestFilter<'a> {
    pub status: Option<&'a str>,
    pub method: Option<&'a str>,
    pub path: Option<&'a str>,
    pub limit: Option<usize>,
}

impl RequestFilter<'_> {
    pub fn query(&self) -> String {
        let mut pairs: Vec<String> = Vec::new();
        for (name, value) in [
            ("status", self.status),
            ("method", self.method),
            ("path", self.path),
        ] {
            if let Some(value) = value.map(str::trim).filter(|value| !value.is_empty()) {
                pairs.push(format!(
                    "{name}={}",
                    utf8_percent_encode(value, NON_ALPHANUMERIC)
                ));
            }
        }
        if let Some(limit) = self.limit {
            pairs.push(format!("limit={limit}"));
        }

        if pairs.is_empty() {
            String::new()
        } else {
            format!("?{}", pairs.join("&"))
        }
    }
}

impl ApiClient {
    pub async fn requests(
        &self,
        service_environment_id: &str,
        filter: RequestFilter<'_>,
    ) -> Result<Answer<RequestWindow>> {
        self.answer(
            Method::GET,
            &format!(
                "/api/infra/services/{service_environment_id}/requests{}",
                filter.query()
            ),
            None::<&()>,
        )
        .await
    }

    pub async fn stream_requests<F>(
        &self,
        service_environment_id: &str,
        filter: RequestFilter<'_>,
        mut on_request: F,
    ) -> Result<()>
    where
        F: FnMut(InspectedRequest),
    {
        let path = format!(
            "/api/infra/services/{service_environment_id}/requests/stream{}",
            filter.query()
        );

        self.read_events(&path, |event| {
            if let Some(request) = parse_request_event(event) {
                on_request(request);
            }
            ControlFlow::Continue(())
        })
        .await
    }
}

fn parse_request_event(event: &str) -> Option<InspectedRequest> {
    if !event.lines().any(|line| line == "event: request") {
        return None;
    }
    serde_json::from_str::<InspectedRequest>(&payload_of(event)?).ok()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_should_ask_for_nothing_without_a_filter() {
        assert_eq!(RequestFilter::default().query(), "");
    }

    #[test]
    fn it_should_encode_the_piece_of_path() {
        let filter = RequestFilter {
            status: Some("5xx"),
            path: Some("/api?x"),
            limit: Some(20),
            ..RequestFilter::default()
        };
        assert_eq!(filter.query(), "?status=5xx&path=%2Fapi%3Fx&limit=20");
    }

    #[test]
    fn it_should_read_a_request_from_its_event() {
        let event = "id: 7\nevent: request\ndata: {\"sequence\":7,\"at\":\"2026-10-16T10:00:00.000Z\",\"method\":\"GET\",\"path\":\"/\",\"status\":200,\"durationMillis\":12,\"clientAddress\":null,\"userAgent\":null,\"cached\":false,\"woke\":false,\"webSocket\":false,\"blockedBy\":null}";
        assert_eq!(
            parse_request_event(event).map(|request| request.status),
            Some(200)
        );
    }

    #[test]
    fn it_should_ignore_a_keep_alive() {
        assert!(parse_request_event(": keep-alive").is_none());
    }
}