use crate::{
MyError, MyLanguageTag,
data::{Canonical, EMPTY_LANGUAGE_MAP, Format, Verb},
db::{Aggregates, RowID, schema::TVerb},
emit_db_error,
lrs::resources::verbs::{QueryParams, VerbExt, VerbUI},
};
use iri_string::types::IriStr;
use sqlx::PgPool;
use std::str::FromStr;
use tracing::{debug, error};
const FIND_BY_IRI: &str = r#"SELECT * FROM verb WHERE iri = $1"#;
#[cfg(test)]
async fn find_verb_by_iri(conn: &PgPool, iri: &str, format: &Format) -> Result<Verb, MyError> {
match sqlx::query_as::<_, TVerb>(FIND_BY_IRI)
.bind(iri)
.fetch_one(conn)
.await
{
Ok(x) => build_verb(x, format),
Err(x) => emit_db_error!(x, "Failed find Verb ({})", iri),
}
}
const INSERT: &str = r#"INSERT INTO verb (iri, display) VALUES ($1, $2) RETURNING id"#;
pub(crate) async fn insert_verb(conn: &PgPool, v: &Verb) -> Result<i32, MyError> {
let iri = v.id_as_str();
let display = match v.display_as_map() {
Some(x) => sqlx::types::Json(x.clone()),
_ => sqlx::types::Json(EMPTY_LANGUAGE_MAP),
};
match sqlx::query_as::<_, RowID>(INSERT)
.bind(iri)
.bind(display)
.fetch_one(conn)
.await
{
Ok(x) => Ok(x.0),
Err(x) => emit_db_error!(x, "Failed insert ({})", v),
}
}
#[allow(dead_code)]
const UPDATE: &str = r#"UPDATE verb SET display = $2 WHERE iri = $1 RETURNING id"#;
pub(crate) async fn update_verb(conn: &PgPool, v: &Verb) -> Result<i32, MyError> {
let iri = v.id_as_str();
match sqlx::query_as::<_, TVerb>(FIND_BY_IRI)
.bind(iri)
.fetch_one(conn)
.await
{
Ok(x) => {
let new_display = v.display_as_map();
match new_display {
Some(nd) => {
if nd.is_empty() {
Ok(x.id)
} else {
let display = match x.display {
Some(y) => {
let mut old_display = y.0;
old_display.extend(nd.clone());
old_display
}
None => new_display.unwrap().to_owned(),
};
match sqlx::query_as::<_, RowID>(UPDATE)
.bind(iri)
.bind(Some(sqlx::types::Json(display)))
.fetch_one(conn)
.await
{
Ok(x) => Ok(x.0),
Err(x) => emit_db_error!(x, "Failed update display for Verb <{}>", iri),
}
}
}
None => Ok(x.id),
}
}
Err(_) => insert_verb(conn, v).await,
}
}
const FIND_ID: &str = r#"SELECT id FROM verb WHERE iri = $1"#;
pub(crate) async fn find_verb_id(conn: &PgPool, iri: &IriStr) -> Result<Option<i32>, MyError> {
match sqlx::query_as::<_, RowID>(FIND_ID)
.bind(iri.normalize().to_string().as_str())
.fetch_one(conn)
.await
{
Ok(x) => Ok(Some(x.0)),
Err(x) => match x {
sqlx::Error::RowNotFound => Ok(None),
x => emit_db_error!(x, "Failed find Verb ({})", iri),
},
}
}
const FIND: &str = r#"SELECT * FROM verb WHERE id = $1"#;
pub(crate) async fn find_verb(conn: &PgPool, id: i32, format: &Format) -> Result<Verb, MyError> {
match sqlx::query_as::<_, TVerb>(FIND)
.bind(id)
.fetch_one(conn)
.await
{
Ok(x) => build_verb(x, format),
Err(x) => emit_db_error!(x, "Failed find Verb #{}", id),
}
}
fn build_verb(row: TVerb, format: &Format) -> Result<Verb, MyError> {
let builder = Verb::builder().id(&row.iri)?;
if format.is_ids() {
Ok(builder.build()?)
} else if let Some(map) = row.display {
let mut res = builder.with_display(map.0)?.build()?;
if format.is_canonical() {
res.canonicalize(format.tags());
}
debug!("res = {}", res);
Ok(res)
} else {
Ok(builder.build()?)
}
}
pub(crate) async fn ext_find_by_iri(conn: &PgPool, iri: &str) -> Result<VerbExt, MyError> {
match sqlx::query_as::<_, TVerb>(FIND_BY_IRI)
.bind(iri)
.fetch_one(conn)
.await
{
Ok(row) => Ok(VerbExt {
rid: row.id,
verb: build_verb(row, &Format::default())?,
}),
Err(x) => emit_db_error!(x, "Failed finding Verb <{}>", iri),
}
}
const FIND_BY_RID: &str = r#"SELECT * FROM verb WHERE id = $1"#;
pub(crate) async fn ext_find_by_rid(conn: &PgPool, rid: i32) -> Result<Verb, MyError> {
match sqlx::query_as::<_, TVerb>(FIND_BY_RID)
.bind(rid)
.fetch_one(conn)
.await
{
Ok(row) => Ok(build_verb(row, &Format::default())?),
Err(x) => emit_db_error!(x, "Failed finding Verb @{}", rid),
}
}
const UPDATE_DISPLAY: &str = r#"UPDATE verb SET display = $2 WHERE id = $1"#;
pub(crate) async fn ext_update(conn: &PgPool, id: i32, v: &Verb) -> Result<(), MyError> {
let display = match v.display_as_map() {
Some(x) => sqlx::types::Json(x.clone()),
_ => sqlx::types::Json(EMPTY_LANGUAGE_MAP),
};
match sqlx::query(UPDATE_DISPLAY)
.bind(id)
.bind(display)
.execute(conn)
.await
{
Ok(_) => Ok(()),
Err(x) => emit_db_error!(x, "Failed updating Verb @{}", id),
}
}
const COMPUTE_AGGREGATES: &str = r#"SELECT MIN(id), MAX(id), COUNT(id) FROM verb"#;
pub(crate) async fn ext_compute_aggregates(conn: &PgPool) -> Result<Aggregates, MyError> {
match sqlx::query_as::<_, Aggregates>(COMPUTE_AGGREGATES)
.fetch_one(conn)
.await
{
Ok(x) => Ok(x),
Err(x) => emit_db_error!(x, "Failed computing Verb aggregates"),
}
}
const FIND_SOME_ASC: &str = r#"SELECT * FROM verb WHERE id >= $1 ORDER BY id LIMIT $2"#;
const FIND_SOME_DESC: &str = r#"SELECT * FROM verb WHERE id >= $1 ORDER BY id DESC LIMIT $2"#;
pub(crate) async fn ext_find_some(
conn: &PgPool,
q: QueryParams<'_>,
) -> Result<Vec<VerbUI>, MyError> {
let language = match MyLanguageTag::from_str(q.language) {
Ok(x) => x,
Err(x) => {
error!("Failed coercing '{}' to a language tag: {}", q.language, x);
return Err(MyError::Data(x));
}
};
let mut result: Vec<VerbUI> = vec![];
let sql = if q.asc { FIND_SOME_ASC } else { FIND_SOME_DESC };
match sqlx::query_as::<_, TVerb>(sql)
.bind(q.start)
.bind(q.count)
.fetch_all(conn)
.await
{
Ok(rows) => {
for r in rows {
result.push(VerbUI::from(r, &language));
}
Ok(result)
}
Err(x) => emit_db_error!(x, "Failed finding some verbs"),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{MyError, MyLanguageTag, db::MockDB};
use std::str::FromStr;
use tracing_test::traced_test;
#[traced_test]
#[tokio::test]
async fn test_valid_verb() -> Result<(), MyError> {
let mdb = MockDB::new();
let conn = &mdb.pool().await;
const VOIDED: &str = "http://adlnet.gov/expapi/verbs/voided";
let result = find_verb_by_iri(conn, VOIDED, &Format::default()).await;
assert!(result.is_ok());
let verb = result.unwrap();
assert_eq!(verb.id(), VOIDED);
let en = MyLanguageTag::from_str("en")?;
assert_eq!(verb.display(&en).unwrap(), "voided");
Ok(())
}
#[traced_test]
#[tokio::test]
async fn test_invalid_verb() {
let mdb = MockDB::new();
let conn = &mdb.pool().await;
let result = find_verb_by_iri(conn, "foo", &Format::default()).await;
assert!(result.is_err());
}
#[traced_test]
#[tokio::test]
async fn test_verb_ops() -> Result<(), MyError> {
let mdb = MockDB::new();
let conn = &mdb.pool().await;
const SENT_IRI: &str = "http://example.com/xapi/verbs#sent-a-statement";
let us = MyLanguageTag::from_str("en-US")?;
let fr = MyLanguageTag::from_str("fr")?;
let r1 = find_verb_by_iri(conn, SENT_IRI, &Format::default()).await;
assert!(r1.is_err());
let v1 = Verb::builder()
.id(SENT_IRI)?
.display(&us, "sent")?
.build()?;
let r2 = insert_verb(conn, &v1).await;
assert!(r2.is_ok());
let r3 = insert_verb(conn, &v1).await;
assert!(r3.is_err());
let v1bis = Verb::builder()
.id(SENT_IRI)?
.display(&us, "sent")?
.display(&fr, "envoyé")?
.build()?;
let r4 = update_verb(conn, &v1bis).await;
assert!(r4.is_ok());
let r5 = find_verb_by_iri(conn, SENT_IRI, &Format::default()).await;
assert!(r5.is_ok());
let v2 = r5.unwrap();
assert_eq!(v1bis, v2);
assert_eq!(v2.display(&fr), Some("envoyé"));
Ok(())
}
}