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