use std::{collections::BTreeMap, time::Duration};
use bytes::Bytes;
use crate::{
common::data::{
self, ActiveRecording, MockDefinition, MockServerHttpResponse, RecordingRuleConfig, RequestRequirements,
},
prelude::HttpMockRequest,
server::{
persistence::{deserialize_mock_defs_from_yaml, serialize_mock_defs_to_yaml},
state::{
Error,
Error::{DataConversionError, ValidationError},
HttpMockStateManager, request_matches,
},
},
};
#[derive(Default)]
pub(super) struct State {
next_recording_id: usize,
recordings: BTreeMap<usize, ActiveRecording>,
}
impl HttpMockStateManager {
pub(crate) fn create_recording(&self, config: RecordingRuleConfig) -> ActiveRecording {
let mut state = self.state.lock().unwrap();
let rec = ActiveRecording {
id: state.recording.next_recording_id,
config,
mocks: Vec::new(),
};
state.recording.recordings.insert(rec.id, rec.clone());
state.recording.next_recording_id += 1;
rec
}
pub(crate) fn delete_recording(&self, id: usize) -> Option<ActiveRecording> {
let mut state = self.state.lock().unwrap();
let result = state.recording.recordings.remove(&id);
if result.is_some() {
tracing::debug!("Deleting recording with id={}", id);
} else {
tracing::warn!(
"Could not delete recording with id={} (no recording with that id found)",
id
);
}
result
}
pub(crate) fn delete_all_recordings(&self) {
let mut state = self.state.lock().unwrap();
state.recording.recordings.clear();
tracing::debug!("Deleted all recordings");
}
pub(crate) fn export_recording(&self, id: usize) -> Result<Option<Bytes>, Error> {
let state = self.state.lock().unwrap();
if let Some(rec) = state.recording.recordings.get(&id) {
return Ok(Some(
serialize_mock_defs_to_yaml(&rec.mocks).map_err(|err| DataConversionError(err.to_string()))?,
));
}
Ok(None)
}
pub(crate) fn load_mocks_from_recording(&self, recording_file_content: &str) -> Result<Vec<usize>, Error> {
let all_static_mock_defs = deserialize_mock_defs_from_yaml(recording_file_content)
.map_err(|err| DataConversionError(err.to_string()))?;
if all_static_mock_defs.is_empty() {
return Err(ValidationError(
"no mock definitions could be found in the provided recording content".to_string(),
));
}
let mut mock_ids = Vec::with_capacity(all_static_mock_defs.len());
for static_mock_def in all_static_mock_defs {
let mock_def: MockDefinition = static_mock_def
.try_into()
.map_err(|err: data::Error| DataConversionError(err.to_string()))?;
let active_mock = self.add_mock(mock_def, false)?;
mock_ids.push(active_mock.id);
}
Ok(mock_ids)
}
pub(crate) fn record<
IntoResponse: TryInto<MockServerHttpResponse, Error = impl std::fmt::Display + std::fmt::Debug + 'static>,
>(
&self,
is_proxied: bool,
time_taken: Duration,
req: HttpMockRequest,
res: IntoResponse,
) -> Result<(), Error> {
let mut state = self.state.lock().unwrap();
let recording_ids: Vec<usize> = state
.recording
.recordings
.values()
.filter(|rec| request_matches(&state.matchers, &req, &rec.config.request_requirements))
.map(|r| r.id)
.collect();
if recording_ids.is_empty() {
return Ok(());
}
let res = res.try_into().map_err(|err| DataConversionError(err.to_string()))?;
for id in recording_ids {
let rec = state.recording.recordings.get_mut(&id).unwrap();
let definition = build_mock_definition(is_proxied, time_taken, &req, &res, &rec.config)?;
rec.mocks.push(definition);
}
Ok(())
}
}
fn build_mock_definition(
is_proxied: bool,
time_taken: Duration,
request: &HttpMockRequest,
response: &MockServerHttpResponse,
config: &RecordingRuleConfig,
) -> Result<MockDefinition, Error> {
let mut headers = Vec::with_capacity(config.record_headers.len());
for header_name in &config.record_headers {
let header_name_lowercase = header_name.to_lowercase();
for (key, value) in request.headers() {
if let Some(key) = key
&& header_name_lowercase == key.to_string().to_lowercase()
{
let value = value.to_str().map_err(|err| DataConversionError(err.to_string()))?;
headers.push((header_name.to_string(), value.to_string()))
}
}
}
let request = RequestRequirements {
host: if is_proxied {
request.uri().host().map(|h| h.to_string())
} else {
None
},
port: if is_proxied {
request.uri().port().map(|h| h.as_u16())
} else {
None
},
scheme: if is_proxied {
request.uri().scheme().map(|h| h.to_string())
} else {
None
},
path: Some(request.uri().path().to_string()),
method: Some(request.method().to_string()),
header: if !headers.is_empty() { Some(headers) } else { None },
body: if request.body().is_empty() {
None
} else {
Some(request.body().clone())
},
query_param: if request.query_param_length() == 0 {
None
} else {
Some(request.query_params())
},
..Default::default()
};
let mut response = response.clone();
if config.record_response_delays {
response.delay = Some(time_taken.as_millis() as u64)
}
Ok(MockDefinition {
request,
response,
up_to_n_times: None,
})
}