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        json_response::json_response,
22        status_code_response::{status_code_response, status_code_response_with_message},
23        text_response::text_response,
24    },
25    types::BoxBody,
26};
27
28/// Produce the HTTP response for a matched `Respond` declaration.
29///
30/// # Why the branches are ordered file → text → json → status → error
31///
32/// The fields are mutually specialised (RFC 065: `file_path`, `text`
33/// and `json` are the three body sources, `Respond::validate` rejects
34/// more than one being set):
35/// - `file_path` serves a file (possibly with CSV→JSON conversion).
36/// - `text` + `status` yields a custom-status text response.
37/// - `text` alone yields a plain 200 text response.
38/// - `json` (+ an optional `status`) yields an `application/json`
39///   response, parsed with the same JSON5 parser `Respond::validate`
40///   already checked it against at load time.
41/// - `status` alone yields an empty body with that status.
42///
43/// `Respond::validate` rejects nonsensical combinations at startup, so
44/// hitting the final `Err` branch means something slipped past
45/// validation — a real bug, not user input.
46///
47/// `rule_set_default_delay_ms` is the matched rule set's
48/// `[default].delay_response_milliseconds`, if any (RFC 045 Defect 2).
49/// The per-rule `respond.delay_response_milliseconds` always overrides
50/// it when both are set; the rule-set value only applies when the rule
51/// itself is silent.
52pub async fn respond_response(
53    respond: &Respond,
54    dir_prefix: &str,
55    parsed_request: &ParsedRequest,
56    rule_set_default_delay_ms: Option<u32>,
57    confine_to: Option<&Path>,
58    cors_allow_credentials_origins: &[String],
59) -> Result<hyper::Response<BoxBody>, hyper::http::Error> {
60    if let Some(delay_ms) = respond
61        .delay_response_milliseconds
62        .or(rule_set_default_delay_ms)
63    {
64        delay_response(delay_ms).await;
65    }
66
67    let request_headers = &parsed_request.component_parts.headers;
68
69    // file_path → file/CSV/JSON response
70    if let Some(file_path) = respond.file_path.as_ref() {
71        let Some(full_file_path) = full_file_path(file_path.as_str(), dir_prefix) else {
72            log::error!(
73                "{}:\n{} (prefix = {})",
74                style("file not found").red(),
75                file_path,
76                dir_prefix,
77            );
78            return internal_server_error_response(
79                "failed to get response file",
80                request_headers,
81                cors_allow_credentials_origins,
82            );
83        };
84
85        // dir_prefix is used only for the file-not-found message above;
86        // the actual read happens against the resolved full_file_path.
87        let _ = Path::new(dir_prefix);
88
89        return FileResponse::new_with_csv_records_jsonpath(
90            full_file_path.as_str(),
91            respond.headers.as_ref(),
92            respond.csv_records_key.clone(),
93            request_headers,
94            confine_to,
95            cors_allow_credentials_origins,
96        )
97        .file_content_response()
98        .await;
99    }
100
101    if let Some(text) = respond.text.as_ref() {
102        return match respond.status_code.as_ref() {
103            Some(status_code) => status_code_response_with_message(
104                status_code,
105                text.as_str(),
106                respond.headers.as_ref(),
107                request_headers,
108                cors_allow_credentials_origins,
109            ),
110            None => text_response(
111                text.as_str(),
112                None,
113                respond.headers.as_ref(),
114                request_headers,
115                cors_allow_credentials_origins,
116            ),
117        };
118    }
119
120    if let Some(json_str) = respond.json.as_ref() {
121        return json_response(
122            json_str.as_str(),
123            respond.status_code.as_ref(),
124            respond.headers.as_ref(),
125            request_headers,
126            None,
127            cors_allow_credentials_origins,
128        );
129    }
130
131    if let Some(status_code) = respond.status_code.as_ref() {
132        return status_code_response(
133            status_code,
134            respond.headers.as_ref(),
135            request_headers,
136            cors_allow_credentials_origins,
137        );
138    }
139
140    internal_server_error_response(
141        "invalid respond def",
142        request_headers,
143        cors_allow_credentials_origins,
144    )
145}