cufflink-cli 0.23.0

CLI for the Cufflink CRUD microservice platform — deploy, init, and manage services
use crate::config::CliConfig;
use serde_json::Value;
use std::sync::{Arc, Mutex, PoisonError};

#[derive(Clone, Default)]
pub(crate) struct Asked(Arc<Mutex<Vec<(String, Value)>>>);

impl Asked {
    fn recorded(&self) -> Vec<(String, Value)> {
        self.0
            .lock()
            .unwrap_or_else(PoisonError::into_inner)
            .clone()
    }

    pub(crate) fn requests(&self) -> Vec<String> {
        self.recorded()
            .into_iter()
            .map(|(asked, _)| asked)
            .collect()
    }

    pub(crate) fn bodies(&self) -> Vec<Value> {
        self.recorded().into_iter().map(|(_, body)| body).collect()
    }

    pub(crate) fn count(&self) -> usize {
        self.recorded().len()
    }

    fn record(&self, request: &[u8]) {
        let (head, body) =
            head_end(request).map_or((request, &[][..]), |end| request.split_at(end));
        let asked = String::from_utf8_lossy(head)
            .lines()
            .next()
            .and_then(|line| line.rsplit_once(' '))
            .map(|(asked, _)| asked.to_string())
            .unwrap_or_default();
        self.0
            .lock()
            .unwrap_or_else(PoisonError::into_inner)
            .push((asked, serde_json::from_slice(body).unwrap_or(Value::Null)));
    }
}

fn head_end(request: &[u8]) -> Option<usize> {
    request
        .windows(4)
        .position(|w| w == b"\r\n\r\n")
        .map(|head| head + 4)
}

fn content_length(head: &[u8]) -> usize {
    String::from_utf8_lossy(head)
        .lines()
        .filter_map(|line| line.split_once(':'))
        .find(|(name, _)| name.eq_ignore_ascii_case("content-length"))
        .and_then(|(_, length)| length.trim().parse().ok())
        .unwrap_or(0)
}

pub(crate) async fn answering(answers: Vec<Option<(u16, Value)>>) -> (CliConfig, Asked) {
    use tokio::io::{AsyncReadExt, AsyncWriteExt};
    let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
        .await
        .expect("bind");
    let address = listener.local_addr().expect("address");
    let asked = Asked::default();
    let recorded = asked.clone();
    tokio::spawn(async move {
        for answer in answers {
            let Ok((mut socket, _)) = listener.accept().await else {
                return;
            };
            let mut request = Vec::new();
            let mut buffer = [0u8; 1024];
            let read = |request: &[u8]| {
                head_end(request)
                    .is_some_and(|end| request.len() >= end + content_length(&request[..end]))
            };
            while !read(&request) {
                match socket.read(&mut buffer).await {
                    Ok(0) | Err(_) => break,
                    Ok(n) => request.extend_from_slice(&buffer[..n]),
                }
            }
            recorded.record(&request);
            if let Some((status, body)) = answer {
                let body = body.to_string();
                let response = format!(
                    "HTTP/1.1 {status} X\r\ncontent-type: application/json\r\n\
                     content-length: {}\r\nconnection: close\r\n\r\n{body}",
                    body.len()
                );
                let _ = socket.write_all(response.as_bytes()).await;
            }
        }
    });
    let config = CliConfig {
        api_url: format!("http://{address}"),
        tenant_slug: "acme".into(),
        api_key: Some("key".into()),
        env_name: None,
        keycloak_url: None,
        keycloak_realm: None,
        keycloak_client_id: None,
        tenant_override: None,
    };
    (config, asked)
}