#![warn(missing_docs)]
mod headers;
pub use headers::parse_guc_headers;
mod json;
pub use headers::ContentRange;
#[allow(deprecated)]
pub use headers::build_response_headers;
pub use json::format_json_response;
use http::{HeaderMap, HeaderValue, StatusCode};
use postrust_core::{ApiRequest, MediaType};
use serde::Serialize;
#[derive(Clone, Debug)]
pub struct Response {
pub status: StatusCode,
pub headers: HeaderMap,
pub body: bytes::Bytes,
}
impl Response {
pub fn new(status: StatusCode, body: impl Into<bytes::Bytes>) -> Self {
Self {
status,
headers: HeaderMap::new(),
body: body.into(),
}
}
pub fn json<T: Serialize>(status: StatusCode, value: &T) -> Result<Self, serde_json::Error> {
let body = serde_json::to_vec(value)?;
let mut response = Self::new(status, body);
response.set_content_type("application/json; charset=utf-8");
Ok(response)
}
pub fn empty(status: StatusCode) -> Self {
Self::new(status, bytes::Bytes::new())
}
pub fn append_header(&mut self, name: &str, value: &str) {
if let (Ok(name), Ok(value)) = (
http::header::HeaderName::from_bytes(name.as_bytes()),
HeaderValue::from_str(value),
) {
self.headers.append(name, value);
}
}
pub fn set_header(&mut self, name: &str, value: &str) {
if let Ok(v) = HeaderValue::from_str(value) {
self.headers.insert(
http::header::HeaderName::from_bytes(name.as_bytes()).unwrap(),
v,
);
}
}
pub fn set_content_type(&mut self, content_type: &str) {
self.set_header("content-type", content_type);
}
pub fn set_content_range(&mut self, range: &ContentRange) {
self.set_header("content-range", &range.to_string());
}
pub fn set_location(&mut self, location: &str) {
self.set_header("location", location);
}
}
pub fn format_response(
request: &ApiRequest,
result: &QueryResult,
) -> Result<Response, FormatError> {
let media_type = request
.accept_media_types
.first()
.cloned()
.unwrap_or(MediaType::ApplicationJson);
if let Some((media_type, body)) = &result.raw_body {
let mut response = Response::new(result.status, body.clone());
let charset = match media_type.as_str() {
"application/octet-stream" => "",
already if already.contains("charset=") => "",
text if text.starts_with("text/") || text.contains("json") => "; charset=utf-8",
_ => "",
};
response.set_content_type(&format!("{}{}", media_type, charset));
add_common_headers(&mut response, request, result);
return Ok(response);
}
if result.omit_body {
if let MediaType::SingularJson { nullable, .. } = &media_type {
match (result.affected, nullable) {
(1, _) | (0, true) => {}
(0, false) => return Err(FormatError::NotFound),
_ => return Err(FormatError::MultipleRows),
}
}
let mut response = Response::new(result.status, bytes::Bytes::new());
add_common_headers(&mut response, request, result);
return Ok(response);
}
let rows = match &media_type {
MediaType::SingularJson {
strip_nulls: true, ..
}
| MediaType::ArrayJson {
strip_nulls: true, ..
} => result.rows.iter().cloned().map(strip_nulls).collect(),
_ => result.rows.clone(),
};
let result = &QueryResult {
rows,
..result.clone()
};
match &media_type {
MediaType::ApplicationJson => {
let body = if result.singular {
format_singular_or_null(&result.rows)?
} else {
format_json_response(&result.rows)?
};
let mut response = Response::new(result.status, body);
response.set_content_type("application/json; charset=utf-8");
add_common_headers(&mut response, request, result);
Ok(response)
}
MediaType::TextCsv => {
let body = format_csv_response(&result.rows)?;
let mut response = Response::new(result.status, body);
response.set_content_type("text/csv; charset=utf-8");
add_common_headers(&mut response, request, result);
Ok(response)
}
MediaType::SingularJson { nullable, .. } => {
let body = format_singular_json(&result.rows, *nullable)?;
let mut response = Response::new(result.status, body);
response.set_content_type("application/vnd.pgrst.object+json; charset=utf-8");
add_common_headers(&mut response, request, result);
Ok(response)
}
other => {
let body = if result.singular {
format_singular_or_null(&result.rows)?
} else {
format_json_response(&result.rows)?
};
let mut response = Response::new(result.status, body);
let content_type = match other {
MediaType::ArrayJson { .. } => "application/vnd.pgrst.array+json; charset=utf-8",
MediaType::OpenApi => "application/openapi+json; charset=utf-8",
_ => "application/json; charset=utf-8",
};
response.set_content_type(content_type);
add_common_headers(&mut response, request, result);
Ok(response)
}
}
}
pub(crate) fn preference_scope(
request: &ApiRequest,
) -> postrust_core::api_request::preferences::PreferenceScope {
use postrust_core::api_request::preferences::PreferenceScope;
use postrust_core::api_request::{Action, DbAction, Mutation};
let Action::Db(action) = &request.action else {
return PreferenceScope::read();
};
match action {
DbAction::RelationMut { mutation, .. } => PreferenceScope {
resolution: matches!(mutation, Mutation::Create),
representation: true,
missing: matches!(mutation, Mutation::Create | Mutation::Update),
max_affected: matches!(mutation, Mutation::Update | Mutation::Delete),
},
DbAction::Routine { .. } => PreferenceScope {
max_affected: true,
..PreferenceScope::read()
},
_ => PreferenceScope::read(),
}
}
fn strip_nulls(value: serde_json::Value) -> serde_json::Value {
match value {
serde_json::Value::Object(fields) => serde_json::Value::Object(
fields
.into_iter()
.filter(|(_, value)| !value.is_null())
.map(|(key, value)| (key, strip_nulls(value)))
.collect(),
),
serde_json::Value::Array(items) => {
serde_json::Value::Array(items.into_iter().map(strip_nulls).collect())
}
other => other,
}
}
fn add_common_headers(response: &mut Response, request: &ApiRequest, result: &QueryResult) {
if let Some(guc) = &result.guc_headers {
if let Some(headers) = crate::headers::parse_guc_headers(guc) {
for (name, value) in headers {
response.append_header(&name, &value);
}
}
}
if let Some(range) = &result.content_range {
response.set_content_range(range);
}
if let Some(location) = &result.location {
response.set_location(location);
}
if let Some(allow) = &result.allow {
response.set_header("allow", allow);
}
if let Some(applied) = postrust_core::api_request::preferences::preference_applied(
&request.preferences,
preference_scope(request),
) {
response.set_header("preference-applied", &applied);
}
if request.negotiated_by_profile {
response.set_header("content-profile", &request.schema);
}
}
fn format_singular_or_null(rows: &[serde_json::Value]) -> Result<bytes::Bytes, FormatError> {
match rows.len() {
0 => Ok(bytes::Bytes::from_static(b"null")),
1 => Ok(bytes::Bytes::from(serde_json::to_vec(&rows[0])?)),
_ => format_json_response(rows),
}
}
fn format_singular_json(
rows: &[serde_json::Value],
nullable: bool,
) -> Result<bytes::Bytes, FormatError> {
match rows.len() {
0 if nullable => Ok(bytes::Bytes::from_static(b"null")),
0 => Err(FormatError::NotFound),
1 => Ok(bytes::Bytes::from(serde_json::to_vec(&rows[0])?)),
_ => Err(FormatError::MultipleRows),
}
}
fn format_csv_response(rows: &[serde_json::Value]) -> Result<bytes::Bytes, FormatError> {
if rows.is_empty() {
return Ok(bytes::Bytes::new());
}
let mut output = Vec::new();
if let Some(serde_json::Value::Object(map)) = rows.first() {
let headers: Vec<&str> = map.keys().map(|s| s.as_str()).collect();
output.extend_from_slice(headers.join(",").as_bytes());
output.push(b'\n');
for row in rows {
if let serde_json::Value::Object(row_map) = row {
let values: Vec<String> = headers
.iter()
.map(|h| row_map.get(*h).map(csv_escape).unwrap_or_default())
.collect();
output.extend_from_slice(values.join(",").as_bytes());
output.push(b'\n');
}
}
}
Ok(bytes::Bytes::from(output))
}
fn csv_escape(value: &serde_json::Value) -> String {
match value {
serde_json::Value::String(s) => {
if s.contains(',') || s.contains('"') || s.contains('\n') {
format!("\"{}\"", s.replace('"', "\"\""))
} else {
s.clone()
}
}
serde_json::Value::Null => String::new(),
other => other.to_string(),
}
}
#[derive(Clone, Debug, Default)]
pub struct QueryResult {
pub status: StatusCode,
pub rows: Vec<serde_json::Value>,
pub total_count: Option<i64>,
pub content_range: Option<ContentRange>,
pub location: Option<String>,
pub allow: Option<String>,
pub guc_headers: Option<String>,
pub guc_status: Option<String>,
pub omit_body: bool,
pub affected: usize,
pub singular: bool,
pub raw_body: Option<(String, Vec<u8>)>,
}
#[derive(Debug, thiserror::Error)]
pub enum FormatError {
#[error("JSON serialization error: {0}")]
Json(#[from] serde_json::Error),
#[error("Resource not found")]
NotFound,
#[error("Multiple rows returned for singular response")]
MultipleRows,
}
impl FormatError {
pub fn status_code(&self) -> StatusCode {
match self {
Self::Json(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::NotFound => StatusCode::NOT_FOUND,
Self::MultipleRows => StatusCode::NOT_ACCEPTABLE,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn result(rows: Vec<serde_json::Value>, singular: bool) -> QueryResult {
QueryResult {
status: StatusCode::OK,
rows,
singular,
..Default::default()
}
}
#[test]
fn singular_result_renders_bare_object() {
let req = ApiRequest::default();
let resp = format_response(&req, &result(vec![json!({"ok": true})], true)).unwrap();
assert_eq!(&resp.body[..], br#"{"ok":true}"#);
}
#[test]
fn singular_empty_result_renders_null() {
let req = ApiRequest::default();
let resp = format_response(&req, &result(vec![], true)).unwrap();
assert_eq!(&resp.body[..], b"null");
}
#[test]
fn non_singular_result_renders_array() {
let req = ApiRequest::default();
let resp = format_response(&req, &result(vec![json!(1), json!(2)], false)).unwrap();
assert_eq!(&resp.body[..], b"[1,2]");
}
}