use std::fmt::Write as FmtWrite;
use crate::e2e::config::E2eConfig;
use crate::e2e::escape::rust_raw_string;
use crate::e2e::fixture::Fixture;
pub fn render_mock_server_setup(out: &mut String, fixture: &Fixture, e2e_config: &E2eConfig, var_name: &str) {
let mut routes = Vec::new();
if let Some(mock_responses) = fixture.input.get("mock_responses").and_then(|v| v.as_array()) {
let call_config = e2e_config.resolve_call(fixture.call.as_deref());
let default_path = call_config.path.as_deref().unwrap_or("/");
let default_method = call_config.method.as_deref().unwrap_or("POST");
for response in mock_responses {
if let Ok(obj) = serde_json::from_value::<serde_json::Map<String, serde_json::Value>>(response.clone()) {
let path = obj
.get("path")
.and_then(|v| v.as_str())
.unwrap_or(default_path)
.to_string();
let method = obj
.get("method")
.and_then(|v| v.as_str())
.unwrap_or(default_method)
.to_string();
let status: u16 = obj.get("status_code").and_then(|v| v.as_u64()).unwrap_or(200) as u16;
let headers: Vec<(String, String)> = obj
.get("headers")
.and_then(|v| v.as_object())
.map(|h| {
let mut entries: Vec<_> = h
.iter()
.map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string()))
.collect();
entries.sort_by(|a, b| a.0.cmp(&b.0));
entries
})
.unwrap_or_default();
let body_str = if let Some(body_inline) = obj.get("body_inline").and_then(|v| v.as_str()) {
rust_raw_string(body_inline)
} else if let Some(body_file) = obj.get("body_file").and_then(|v| v.as_str()) {
let content = read_mock_body_file(&e2e_config.fixtures, body_file, &fixture.id);
rust_raw_string(&content)
} else {
rust_raw_string("{}")
};
let delay_ms = obj.get("delay_ms").and_then(|v| v.as_u64());
routes.push((path, method, status, body_str, headers, delay_ms));
}
}
} else if let Some(mock) = fixture.mock_response.as_ref() {
let call_config = e2e_config.resolve_call(fixture.call.as_deref());
let path = call_config.path.as_deref().unwrap_or("/");
let method = call_config.method.as_deref().unwrap_or("POST");
let status = mock.status;
let mut header_entries: Vec<(&String, &String)> = mock.headers.iter().collect();
header_entries.sort_by(|a, b| a.0.cmp(b.0));
let header_tuples: Vec<(String, String)> = header_entries
.into_iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
let body_str = match &mock.body {
Some(b) => {
let s = serde_json::to_string(b).unwrap_or_default();
rust_raw_string(&s)
}
None => rust_raw_string("{}"),
};
if let Some(chunks) = &mock.stream_chunks {
let _ = writeln!(out, " let mock_route = MockRoute {{");
let _ = writeln!(out, " path: \"{path}\",");
let _ = writeln!(out, " method: \"{method}\",");
let _ = writeln!(out, " status: {status},");
let _ = writeln!(out, " body: String::new(),");
let _ = writeln!(out, " is_streaming: true,");
let _ = writeln!(out, " stream_chunks: vec![");
for chunk in chunks {
let chunk_str = match chunk {
serde_json::Value::String(s) => rust_raw_string(s),
other => {
let s = serde_json::to_string(other).unwrap_or_default();
rust_raw_string(&s)
}
};
let _ = writeln!(out, " {chunk_str}.to_string(),");
}
let _ = writeln!(out, " ],");
let _ = writeln!(out, " headers: vec![");
for (name, value) in &header_tuples {
let n = rust_raw_string(name);
let v = rust_raw_string(value);
let _ = writeln!(out, " ({n}.to_string(), {v}.to_string()),");
}
let _ = writeln!(out, " ],");
let _ = writeln!(out, " delay_ms: None,");
let _ = writeln!(out, " }};");
let _ = writeln!(out, " let {var_name} = MockServer::start(vec![mock_route]).await;");
return;
}
routes.push((
path.to_string(),
method.to_string(),
status,
body_str,
header_tuples,
None,
));
} else {
return;
}
if routes.len() == 1 {
let (path, method, status, body_str, header_entries, delay_ms) = routes.pop().unwrap();
let delay_literal = match delay_ms {
Some(ms) => format!("Some({ms})"),
None => "None".to_string(),
};
let _ = writeln!(out, " let mock_route = MockRoute {{");
let _ = writeln!(out, " path: \"{path}\",");
let _ = writeln!(out, " method: \"{method}\",");
let _ = writeln!(out, " status: {status},");
let _ = writeln!(out, " body: {body_str}.to_string(),");
let _ = writeln!(out, " is_streaming: false,");
let _ = writeln!(out, " stream_chunks: vec![],");
let _ = writeln!(out, " headers: vec![");
for (name, value) in &header_entries {
let n = rust_raw_string(name);
let v = rust_raw_string(value);
let _ = writeln!(out, " ({n}.to_string(), {v}.to_string()),");
}
let _ = writeln!(out, " ],");
let _ = writeln!(out, " delay_ms: {delay_literal},");
let _ = writeln!(out, " }};");
let _ = writeln!(out, " let {var_name} = MockServer::start(vec![mock_route]).await;");
} else {
let _ = writeln!(out, " let mut mock_routes = vec![];");
for (path, method, status, body_str, header_entries, delay_ms) in routes {
let delay_literal = match delay_ms {
Some(ms) => format!("Some({ms})"),
None => "None".to_string(),
};
let _ = writeln!(out, " mock_routes.push(MockRoute {{");
let _ = writeln!(out, " path: \"{path}\",");
let _ = writeln!(out, " method: \"{method}\",");
let _ = writeln!(out, " status: {status},");
let _ = writeln!(out, " body: {body_str}.to_string(),");
let _ = writeln!(out, " is_streaming: false,");
let _ = writeln!(out, " stream_chunks: vec![],");
let _ = writeln!(out, " headers: vec![");
for (name, value) in &header_entries {
let n = rust_raw_string(name);
let v = rust_raw_string(value);
let _ = writeln!(out, " ({n}.to_string(), {v}.to_string()),");
}
let _ = writeln!(out, " ],");
let _ = writeln!(out, " delay_ms: {delay_literal},");
let _ = writeln!(out, " }});");
}
let _ = writeln!(out, " let {var_name} = MockServer::start(mock_routes).await;");
}
}
fn read_mock_body_file(fixtures_dir: &str, body_file: &str, fixture_id: &str) -> String {
let base = std::path::Path::new(fixtures_dir);
let candidates = [base.join("responses").join(body_file), base.join(body_file)];
for candidate in &candidates {
if let Ok(bytes) = std::fs::read(candidate) {
return String::from_utf8(bytes).unwrap_or_else(|_| {
panic!(
"Rust e2e generator: fixture `{fixture_id}` declares body_file `{body_file}` \
({}), but its content is not valid UTF-8 — the in-process mock server can \
only serve it embedded as a Rust string literal. Use body_inline for binary \
responses in this fixture, or exclude `rust` from its languages.",
candidate.display()
)
});
}
}
panic!(
"Rust e2e generator: fixture `{fixture_id}` declares body_file `{body_file}`, but it \
could not be read (tried `{}` and `{}`). The in-process mock server would otherwise \
silently serve `{{}}` instead of the declared body.",
candidates[0].display(),
candidates[1].display()
);
}
#[cfg(test)]
mod body_file_tests {
use super::render_mock_server_setup;
use crate::e2e::config::E2eConfig;
use crate::e2e::fixture::Fixture;
fn fixture_with_body_file(id: &str, path: &str, body_file: &str) -> Fixture {
let input = serde_json::json!({
"mock_responses": [
{ "path": path, "method": "GET", "status_code": 200, "body_file": body_file }
]
});
Fixture {
id: id.to_string(),
input,
..Fixture::default()
}
}
#[test]
fn body_file_content_is_read_and_embedded() {
let dir = tempfile::tempdir().expect("temp fixtures dir");
std::fs::write(dir.path().join("payload.json"), r#"{"greeting":"hello"}"#).expect("write body file");
let fixture = fixture_with_body_file("serves_payload", "/fixtures/serves_payload", "payload.json");
let e2e_config = E2eConfig {
fixtures: dir.path().to_string_lossy().to_string(),
..E2eConfig::default()
};
let mut out = String::new();
render_mock_server_setup(&mut out, &fixture, &e2e_config, "mock_server");
assert!(
out.contains(r#"{"greeting":"hello"}"#),
"expected the real body_file content embedded, got:\n{out}"
);
assert!(
!out.contains("body: r#\"{}\"#"),
"must not fall back to the `{{}}` placeholder, got:\n{out}"
);
}
#[test]
fn body_file_prefers_the_responses_subdirectory() {
let dir = tempfile::tempdir().expect("temp fixtures dir");
std::fs::create_dir(dir.path().join("responses")).expect("mkdir responses");
std::fs::write(dir.path().join("responses").join("payload.json"), "from-responses-dir")
.expect("write nested body file");
std::fs::write(dir.path().join("payload.json"), "from-fixtures-root").expect("write root body file");
let fixture = fixture_with_body_file("prefers_nested", "/fixtures/prefers_nested", "payload.json");
let e2e_config = E2eConfig {
fixtures: dir.path().to_string_lossy().to_string(),
..E2eConfig::default()
};
let mut out = String::new();
render_mock_server_setup(&mut out, &fixture, &e2e_config, "mock_server");
assert!(out.contains("from-responses-dir"), "got:\n{out}");
assert!(!out.contains("from-fixtures-root"), "got:\n{out}");
}
#[test]
fn body_inline_is_unaffected() {
let input = serde_json::json!({
"mock_responses": [
{ "path": "/fixtures/inline_smoke", "method": "GET", "status_code": 200, "body_inline": "hi" }
]
});
let fixture = Fixture {
id: "inline_smoke".to_string(),
input,
..Fixture::default()
};
let e2e_config = E2eConfig::default();
let mut out = String::new();
render_mock_server_setup(&mut out, &fixture, &e2e_config, "mock_server");
assert!(out.contains(r##"body: r#"hi"#.to_string(),"##), "got:\n{out}");
}
#[test]
#[should_panic(expected = "fixture `missing_body_file`")]
fn unreadable_body_file_fails_loudly_instead_of_falling_back() {
let dir = tempfile::tempdir().expect("temp fixtures dir");
let fixture = fixture_with_body_file("missing_body_file", "/fixtures/missing_body_file", "nope.json");
let e2e_config = E2eConfig {
fixtures: dir.path().to_string_lossy().to_string(),
..E2eConfig::default()
};
let mut out = String::new();
render_mock_server_setup(&mut out, &fixture, &e2e_config, "mock_server");
}
}