Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add dynamic response body generation based on request #104

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/api/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use std::cell::Cell;
use std::future::pending;
use std::net::{SocketAddr, ToSocketAddrs};
use std::rc::Rc;
use std::sync::Arc;
use std::sync::{Arc, Mutex};
use std::thread;
use tokio::task::LocalSet;

Expand Down Expand Up @@ -242,12 +242,14 @@ impl MockServer {
let mut req = Rc::new(Cell::new(RequestRequirements::new()));
let mut res = Rc::new(Cell::new(MockServerHttpResponse::new()));

let mut body_generator = Arc::new(Mutex::new(None));
spec_fn(
When {
expectations: req.clone(),
},
Then {
response_template: res.clone(),
body_generator: body_generator.clone(),
},
);

Expand All @@ -258,6 +260,7 @@ impl MockServer {
.create_mock(&MockDefinition {
request: req.take(),
response: res.take(),
body_generator: body_generator.lock().unwrap().take(),
})
.await
.expect("Cannot deserialize mock server response");
Expand Down
17 changes: 14 additions & 3 deletions src/api/spec.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::common::data::{
MockMatcherFunction, MockServerHttpResponse, Pattern, RequestRequirements,
MockMatcherFunction, MockServerHttpResponse, MockValueFunction, Pattern, RequestRequirements,
};
use crate::common::util::{get_test_resource_file_path, read_file, update_cell};
use crate::{Method, Regex};
Expand All @@ -9,6 +9,7 @@ use std::cell::Cell;
use std::path::Path;
use std::rc::Rc;
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use std::time::Duration;

/// A function that encapsulates one or more
Expand Down Expand Up @@ -779,12 +780,12 @@ impl When {
/// m.assert();
/// assert_eq!(response.status(), 200);
/// ```
pub fn matches(mut self, matcher: MockMatcherFunction) -> Self {
pub fn matches(mut self, matcher: impl MockMatcherFunction) -> Self {
update_cell(&self.expectations, |e| {
if e.matchers.is_none() {
e.matchers = Some(Vec::new());
}
e.matchers.as_mut().unwrap().push(matcher);
e.matchers.as_mut().unwrap().push(Arc::new(matcher));
});
self
}
Expand Down Expand Up @@ -832,6 +833,7 @@ impl When {
/// A type that allows the specification of HTTP response values.
pub struct Then {
pub(crate) response_template: Rc<Cell<MockServerHttpResponse>>,
pub(crate) body_generator: Arc<Mutex<Option<Arc<MockValueFunction>>>>,
}

impl Then {
Expand Down Expand Up @@ -988,6 +990,15 @@ impl Then {
self
}

pub fn json_body_given_request(mut self, value_generator: impl MockValueFunction) -> Self {
let body_gen = &self.body_generator;
{
let mut data = body_gen.lock().unwrap();
*data = Some(Arc::new(value_generator));
}
self
}

/// Sets the JSON body that will be returned by the mock server.
/// This method expects a serializable serde object that will be serialized/deserialized
/// to/from a JSON string.
Expand Down
19 changes: 17 additions & 2 deletions src/common/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,11 @@ impl PartialEq for Pattern {

impl Eq for Pattern {}

pub type MockMatcherFunction = fn(&HttpMockRequest) -> bool;
pub trait MockMatcherFunction: Fn(&HttpMockRequest) -> bool + Send + Sync + 'static {}
impl<F> MockMatcherFunction for F where F: Fn(&HttpMockRequest) -> bool + Send + Sync + 'static {}

pub trait MockValueFunction: Fn(&HttpMockRequest) -> Value + Send + Sync + 'static {}
impl<F> MockValueFunction for F where F: Fn(&HttpMockRequest) -> Value + Send + Sync + 'static {}

/// A general abstraction of an HTTP request for all handlers.
#[derive(Serialize, Deserialize, Clone)]
Expand All @@ -192,7 +196,7 @@ pub struct RequestRequirements {
pub x_www_form_urlencoded: Option<Vec<(String, String)>>,

#[serde(skip_serializing, skip_deserializing)]
pub matchers: Option<Vec<MockMatcherFunction>>,
pub matchers: Option<Vec<Arc<dyn MockMatcherFunction>>>,
}

impl Default for RequestRequirements {
Expand Down Expand Up @@ -306,13 +310,16 @@ impl RequestRequirements {
pub struct MockDefinition {
pub request: RequestRequirements,
pub response: MockServerHttpResponse,
#[serde(skip)]
pub body_generator: Option<Arc<dyn MockValueFunction>>,
}

impl MockDefinition {
pub fn new(req: RequestRequirements, mock: MockServerHttpResponse) -> Self {
Self {
request: req,
response: mock,
body_generator: None,
}
}
}
Expand Down Expand Up @@ -345,6 +352,14 @@ impl ActiveMock {
is_static,
}
}

pub fn generate_response(&self, req: Arc<HttpMockRequest>) -> MockServerHttpResponse {
let mut response = self.definition.response.clone();
if let Some(body_generator) = &self.definition.body_generator {
response.body = Some(body_generator(req.as_ref()).to_string().into_bytes());
}
response
}
}

#[derive(Serialize, Deserialize)]
Expand Down
14 changes: 11 additions & 3 deletions src/server/matchers/comparators.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::sync::Arc;

use assert_json_diff::{assert_json_matches_no_panic, CompareMode, Config};
use serde_json::Value;

Expand Down Expand Up @@ -180,8 +182,14 @@ impl FunctionMatchesRequestComparator {
}
}

impl ValueComparator<MockMatcherFunction, HttpMockRequest> for FunctionMatchesRequestComparator {
fn matches(&self, mock_value: &MockMatcherFunction, req_value: &HttpMockRequest) -> bool {
impl ValueComparator<Arc<dyn MockMatcherFunction>, HttpMockRequest>
for FunctionMatchesRequestComparator
{
fn matches(
&self,
mock_value: &Arc<dyn MockMatcherFunction>,
req_value: &HttpMockRequest,
) -> bool {
(*mock_value)(req_value)
}

Expand All @@ -191,7 +199,7 @@ impl ValueComparator<MockMatcherFunction, HttpMockRequest> for FunctionMatchesRe

fn distance(
&self,
mock_value: &Option<&MockMatcherFunction>,
mock_value: &Option<&Arc<dyn MockMatcherFunction>>,
req_value: &Option<&HttpMockRequest>,
) -> usize {
let mock_value = match mock_value {
Expand Down
5 changes: 3 additions & 2 deletions src/server/matchers/sources.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::collections::BTreeMap;
use std::sync::Arc;

use serde_json::Value;

Expand Down Expand Up @@ -366,11 +367,11 @@ impl FunctionSource {
}
}

impl ValueRefSource<MockMatcherFunction> for FunctionSource {
impl ValueRefSource<Arc<dyn MockMatcherFunction>> for FunctionSource {
fn parse_from_mock<'a>(
&self,
mock: &'a RequestRequirements,
) -> Option<Vec<&'a MockMatcherFunction>> {
) -> Option<Vec<&'a Arc<dyn MockMatcherFunction>>> {
mock.matchers
.as_ref()
.map(|b| b.iter().map(|f| f).collect())
Expand Down
2 changes: 1 addition & 1 deletion src/server/web/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ pub(crate) fn find_mock(
let mock = mocks.get_mut(&found_id).unwrap();
mock.call_counter += 1;

return Ok(Some(mock.definition.response.clone()));
return Ok(Some(mock.generate_response(req)));
}

log::debug!(
Expand Down