1#![doc = include_str!("../../../doc/EXT_VERBS.md")]
4
5use crate::{
6 DataError, MyError, MyLanguageTag, Validate, Verb, config,
7 db::{
8 Aggregates,
9 schema::TVerb,
10 verb::{
11 ext_compute_aggregates, ext_find_by_iri, ext_find_by_rid, ext_find_some, ext_update,
12 insert_verb,
13 },
14 },
15 eval_preconditions,
16 lrs::{DB, Headers, User, etag_from_str, no_content, resources::WithETag},
17};
18use core::fmt;
19use iri_string::types::IriStr;
20use rocket::{
21 Request, Responder, State,
22 form::FromForm,
23 get,
24 http::{Header, Status, hyper::header},
25 patch, post, put,
26 request::{FromRequest, Outcome},
27 routes,
28};
29use serde::{Deserialize, Serialize};
30use sqlx::PgPool;
31use std::str::FromStr;
32use tracing::{debug, error, info, warn};
33
34const DEFAULT_START_RID: i32 = 0;
35const DEFAULT_COUNT: i32 = 50;
36const DEFAULT_ASC: bool = true;
37
38#[derive(Debug, Serialize)]
39pub(crate) struct VerbExt {
40 pub(crate) rid: i32,
41 pub(crate) verb: Verb,
42}
43
44#[derive(Debug, Deserialize, Serialize)]
46pub struct VerbUI {
47 pub(crate) rid: i32,
48 pub(crate) iri: String,
49 pub(crate) display: String,
50}
51
52impl VerbUI {
53 pub(crate) fn from(v: TVerb, language: &MyLanguageTag) -> Self {
54 VerbUI {
55 rid: v.id,
56 iri: v.iri,
57 display: match v.display {
58 Some(x) => String::from(x.0.get(language).unwrap_or("")),
59 None => String::from(""),
60 },
61 }
62 }
63
64 pub fn rid(&self) -> i32 {
66 self.rid
67 }
68
69 pub fn iri_as_str(&self) -> &str {
71 &self.iri
72 }
73
74 pub fn display(&self) -> &str {
76 &self.display
77 }
78}
79
80#[derive(Responder)]
83#[response(status = 200, content_type = "json")]
84struct ETaggedResource {
85 inner: String,
86 etag: Header<'static>,
87}
88
89pub(crate) struct QueryParams<'a> {
90 pub(crate) language: &'a str,
91 pub(crate) start: i32,
92 pub(crate) count: i32,
93 pub(crate) asc: bool,
94}
95
96#[rocket::async_trait]
98impl<'r> FromRequest<'r> for QueryParams<'r> {
99 type Error = MyError;
100
101 async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
102 let language = match qp::<&str>(req, "language", &config().default_language) {
103 Ok(x) => x,
104 Err(x) => return Outcome::Error((Status::BadRequest, x)),
105 };
106 match MyLanguageTag::from_str(language) {
108 Ok(_) => (),
109 Err(x) => {
110 error!("This ({}) is NOT a valid language tag: {}", language, x);
111 return Outcome::Error((Status::BadRequest, MyError::Data(x)));
112 }
113 }
114
115 let start = match qp::<i32>(req, "start", DEFAULT_START_RID) {
116 Ok(x) => x,
117 Err(x) => return Outcome::Error((Status::BadRequest, x)),
118 };
119 if start < 0 {
121 let msg = format!("Start ({start}) MUST be greater than or equal to 0");
122 error!("Failed: {}", msg);
123 return Outcome::Error((Status::BadRequest, MyError::Runtime(msg.into())));
124 }
125
126 let count = match qp::<i32>(req, "count", DEFAULT_COUNT) {
127 Ok(x) => x,
128 Err(x) => return Outcome::Error((Status::BadRequest, x)),
129 };
130 if !(10..=100).contains(&count) {
132 let msg = format!("Count ({count}) MUST be w/in [10..101]");
133 error!("Failed: {}", msg);
134 return Outcome::Error((Status::BadRequest, MyError::Runtime(msg.into())));
135 }
136
137 let asc = match qp::<bool>(req, "asc", DEFAULT_ASC) {
138 Ok(x) => x,
139 Err(x) => return Outcome::Error((Status::BadRequest, x)),
140 };
141
142 Outcome::Success(QueryParams {
143 language,
144 start,
145 count,
146 asc,
147 })
148 }
149}
150
151fn qp<'r, T: FromForm<'r>>(
154 req: &'r Request<'_>,
155 name: &str,
156 default_value: T,
157) -> Result<T, MyError> {
158 match req.query_value::<T>(name) {
159 Some(Ok(x)) => Ok(x),
160 Some(Err(x)) => {
161 let msg = format!("Failed parsing query parameter '{name}': {x}");
162 error!("{}", msg);
163 Err(MyError::Runtime(msg.into()))
164 }
165 None => {
166 info!("Missing query parameter '{}'. Use default value", name);
167 Ok(default_value)
168 }
169 }
170}
171
172impl fmt::Display for QueryParams<'_> {
173 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174 write!(
175 f,
176 "[language={}, start={}, count={}, asc? {}]",
177 self.language, self.start, self.count, self.asc
178 )
179 }
180}
181
182#[doc(hidden)]
183pub fn routes() -> Vec<rocket::Route> {
184 routes![
185 post,
186 put,
187 put_rid,
188 patch,
189 patch_rid,
190 get_iri,
191 get_rid,
192 get_aggregates,
193 get_some
194 ]
195}
196
197fn parse_verb(s: &str) -> Result<Verb, MyError> {
198 if s.is_empty() {
199 return Err(MyError::HTTP {
200 status: Status::BadRequest,
201 info: "Body must NOT be empty".into(),
202 });
203 }
204
205 let v = serde_json::from_str::<Verb>(s)
206 .map_err(|x| MyError::Data(DataError::JSON(x)).with_status(Status::BadRequest))?;
207 debug!("v = {}", v);
208 if v.is_valid() {
211 match v.display_as_map() {
212 Some(x) => {
213 if x.is_empty() {
214 Err(MyError::HTTP {
215 status: Status::BadRequest,
216 info: "Verb's 'display' language map MUST not be empty".into(),
217 })
218 } else {
219 Ok(v)
220 }
221 }
222 None => Err(MyError::HTTP {
223 status: Status::BadRequest,
224 info: "Verb's 'display' language map MUST not be null".into(),
225 }),
226 }
227 } else {
228 Err(MyError::HTTP {
229 status: Status::BadRequest,
230 info: "Verb is invalid".into(),
231 })
232 }
233}
234
235#[post("/", data = "<body>")]
237async fn post(body: &str, db: &State<DB>, user: User) -> Result<WithETag, MyError> {
238 debug!("----- post ----- {}", user);
239 user.can_use_verbs()?;
240
241 let new_verb = parse_verb(body)?;
242 let conn = db.pool();
243 let rid = insert_verb(conn, &new_verb)
244 .await
245 .map_err(|x| x.with_status(Status::BadRequest))?;
246 info!("Created Verb at #{}", rid);
247 let etag = etag_from_str(body);
248 Ok(WithETag {
249 inner: Status::Ok,
250 etag: Header::new(header::ETAG.as_str(), etag.to_string()),
251 })
252}
253
254#[put("/", data = "<body>")]
256async fn put(c: Headers, body: &str, db: &State<DB>, user: User) -> Result<WithETag, MyError> {
257 debug!("----- put ----- {}", user);
258 user.can_use_verbs()?;
259
260 let new_verb = parse_verb(body)?;
261 let conn = db.pool();
262 let x = ext_find_by_iri(conn, new_verb.id_as_str())
264 .await
265 .map_err(|x| x.with_status(Status::NotFound))?;
266 update_it(c, conn, x.rid, x.verb, new_verb).await
267}
268
269#[put("/<rid>", data = "<body>")]
270async fn put_rid(
271 c: Headers,
272 rid: i32,
273 body: &str,
274 db: &State<DB>,
275 user: User,
276) -> Result<WithETag, MyError> {
277 debug!("----- put_rid ----- {}", user);
278 user.can_use_verbs()?;
279
280 let new_verb = parse_verb(body)?;
281 let conn = db.pool();
282 let old_verb = ext_find_by_rid(conn, rid)
284 .await
285 .map_err(|x| x.with_status(Status::NotFound))?;
286 update_it(c, conn, rid, old_verb, new_verb).await
287}
288
289async fn update_it(
290 c: Headers,
291 conn: &PgPool,
292 rid: i32,
293 old_verb: Verb,
294 new_verb: Verb,
295) -> Result<WithETag, MyError> {
296 if c.has_no_conditionals() {
298 Err(MyError::HTTP {
299 status: Status::Conflict,
300 info: "Update existing Verb w/ no pre-conditions is NOT allowed".into(),
301 })
302 } else {
303 debug!("old_verb = {}", old_verb);
304 let x = serde_json::to_string(&old_verb).map_err(|x| MyError::Data(DataError::JSON(x)))?;
305 let etag = etag_from_str(&x);
306 debug!("etag (old) = {}", etag);
307 match eval_preconditions!(&etag, c) {
308 s if s != Status::Ok => Err(MyError::HTTP {
309 status: s,
310 info: "Failed pre-condition(s)".into(),
311 }),
312 _ => {
313 if new_verb == old_verb {
314 info!("Old + new Verbs are identical. Pass");
315 Ok(no_content(&etag))
316 } else {
317 ext_update(conn, rid, &new_verb).await?;
318 let x = serde_json::to_string(&new_verb)
320 .map_err(|x| MyError::Data(DataError::JSON(x)))?;
321 let etag = etag_from_str(&x);
322 debug!("etag (new) = {}", etag);
323 Ok(no_content(&etag))
324 }
325 }
326 }
327 }
328}
329
330#[patch("/", data = "<body>")]
332async fn patch(c: Headers, body: &str, db: &State<DB>, user: User) -> Result<WithETag, MyError> {
333 debug!("----- patch ----- {}", user);
334 user.can_use_verbs()?;
335
336 let new_verb = parse_verb(body)?;
337 let conn = db.pool();
338 let x = ext_find_by_iri(conn, new_verb.id_as_str())
340 .await
341 .map_err(|x| x.with_status(Status::NotFound))?;
342 patch_it(c, conn, x.rid, x.verb, new_verb).await
343}
344
345#[patch("/<rid>", data = "<body>")]
347async fn patch_rid(
348 c: Headers,
349 rid: i32,
350 body: &str,
351 db: &State<DB>,
352 user: User,
353) -> Result<WithETag, MyError> {
354 debug!("----- patch_rid ----- {}", user);
355 user.can_use_verbs()?;
356
357 let new_verb = parse_verb(body)?;
358 let conn = db.pool();
359 let old_verb = ext_find_by_rid(conn, rid)
361 .await
362 .map_err(|x| x.with_status(Status::NotFound))?;
363 patch_it(c, conn, rid, old_verb, new_verb).await
364}
365
366async fn patch_it(
367 c: Headers,
368 conn: &PgPool,
369 rid: i32,
370 mut old_verb: Verb,
371 new_verb: Verb,
372) -> Result<WithETag, MyError> {
373 if c.has_no_conditionals() {
375 Err(MyError::HTTP {
376 status: Status::Conflict,
377 info: "Patching existing Verb w/ no pre-conditions is NOT allowed".into(),
378 })
379 } else {
380 debug!("old_verb = {}", old_verb);
381 let x = serde_json::to_string(&old_verb).map_err(|x| MyError::Data(DataError::JSON(x)))?;
382 let etag = etag_from_str(&x);
383 debug!("etag (old) = {}", etag);
384 match eval_preconditions!(&etag, c) {
385 s if s != Status::Ok => Err(MyError::HTTP {
386 status: s,
387 info: "Failed pre-condition(s)".into(),
388 }),
389 _ => {
390 if new_verb == old_verb {
391 info!("Old + new Verbs are identical. Pass");
392 Ok(no_content(&etag))
393 } else if !old_verb.extend(new_verb) {
394 info!("Old + merged versions are identical. Pass");
395 Ok(no_content(&etag))
396 } else {
397 debug!("patched_verb = {}", old_verb);
398 ext_update(conn, rid, &old_verb).await?;
399 let x = serde_json::to_string(&old_verb)
400 .map_err(|x| MyError::Data(DataError::JSON(x)))?;
401 let etag = etag_from_str(&x);
402 debug!("etag (new) = {}", etag);
403 Ok(no_content(&etag))
404 }
405 }
406 }
407 }
408}
409
410#[get("/?<iri>")]
411async fn get_iri(iri: &str, db: &State<DB>, user: User) -> Result<ETaggedResource, MyError> {
412 debug!("----- get_iri ----- {}", user);
413 user.can_use_verbs()?;
414
415 let iri = if IriStr::new(iri).is_err() {
416 warn!(
417 "This <{}> is not a valid IRI. Assume it's an alias + continue",
418 iri
419 );
420 let iri2 = format!("http://adlnet.gov/expapi/verbs/{iri}");
421 if IriStr::new(&iri2).is_err() {
423 return Err(MyError::HTTP {
424 status: Status::BadRequest,
425 info: format!("Input <{iri}> is not a valid IRI nor an alias of one").into(),
426 });
427 } else {
428 iri2
429 }
430 } else {
431 iri.to_owned()
432 };
433
434 let x = ext_find_by_iri(db.pool(), &iri)
435 .await
436 .map_err(|x| x.with_status(Status::NotFound))?;
437 tag_n_bag_it::<Verb>(x.verb)
438}
439
440#[get("/<rid>")]
441async fn get_rid(rid: i32, db: &State<DB>, user: User) -> Result<ETaggedResource, MyError> {
442 debug!("----- get_rid ----- {}", user);
443 user.can_use_verbs()?;
444
445 let x = ext_find_by_rid(db.pool(), rid)
446 .await
447 .map_err(|x| x.with_status(Status::NotFound))?;
448 tag_n_bag_it::<Verb>(x)
449}
450
451#[get("/aggregates")]
452async fn get_aggregates(db: &State<DB>, user: User) -> Result<ETaggedResource, MyError> {
453 debug!("----- get_aggregates ----- {}", user);
454 user.can_use_verbs()?;
455
456 let x = ext_compute_aggregates(db.pool()).await?;
457 tag_n_bag_it::<Aggregates>(x)
458}
459
460#[get("/")]
461async fn get_some(
462 q: QueryParams<'_>,
463 db: &State<DB>,
464 user: User,
465) -> Result<ETaggedResource, MyError> {
466 debug!("----- get_some ----- {}", user);
467 user.can_use_verbs()?;
468
469 debug!("q = {}", q);
470 let x = ext_find_some(db.pool(), q).await?;
471 tag_n_bag_it::<Vec<VerbUI>>(x)
472}
473
474fn tag_n_bag_it<T: Serialize>(resource: T) -> Result<ETaggedResource, MyError> {
475 let json = serde_json::to_string(&resource).map_err(|x| MyError::Data(DataError::JSON(x)))?;
476 debug!("json = {}", json);
477 let etag = etag_from_str(&json);
478 debug!("etag = {}", etag);
479
480 Ok(ETaggedResource {
481 inner: json,
482 etag: Header::new(header::ETAG.as_str(), etag.to_string()),
483 })
484}