pub mod helpers;
#[cfg(test)]
mod tests;
use axum::http::{HeaderMap, HeaderValue, StatusCode};
use fraiseql_core::schema::{DeleteResponse, RestConfig};
use helpers::{
build_cursor_links, build_offset_links, check_if_none_match, compute_etag,
extract_collection_data, extract_delete_entity, extract_id_from_data, extract_mutation_data,
extract_relay_page_info, extract_single_data, format_id_for_url, header_value,
};
use serde_json::json;
use super::{
handler::{PreferHeader, RestError, RestResponse, set_request_id},
params::PaginationParams,
resource::HttpMethod,
};
pub struct RestResponseFormatter<'a> {
config: &'a RestConfig,
base_path: &'a str,
}
impl<'a> RestResponseFormatter<'a> {
#[must_use]
pub const fn new(config: &'a RestConfig, base_path: &'a str) -> Self {
Self { config, base_path }
}
pub fn format_single(
&self,
result: &serde_json::Value,
request_headers: &HeaderMap,
) -> Result<RestResponse, RestError> {
let data = extract_single_data(result)?;
let serialized = serde_json::to_vec(&data)
.map_err(|e| RestError::internal(format!("Failed to serialize response: {e}")))?;
let etag = if self.config.etag {
Some(compute_etag(&serialized))
} else {
None
};
if let Some(ref etag_val) = etag {
if check_if_none_match(request_headers, etag_val).unwrap_or(false) {
let mut headers = HeaderMap::new();
headers.insert("etag", header_value(etag_val));
set_request_id(request_headers, &mut headers);
return Ok(RestResponse {
status: StatusCode::NOT_MODIFIED,
headers,
body: None,
});
}
}
let mut headers = HeaderMap::new();
if let Some(etag_val) = etag {
headers.insert("etag", header_value(&etag_val));
}
set_request_id(request_headers, &mut headers);
let body = json!({
"data": data,
});
Ok(RestResponse {
status: StatusCode::OK,
headers,
body: Some(body),
})
}
#[doc(hidden)] pub fn format_collection(
&self,
result: &serde_json::Value,
pagination: &PaginationParams,
request_headers: &HeaderMap,
) -> Result<RestResponse, RestError> {
let data = extract_collection_data(result)?;
let serialized = serde_json::to_vec(&data)
.map_err(|e| RestError::internal(format!("Failed to serialize response: {e}")))?;
let etag = if self.config.etag {
Some(compute_etag(&serialized))
} else {
None
};
if let Some(ref etag_val) = etag {
if check_if_none_match(request_headers, etag_val).unwrap_or(false) {
let mut headers = HeaderMap::new();
headers.insert("etag", header_value(etag_val));
set_request_id(request_headers, &mut headers);
return Ok(RestResponse {
status: StatusCode::NOT_MODIFIED,
headers,
body: None,
});
}
}
let mut headers = HeaderMap::new();
if let Some(etag_val) = etag {
headers.insert("etag", header_value(&etag_val));
}
set_request_id(request_headers, &mut headers);
let mut body = json!({
"data": data,
});
match pagination {
PaginationParams::Offset { limit, offset } => {
body["meta"] = json!({
"limit": limit,
"offset": offset,
});
let base = self.base_path;
body["links"] = build_offset_links(base, *limit, *offset, None);
},
PaginationParams::Cursor {
first,
after,
last: _,
before: _,
} => {
let mut meta = serde_json::Map::new();
if let Some(page_info) = extract_relay_page_info(&data) {
if let Some(has_next) = page_info.get("hasNextPage") {
meta.insert("hasNextPage".to_string(), has_next.clone());
}
if let Some(has_prev) = page_info.get("hasPreviousPage") {
meta.insert("hasPreviousPage".to_string(), has_prev.clone());
}
}
body["meta"] = serde_json::Value::Object(meta);
let base = self.base_path;
body["links"] = build_cursor_links(base, *first, after.as_deref(), &data);
},
PaginationParams::None => {
},
}
Ok(RestResponse {
status: StatusCode::OK,
headers,
body: Some(body),
})
}
pub fn format_mutation_post(
&self,
result: &serde_json::Value,
resource_path: &str,
request_headers: &HeaderMap,
) -> Result<RestResponse, RestError> {
let data = extract_mutation_data(result)?;
let mut headers = HeaderMap::new();
set_request_id(request_headers, &mut headers);
if let Some(id_val) = extract_id_from_data(&data) {
let id_str = format_id_for_url(id_val);
let location = format!("{resource_path}/{id_str}");
if let Ok(loc_val) = HeaderValue::from_str(&location) {
headers.insert("location", loc_val);
}
}
let body = json!({
"data": data,
});
Ok(RestResponse {
status: StatusCode::CREATED,
headers,
body: Some(body),
})
}
pub fn format_mutation_update(
&self,
result: &serde_json::Value,
request_headers: &HeaderMap,
) -> Result<RestResponse, RestError> {
let data = extract_mutation_data(result)?;
let mut headers = HeaderMap::new();
set_request_id(request_headers, &mut headers);
let body = json!({
"data": data,
});
Ok(RestResponse {
status: StatusCode::OK,
headers,
body: Some(body),
})
}
pub fn format_delete(
&self,
result: &serde_json::Value,
prefer: &PreferHeader,
mutation_name: &str,
request_headers: &HeaderMap,
) -> Result<RestResponse, RestError> {
let mut headers = HeaderMap::new();
set_request_id(request_headers, &mut headers);
let want_entity = if prefer.return_representation {
true
} else if prefer.return_minimal {
false
} else {
matches!(self.config.delete_response, DeleteResponse::Entity)
};
if want_entity {
if let Some(entity) = extract_delete_entity(result, mutation_name) {
if prefer.return_representation {
headers.insert(
"preference-applied",
HeaderValue::from_static("return=representation"),
);
}
let body = json!({
"data": entity,
});
return Ok(RestResponse {
status: StatusCode::OK,
headers,
body: Some(body),
});
}
}
Ok(RestResponse {
status: StatusCode::NO_CONTENT,
headers,
body: None,
})
}
#[must_use]
pub fn format_method_not_allowed(
&self,
allowed_methods: &[HttpMethod],
request_headers: &HeaderMap,
) -> RestResponse {
let mut headers = HeaderMap::new();
set_request_id(request_headers, &mut headers);
let method_strs: Vec<&str> = allowed_methods
.iter()
.map(|m| match m {
HttpMethod::Get => "GET",
HttpMethod::Post => "POST",
HttpMethod::Put => "PUT",
HttpMethod::Patch => "PATCH",
HttpMethod::Delete => "DELETE",
})
.collect();
if let Ok(allow_header) = HeaderValue::from_str(&method_strs.join(", ")) {
headers.insert("allow", allow_header);
}
RestResponse {
status: StatusCode::METHOD_NOT_ALLOWED,
headers,
body: Some(RestError::method_not_allowed().to_json()),
}
}
}
impl RestError {
#[must_use]
pub fn method_not_allowed() -> Self {
Self {
status: StatusCode::METHOD_NOT_ALLOWED,
code: "METHOD_NOT_ALLOWED",
message: "Method not allowed".to_string(),
details: None,
}
}
}