1use std::sync::Arc;
8
9use lino_objects_codec::LinoValue;
10
11use crate::app::LinoApp;
12use crate::codec::encode_for;
13use crate::collection::pagination_link_header;
14use crate::etag::{compute_etag, evaluate_preconditions};
15use crate::problem::LinoHttpError;
16use crate::request::{LinoRequest, percent_encode};
17use crate::response::{Body, LinoResult, created, no_content, ok};
18use crate::store::Store;
19use crate::value::{get, to_query_text, with};
20
21pub const RESOURCE_OPERATIONS: [&str; 6] = ["list", "create", "get", "update", "patch", "remove"];
23
24#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct ResourceOptions {
27 pub id_param: String,
29 pub id_field: Option<String>,
31 pub name: Option<String>,
33 pub operations: Vec<String>,
35 pub require_precondition: bool,
37 pub upsert: bool,
39}
40
41impl Default for ResourceOptions {
42 fn default() -> Self {
43 Self {
44 id_param: "id".to_string(),
45 id_field: None,
46 name: None,
47 operations: RESOURCE_OPERATIONS.iter().map(|s| s.to_string()).collect(),
48 require_precondition: false,
49 upsert: false,
50 }
51 }
52}
53
54impl ResourceOptions {
55 pub fn new() -> Self {
57 Self::default()
58 }
59
60 pub fn with_name<S: Into<String>>(mut self, name: S) -> Self {
62 self.name = Some(name.into());
63 self
64 }
65
66 pub fn with_id_param<S: Into<String>>(mut self, id_param: S) -> Self {
68 self.id_param = id_param.into();
69 self
70 }
71
72 pub fn with_id_field<S: Into<String>>(mut self, id_field: S) -> Self {
74 self.id_field = Some(id_field.into());
75 self
76 }
77
78 pub fn with_operations<I, S>(mut self, operations: I) -> Self
80 where
81 I: IntoIterator<Item = S>,
82 S: Into<String>,
83 {
84 self.operations = operations.into_iter().map(Into::into).collect();
85 self
86 }
87
88 pub fn requiring_precondition(mut self, require: bool) -> Self {
90 self.require_precondition = require;
91 self
92 }
93
94 pub fn with_upsert(mut self, upsert: bool) -> Self {
96 self.upsert = upsert;
97 self
98 }
99
100 fn exposes(&self, operation: &str) -> bool {
102 self.operations.iter().any(|name| name == operation)
103 }
104}
105
106pub fn representation_etag(
115 request: &LinoRequest,
116 value: &LinoValue,
117) -> Result<String, LinoHttpError> {
118 let encoded = encode_for(value, &request.media_type)
119 .map_err(|error| LinoHttpError::new(500, Some(&error.to_string())))?;
120 Ok(compute_etag(&encoded))
121}
122
123fn not_found(name: &str, identifier: &str) -> LinoHttpError {
125 LinoHttpError::new(404, Some(&format!("{name} {identifier} does not exist")))
126}
127
128fn page_number(envelope: &LinoValue, key: &str) -> usize {
130 match get(envelope, "page").and_then(|page| get(page, key)) {
131 Some(LinoValue::Int(number)) if *number >= 0 => *number as usize,
132 _ => 0,
133 }
134}
135
136fn item_location(base_path: &str, identifier: &LinoValue) -> String {
138 format!("{base_path}/{}", percent_encode(&to_query_text(identifier)))
139}
140
141async fn load_for_write(
150 store: &dyn Store,
151 request: &LinoRequest,
152 identifier: &str,
153 require_precondition: bool,
154) -> Result<Option<LinoValue>, LinoHttpError> {
155 let Some(existing) = store.get(identifier).await? else {
156 return Ok(None);
157 };
158 evaluate_preconditions(
159 &request.headers,
160 &request.method,
161 &representation_etag(request, &existing)?,
162 require_precondition,
163 )?;
164 Ok(Some(existing))
165}
166
167pub fn register_resource<S: Store + 'static>(
190 app: &mut LinoApp,
191 base_path: &str,
192 store: Arc<S>,
193 options: ResourceOptions,
194) {
195 let field = options
196 .id_field
197 .clone()
198 .unwrap_or_else(|| store.id_field().to_string());
199 let name = options
200 .name
201 .clone()
202 .unwrap_or_else(|| base_path.trim_matches('/').to_string());
203 let item_path = format!("{base_path}/:{}", options.id_param);
204 let base_path = base_path.to_string();
205
206 if options.exposes("list") {
207 let store = store.clone();
208 app.get(
209 &base_path,
210 &format!("List {name}"),
211 move |request: LinoRequest| {
212 let store = store.clone();
213 async move {
214 let query = request.collection_query()?;
215 let envelope = store.list(&query).await?;
216 let link = pagination_link_header(
217 &request.path,
218 &request.query(),
219 page_number(&envelope, "limit"),
220 page_number(&envelope, "offset"),
221 page_number(&envelope, "total"),
222 );
223 let result = ok(envelope);
224 Ok(if link.is_empty() {
225 result
226 } else {
227 result.with_header("Link", link)
228 })
229 }
230 },
231 );
232 }
233
234 if options.exposes("create") {
235 let store = store.clone();
236 let field = field.clone();
237 let base = base_path.clone();
238 app.post(
239 &base_path,
240 &format!("Create {name}"),
241 move |request: LinoRequest| {
242 let store = store.clone();
243 let field = field.clone();
244 let base = base.clone();
245 async move {
246 let body = request.require_body()?.clone();
247 let item = store.create(&body).await?;
248 let location = match get(&item, &field) {
249 Some(LinoValue::Null) | None => None,
250 Some(identifier) => Some(item_location(&base, identifier)),
251 };
252 Ok(match location {
253 Some(location) => created(item, &location),
254 None => LinoResult::new(201, Body::Value(item)),
255 })
256 }
257 },
258 );
259 }
260
261 if options.exposes("get") {
262 let store = store.clone();
263 let name = name.clone();
264 let id_param = options.id_param.clone();
265 app.get(
266 &item_path,
267 &format!("Read one {name}"),
268 move |request: LinoRequest| {
269 let store = store.clone();
270 let name = name.clone();
271 let id_param = id_param.clone();
272 async move {
273 let identifier = request.param(&id_param).unwrap_or_default();
274 store
275 .get(&identifier)
276 .await?
277 .ok_or_else(|| not_found(&name, &identifier))
278 }
279 },
280 );
281 }
282
283 if options.exposes("update") {
284 let store = store.clone();
285 let name = name.clone();
286 let field = field.clone();
287 let base = base_path.clone();
288 let id_param = options.id_param.clone();
289 let require_precondition = options.require_precondition;
290 let upsert = options.upsert;
291 app.put(
292 &item_path,
293 &format!("Replace one {name}"),
294 move |request: LinoRequest| {
295 let store = store.clone();
296 let name = name.clone();
297 let field = field.clone();
298 let base = base.clone();
299 let id_param = id_param.clone();
300 async move {
301 let body = request.require_body()?.clone();
302 let identifier = request.param(&id_param).unwrap_or_default();
303 let existing =
304 load_for_write(&*store, &request, &identifier, require_precondition)
305 .await?;
306 if existing.is_none() {
307 if !upsert {
308 return Err(not_found(&name, &identifier));
309 }
310 let seeded = with(
311 &body,
312 field.clone(),
313 crate::store::normalize_id(&identifier),
314 );
315 let item = store.create(&seeded).await?;
316 let location = match get(&item, &field) {
317 Some(LinoValue::Null) | None => {
318 item_location(&base, &crate::value::string(identifier))
319 }
320 Some(value) => item_location(&base, value),
321 };
322 return Ok(created(item, &location));
323 }
324 store
325 .update(&identifier, &body)
326 .await?
327 .map(ok)
328 .ok_or_else(|| not_found(&name, &identifier))
329 }
330 },
331 );
332 }
333
334 if options.exposes("patch") {
335 let store = store.clone();
336 let name = name.clone();
337 let id_param = options.id_param.clone();
338 let require_precondition = options.require_precondition;
339 app.patch(
340 &item_path,
341 &format!("Merge changes into one {name}"),
342 move |request: LinoRequest| {
343 let store = store.clone();
344 let name = name.clone();
345 let id_param = id_param.clone();
346 async move {
347 let body = request.require_body()?.clone();
348 let identifier = request.param(&id_param).unwrap_or_default();
349 if load_for_write(&*store, &request, &identifier, require_precondition)
350 .await?
351 .is_none()
352 {
353 return Err(not_found(&name, &identifier));
354 }
355 store
356 .patch(&identifier, &body)
357 .await?
358 .map(ok)
359 .ok_or_else(|| not_found(&name, &identifier))
360 }
361 },
362 );
363 }
364
365 if options.exposes("remove") {
366 let store = store.clone();
367 let name = name.clone();
368 let id_param = options.id_param.clone();
369 let require_precondition = options.require_precondition;
370 app.delete(
371 &item_path,
372 &format!("Delete one {name}"),
373 move |request: LinoRequest| {
374 let store = store.clone();
375 let name = name.clone();
376 let id_param = id_param.clone();
377 async move {
378 let identifier = request.param(&id_param).unwrap_or_default();
379 if load_for_write(&*store, &request, &identifier, require_precondition)
380 .await?
381 .is_none()
382 {
383 return Err(not_found(&name, &identifier));
384 }
385 store.remove(&identifier).await?;
386 Ok(no_content())
387 }
388 },
389 );
390 }
391}