1use lino_objects_codec::LinoValue;
9
10use crate::codec::{decode_from, encode_for};
11use crate::etag::{compute_etag, evaluate_preconditions};
12use crate::headers::{Headers, append_list_value};
13use crate::media_type::{
14 LINO_CONTENT_TYPE, is_decodable_media_type, negotiate_media_type, parse_content_type,
15 problem_media_type, supported_media_types, with_charset,
16};
17use crate::problem::LinoHttpError;
18use crate::response::Body;
19use crate::value::{array, string};
20
21pub const DEFAULT_MAX_BODY_BYTES: usize = 1024 * 1024;
23
24#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct ResponseParts {
27 pub status: u16,
29 pub headers: Headers,
31 pub body: String,
33}
34
35impl ResponseParts {
36 pub fn new(status: u16, headers: Headers, body: impl Into<String>) -> Self {
38 Self {
39 status,
40 headers,
41 body: body.into(),
42 }
43 }
44}
45
46pub fn append_vary(headers: &mut Headers, field_name: &str) {
51 append_list_value(headers, "Vary", field_name);
52}
53
54pub fn negotiate_request(
61 headers: &Headers,
62 supported: Option<&[String]>,
63) -> Result<String, LinoHttpError> {
64 let candidates = supported
65 .map(<[String]>::to_vec)
66 .unwrap_or_else(supported_media_types);
67 let accept = headers.get("accept");
68 negotiate_media_type(accept, &candidates).ok_or_else(|| {
69 LinoHttpError::new(
70 406,
71 Some(&format!(
72 "No acceptable representation for Accept: {}",
73 accept.unwrap_or("")
74 )),
75 )
76 .with_extension("supported", array(candidates.iter().cloned().map(string)))
77 })
78}
79
80#[derive(Debug, Clone, PartialEq)]
82pub struct DecodedBody {
83 pub value: Option<LinoValue>,
85 pub media_type: Option<String>,
87}
88
89pub fn decode_request_body(
96 raw: &[u8],
97 content_type: Option<&str>,
98 max_bytes: usize,
99) -> Result<DecodedBody, LinoHttpError> {
100 if raw.len() > max_bytes {
101 return Err(LinoHttpError::new(
102 413,
103 Some(&format!("Request body exceeds {max_bytes} bytes")),
104 )
105 .with_header("Connection", "close"));
106 }
107
108 let media_type = parse_content_type(content_type);
109 if media_type.is_empty() {
110 return Ok(DecodedBody {
111 value: None,
112 media_type: None,
113 });
114 }
115
116 if !is_decodable_media_type(&media_type) {
117 return Err(LinoHttpError::new(
118 415,
119 Some(&format!("Unsupported request media type: {media_type}")),
120 )
121 .with_extension(
122 "supported",
123 array(supported_media_types().into_iter().map(string)),
124 ));
125 }
126
127 let text = String::from_utf8_lossy(raw);
128 if text.trim().is_empty() {
129 return Ok(DecodedBody {
130 value: None,
131 media_type: Some(media_type),
132 });
133 }
134
135 match decode_from(&text, &media_type) {
136 Ok(value) => Ok(DecodedBody {
137 value: Some(value),
138 media_type: Some(media_type),
139 }),
140 Err(error) => Err(LinoHttpError::new(
141 400,
142 Some(&format!("Malformed {media_type} request body: {error}")),
143 )),
144 }
145}
146
147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub struct EtagPolicy {
150 pub etag: bool,
152 pub preconditions: Option<bool>,
154 pub require_precondition: bool,
156}
157
158impl Default for EtagPolicy {
159 fn default() -> Self {
160 Self {
161 etag: true,
162 preconditions: None,
163 require_precondition: false,
164 }
165 }
166}
167
168pub fn build_response(
175 body: &Body,
176 status: u16,
177 media_type: &str,
178 headers: &Headers,
179 request_headers: &Headers,
180 method: &str,
181 policy: EtagPolicy,
182) -> Result<ResponseParts, LinoHttpError> {
183 let mut response_headers = headers.clone();
184 append_vary(&mut response_headers, "Accept");
185
186 if status == 204 || matches!(body, Body::Empty) {
187 let status = if status == 200 { 204 } else { status };
188 return Ok(ResponseParts::new(status, response_headers, ""));
189 }
190
191 let encoded = match body {
192 Body::Raw(text) => text.clone(),
193 Body::Value(value) => encode_for(value, media_type)
194 .map_err(|error| LinoHttpError::new(500, Some(&error.to_string())))?,
195 Body::Empty => unreachable!("handled above"),
196 };
197
198 if policy.etag {
199 let tag = compute_etag(&encoded);
200 response_headers.insert("ETag", tag.clone());
201
202 let safe = method == "GET" || method == "HEAD";
206 let evaluate = policy.preconditions.unwrap_or(safe);
207 if evaluate
208 && evaluate_preconditions(request_headers, method, &tag, policy.require_precondition)?
209 .not_modified
210 {
211 return Ok(ResponseParts::new(304, response_headers, ""));
212 }
213 }
214
215 response_headers.insert("Content-Type", with_charset(media_type));
216 Ok(ResponseParts::new(status, response_headers, encoded))
217}
218
219pub fn build_problem_response(
239 error: &LinoHttpError,
240 media_type: &str,
241 instance: Option<&str>,
242) -> ResponseParts {
243 let problem = error.to_problem(instance);
244 let mut headers = Headers::new();
245 for (name, value) in &error.headers {
246 headers.insert(name.clone(), value.clone());
247 }
248 append_vary(&mut headers, "Accept");
249 headers.insert("Content-Type", with_charset(problem_media_type(media_type)));
250
251 let body = encode_for(&problem, media_type)
254 .or_else(|_| encode_for(&problem, LINO_CONTENT_TYPE))
255 .unwrap_or_else(|_| String::new());
256
257 ResponseParts::new(error.status, headers, body)
258}