Skip to main content

apimock_server/
respond_response.rs

1//! Turn a matched `Respond` declaration into an HTTP response.
2//!
3//! # Why this is a server-side free function and not a `Respond` method
4//!
5//! Pre-5.0, `Respond::response(...)` lived on the type itself. That
6//! method built a `hyper::Response<BoxBody>` and touched the server's
7//! file-response / text-response / status-response helpers. To keep
8//! `apimock-routing` free of hyper-body construction (so a future GUI
9//! can depend on it cheaply), that work moved here.
10
11use apimock_routing::{ParsedRequest, Respond};
12use console::style;
13use std::path::Path;
14
15use crate::{
16    http_util::delay_response,
17    respond_util::full_file_path,
18    response::{
19        error_response::internal_server_error_response,
20        file_response::FileResponse,
21        status_code_response::{status_code_response, status_code_response_with_message},
22        text_response::text_response,
23    },
24    types::BoxBody,
25};
26
27/// Produce the HTTP response for a matched `Respond` declaration.
28///
29/// # Why the branches are ordered file → text → status → error
30///
31/// The fields are mutually specialised:
32/// - `file_path` serves a file (possibly with CSV→JSON conversion).
33/// - `text` + `status` yields a custom-status text response.
34/// - `text` alone yields a plain 200 text response.
35/// - `status` alone yields an empty body with that status.
36///
37/// `Respond::validate` rejects nonsensical combinations at startup, so
38/// hitting the final `Err` branch means something slipped past
39/// validation — a real bug, not user input.
40///
41/// `rule_set_default_delay_ms` is the matched rule set's
42/// `[default].delay_response_milliseconds`, if any (RFC 045 Defect 2).
43/// The per-rule `respond.delay_response_milliseconds` always overrides
44/// it when both are set; the rule-set value only applies when the rule
45/// itself is silent.
46pub async fn respond_response(
47    respond: &Respond,
48    dir_prefix: &str,
49    parsed_request: &ParsedRequest,
50    rule_set_default_delay_ms: Option<u32>,
51    confine_to: Option<&Path>,
52) -> Result<hyper::Response<BoxBody>, hyper::http::Error> {
53    if let Some(delay_ms) = respond
54        .delay_response_milliseconds
55        .or(rule_set_default_delay_ms)
56    {
57        delay_response(delay_ms).await;
58    }
59
60    let request_headers = &parsed_request.component_parts.headers;
61
62    // file_path → file/CSV/JSON response
63    if let Some(file_path) = respond.file_path.as_ref() {
64        let Some(full_file_path) = full_file_path(file_path.as_str(), dir_prefix) else {
65            log::error!(
66                "{}:\n{} (prefix = {})",
67                style("file not found").red(),
68                file_path,
69                dir_prefix,
70            );
71            return internal_server_error_response("failed to get response file", request_headers);
72        };
73
74        // dir_prefix is used only for the file-not-found message above;
75        // the actual read happens against the resolved full_file_path.
76        let _ = Path::new(dir_prefix);
77
78        return FileResponse::new_with_csv_records_jsonpath(
79            full_file_path.as_str(),
80            respond.headers.as_ref(),
81            respond.csv_records_key.clone(),
82            request_headers,
83            confine_to,
84        )
85        .file_content_response()
86        .await;
87    }
88
89    if let Some(text) = respond.text.as_ref() {
90        return match respond.status_code.as_ref() {
91            Some(status_code) => status_code_response_with_message(
92                status_code,
93                text.as_str(),
94                respond.headers.as_ref(),
95                request_headers,
96            ),
97            None => text_response(
98                text.as_str(),
99                None,
100                respond.headers.as_ref(),
101                request_headers,
102            ),
103        };
104    }
105
106    if let Some(status_code) = respond.status_code.as_ref() {
107        return status_code_response(status_code, respond.headers.as_ref(), request_headers);
108    }
109
110    internal_server_error_response("invalid respond def", request_headers)
111}