use lino_objects_codec::LinoValue;
use crate::codec::{decode_from, encode_for};
use crate::etag::{compute_etag, evaluate_preconditions};
use crate::headers::{Headers, append_list_value};
use crate::media_type::{
LINO_CONTENT_TYPE, is_decodable_media_type, negotiate_media_type, parse_content_type,
problem_media_type, supported_media_types, with_charset,
};
use crate::problem::LinoHttpError;
use crate::response::Body;
use crate::value::{array, string};
pub const DEFAULT_MAX_BODY_BYTES: usize = 1024 * 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResponseParts {
pub status: u16,
pub headers: Headers,
pub body: String,
}
impl ResponseParts {
pub fn new(status: u16, headers: Headers, body: impl Into<String>) -> Self {
Self {
status,
headers,
body: body.into(),
}
}
}
pub fn append_vary(headers: &mut Headers, field_name: &str) {
append_list_value(headers, "Vary", field_name);
}
pub fn negotiate_request(
headers: &Headers,
supported: Option<&[String]>,
) -> Result<String, LinoHttpError> {
let candidates = supported
.map(<[String]>::to_vec)
.unwrap_or_else(supported_media_types);
let accept = headers.get("accept");
negotiate_media_type(accept, &candidates).ok_or_else(|| {
LinoHttpError::new(
406,
Some(&format!(
"No acceptable representation for Accept: {}",
accept.unwrap_or("")
)),
)
.with_extension("supported", array(candidates.iter().cloned().map(string)))
})
}
#[derive(Debug, Clone, PartialEq)]
pub struct DecodedBody {
pub value: Option<LinoValue>,
pub media_type: Option<String>,
}
pub fn decode_request_body(
raw: &[u8],
content_type: Option<&str>,
max_bytes: usize,
) -> Result<DecodedBody, LinoHttpError> {
if raw.len() > max_bytes {
return Err(LinoHttpError::new(
413,
Some(&format!("Request body exceeds {max_bytes} bytes")),
)
.with_header("Connection", "close"));
}
let media_type = parse_content_type(content_type);
if media_type.is_empty() {
return Ok(DecodedBody {
value: None,
media_type: None,
});
}
if !is_decodable_media_type(&media_type) {
return Err(LinoHttpError::new(
415,
Some(&format!("Unsupported request media type: {media_type}")),
)
.with_extension(
"supported",
array(supported_media_types().into_iter().map(string)),
));
}
let text = String::from_utf8_lossy(raw);
if text.trim().is_empty() {
return Ok(DecodedBody {
value: None,
media_type: Some(media_type),
});
}
match decode_from(&text, &media_type) {
Ok(value) => Ok(DecodedBody {
value: Some(value),
media_type: Some(media_type),
}),
Err(error) => Err(LinoHttpError::new(
400,
Some(&format!("Malformed {media_type} request body: {error}")),
)),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EtagPolicy {
pub etag: bool,
pub preconditions: Option<bool>,
pub require_precondition: bool,
}
impl Default for EtagPolicy {
fn default() -> Self {
Self {
etag: true,
preconditions: None,
require_precondition: false,
}
}
}
pub fn build_response(
body: &Body,
status: u16,
media_type: &str,
headers: &Headers,
request_headers: &Headers,
method: &str,
policy: EtagPolicy,
) -> Result<ResponseParts, LinoHttpError> {
let mut response_headers = headers.clone();
append_vary(&mut response_headers, "Accept");
if status == 204 || matches!(body, Body::Empty) {
let status = if status == 200 { 204 } else { status };
return Ok(ResponseParts::new(status, response_headers, ""));
}
let encoded = match body {
Body::Raw(text) => text.clone(),
Body::Value(value) => encode_for(value, media_type)
.map_err(|error| LinoHttpError::new(500, Some(&error.to_string())))?,
Body::Empty => unreachable!("handled above"),
};
if policy.etag {
let tag = compute_etag(&encoded);
response_headers.insert("ETag", tag.clone());
let safe = method == "GET" || method == "HEAD";
let evaluate = policy.preconditions.unwrap_or(safe);
if evaluate
&& evaluate_preconditions(request_headers, method, &tag, policy.require_precondition)?
.not_modified
{
return Ok(ResponseParts::new(304, response_headers, ""));
}
}
response_headers.insert("Content-Type", with_charset(media_type));
Ok(ResponseParts::new(status, response_headers, encoded))
}
pub fn build_problem_response(
error: &LinoHttpError,
media_type: &str,
instance: Option<&str>,
) -> ResponseParts {
let problem = error.to_problem(instance);
let mut headers = Headers::new();
for (name, value) in &error.headers {
headers.insert(name.clone(), value.clone());
}
append_vary(&mut headers, "Accept");
headers.insert("Content-Type", with_charset(problem_media_type(media_type)));
let body = encode_for(&problem, media_type)
.or_else(|_| encode_for(&problem, LINO_CONTENT_TYPE))
.unwrap_or_else(|_| String::new());
ResponseParts::new(error.status, headers, body)
}