Skip to main content

xapi_rs/lrs/resources/
verbs.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3#![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/// Simplified Verb representation.
46#[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    /// Return the row identifier.
66    pub fn rid(&self) -> i32 {
67        self.rid
68    }
69
70    /// Return the IRI as a `&str`.
71    pub fn iri_as_str(&self) -> &str {
72        &self.iri
73    }
74
75    /// Return the `display` as a `&str`.
76    pub fn display(&self) -> &str {
77        &self.display
78    }
79}
80
81/// Rocket Responder that returns an OK HTTP Status w/ a JSON string of either
82/// a _Verb_ or a _VerbExt_ along with ETag, and Content-Type HTTP headers.
83#[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/// A structure grouping the GET multi request parameters.
98#[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        // ensure `language` is a valid language tag...
108        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        // must be >= 0
121        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        // must be in [10..=100]
132        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
152/// Generic function to assign a value to an expected query parameter. If the
153/// named parameter is missing, a provided default value will be used instead.
154fn 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    // a valid Verb may have a None or empty `display` language map.  this is
210    // not acceptable here...
211    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/// Create new Verb resource.
237#[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/// Update existing Verb replacing its `display` field.
256#[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    // must already exist...
264    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    // must already exist...
284    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    // only update if pre-conditions exist + pass...
298    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                    // etag changed.  recompute...
320                    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/// Update existing Verb merging `display` fields.
332#[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    // must already exist...
340    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/// Update existing Verb merging `display` fields.
347#[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    // must already exist...
361    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    // proceed if pre-conditions exist + pass...
375    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        // is it valid now?
423        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}