cinema 0.1.0

HTTP record-replay proxy for Rust tests
Documentation
use crate::prelude::*;
use http_body_util::Full;
use hyper::Response;
use hyper::body::Bytes;
use std::path::PathBuf;
use std::pin::Pin;
use tokio::fs;
use tokio::fs::create_dir_all;

pub struct RecordReplayFileStore {
    path: PathBuf,
    records: RecordReplayRecords,
}

impl RecordReplayFileStore {
    pub async fn from<Str: Into<String>>(path: Str) -> Result<Self, CinemaError> {
        let path = PathBuf::from(path.into());

        let records = match fs::read_to_string(&path).await {
            Ok(content) => {
                // [TODO] Handle errors
                toml::from_str(&content).unwrap()
            }

            Err(_) => Default::default(),
        };

        Ok(RecordReplayFileStore { path, records })
    }
}

impl RecordReplayStore for RecordReplayFileStore {
    fn handle(
        &mut self,
        payload: ProxyHandlerPayload<'_>,
    ) -> Result<Option<Response<Full<Bytes>>>, CinemaError> {
        // [TODO] Handle error

        let record = self.records.requests.iter().find(|req| {
            req.method == payload.request.method
                && req.uri == payload.request.uri
                && req.body == payload.request.body
        });

        match record {
            Some(record) => {
                // [TODO] There can be multiple responses for the same request
                let response = &record.response;

                let mut builder = Response::builder().status(response.status);
                if let Some(headers) = &response.headers {
                    for (name, value) in headers.iter() {
                        builder = builder.header(name, value);
                    }
                }

                let body = Bytes::from(response.body.clone());
                let response = builder
                    .body(Full::new(body))
                    .map_err(|err| CinemaError::RestoreResponseBody(err))?;

                return Ok(Some(response));
            }

            None => Ok(None),
        }
    }

    fn commit<'a>(&'a self) -> Pin<Box<dyn Future<Output = Result<(), CinemaError>> + Send + 'a>> {
        Box::pin(async {
            if let Some(parent) = self.path.parent() {
                if !parent.exists() {
                    // [TODO] Handle errors
                    create_dir_all(parent).await.unwrap();
                }
            }

            let _ = fs::write(
                &self.path,
                toml::to_string(&self.records).map_err(|err| CinemaError::Serialize(err, ""))?,
            )
            .await;

            Ok(())
        })
    }

    fn record(&mut self, payload: RecordReplayRecord) -> Result<(), CinemaError> {
        self.records.requests.push(payload);
        Ok(())
    }
}