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    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/// Simplified Verb representation.
45#[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    /// Return the row identifier.
65    pub fn rid(&self) -> i32 {
66        self.rid
67    }
68
69    /// Return the IRI as a `&str`.
70    pub fn iri_as_str(&self) -> &str {
71        &self.iri
72    }
73
74    /// Return the `display` as a `&str`.
75    pub fn display(&self) -> &str {
76        &self.display
77    }
78}
79
80/// Rocket Responder that returns an OK HTTP Status w/ a JSON string of either
81/// a _Verb_ or a _VerbExt_ along with ETag, and Content-Type HTTP headers.
82#[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/// A structure grouping the GET multi request parameters.
97#[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        // ensure `language` is a valid language tag...
107        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        // must be >= 0
120        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        // must be in [10..=100]
131        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
151/// Generic function to assign a value to an expected query parameter. If the
152/// named parameter is missing, a provided default value will be used instead.
153fn 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    // a valid Verb may have a None or empty `display` language map.  this is
209    // not acceptable here...
210    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/// Create new Verb resource.
236#[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/// Update existing Verb replacing its `display` field.
255#[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    // must already exist...
263    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    // must already exist...
283    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    // only update if pre-conditions exist + pass...
297    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                    // etag changed.  recompute...
319                    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/// Update existing Verb merging `display` fields.
331#[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    // must already exist...
339    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/// Update existing Verb merging `display` fields.
346#[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    // must already exist...
360    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    // proceed if pre-conditions exist + pass...
374    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        // is it valid now?
422        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}