#![allow(non_snake_case)]
#![allow(clippy::too_many_arguments)]
use crate::{
DataError, MyError,
db::state::{
SingleResourceParams, as_many, as_single, find, find_ids, remove, remove_many, upsert,
},
eval_preconditions,
lrs::{
DB, User, emit_doc_response, etag_from_str,
headers::Headers,
no_content,
resources::{WithDocumentOrIDs, WithETag},
},
};
use rocket::{State, delete, futures::TryFutureExt, get, http::Status, post, put, routes};
use serde_json::{Map, Value};
use sqlx::{
PgPool,
types::chrono::{DateTime, Utc},
};
use std::mem;
use tracing::{debug, info};
#[doc(hidden)]
pub fn routes() -> Vec<rocket::Route> {
routes![put, post, get, delete]
}
#[put("/?<activityId>&<agent>&<registration>&<stateId>", data = "<doc>")]
async fn put(
c: Headers,
activityId: &str,
agent: &str,
registration: Option<&str>,
stateId: &str,
doc: &str,
db: &State<DB>,
user: User,
) -> Result<WithETag, MyError> {
debug!("----- put ----- {}", user);
user.can_use_xapi()?;
if doc.is_empty() {
return Err(MyError::HTTP {
status: Status::BadRequest,
info: "Document must NOT be an empty string".into(),
});
}
if c.is_json_content() {
serde_json::from_str::<Map<String, Value>>(doc)
.map_err(|x| MyError::Data(DataError::JSON(x)).with_status(Status::BadRequest))?;
}
let conn = db.pool();
let s = as_single(conn, activityId, agent, registration, stateId)
.map_err(|x| x.with_status(Status::BadRequest))
.await?;
debug!("s = {:?}", s);
let (x, _) = find(conn, &s).await?;
match x {
None => {
upsert(conn, &s, doc).await?;
let etag = etag_from_str(doc);
Ok(no_content(&etag))
}
Some(old_doc) => {
if c.has_no_conditionals() {
Err(MyError::HTTP {
status: Status::Conflict,
info: "PUT a known resource, w/ no pre-conditions, is NOT allowed".into(),
})
} else {
let etag = etag_from_str(&old_doc);
debug!("etag (old) = {}", etag);
match eval_preconditions!(&etag, c) {
s if s != Status::Ok => Err(MyError::HTTP {
status: s,
info: "Failed pre-condition(s)".into(),
}),
_ => {
if old_doc == doc {
info!("Old + new State documents are identidal");
Ok(no_content(&etag))
} else {
upsert(conn, &s, doc).await?;
let etag = etag_from_str(doc);
Ok(no_content(&etag))
}
}
}
}
}
}
}
#[post("/?<activityId>&<agent>&<registration>&<stateId>", data = "<doc>")]
async fn post(
c: Headers,
activityId: &str,
agent: &str,
registration: Option<&str>,
stateId: &str,
doc: &str,
db: &State<DB>,
user: User,
) -> Result<WithETag, MyError> {
debug!("----- post ----- {}", user);
user.can_use_xapi()?;
if doc.is_empty() {
return Err(MyError::HTTP {
status: Status::BadRequest,
info: "Document must NOT be an empty string".into(),
});
}
if c.is_json_content() {
serde_json::from_str::<Map<String, Value>>(doc)
.map_err(|x| MyError::Data(DataError::JSON(x)).with_status(Status::BadRequest))?;
}
let conn = db.pool();
let s = as_single(conn, activityId, agent, registration, stateId)
.map_err(|x| x.with_status(Status::BadRequest))
.await?;
debug!("s = {:?}", s);
let (x, _) = find(conn, &s).await?;
match x {
None => {
upsert(conn, &s, doc).await?;
let etag = etag_from_str(doc);
Ok(no_content(&etag))
}
Some(old_doc) => {
let etag = etag_from_str(&old_doc);
debug!("etag (old) = {}", etag);
if c.has_conditionals() {
match eval_preconditions!(&etag, c) {
s if s != Status::Ok => {
return Err(MyError::HTTP {
status: s,
info: "Failed pre-condition(s)".into(),
});
}
_ => (),
}
}
let mut old: Map<String, Value> = serde_json::from_str(&old_doc)
.map_err(|x| MyError::Data(DataError::JSON(x)).with_status(Status::BadRequest))?;
let mut new: Map<String, Value> = serde_json::from_str(doc)
.map_err(|x| MyError::Data(DataError::JSON(x)).with_status(Status::BadRequest))?;
if old == new {
info!("Old + new State documents are identical");
return Ok(no_content(&etag));
}
debug!("document (before) = '{}'", old_doc);
for (k, v) in new.iter_mut() {
let new_v = mem::take(v);
old.insert(k.to_owned(), new_v);
}
let merged = serde_json::to_string(&old).expect("Failed serialize merged document");
debug!("document ( after) = '{}'", merged);
upsert(conn, &s, &merged).await?;
let etag = etag_from_str(&merged);
Ok(no_content(&etag))
}
}
}
#[get("/?<activityId>&<agent>&<registration>&<stateId>&<since>")]
async fn get(
activityId: &str,
agent: &str,
registration: Option<&str>,
stateId: Option<&str>,
since: Option<&str>,
db: &State<DB>,
user: User,
) -> Result<WithDocumentOrIDs, MyError> {
debug!("----- get ----- {}", user);
user.can_use_xapi()?;
let conn = db.pool();
let resource = if let Some(z_state_id) = stateId {
if since.is_some() {
return Err(MyError::HTTP {
status: Status::BadRequest,
info: "Either `stateId` or `since` should be specified; not both".into(),
});
}
let s = as_single(conn, activityId, agent, registration, z_state_id)
.map_err(|x| x.with_status(Status::BadRequest))
.await?;
debug!("s = {:?}", s);
let res = get_state(conn, &s).await?;
(res.0, Some(res.1))
} else {
let s = as_many(conn, activityId, agent, registration, since)
.map_err(|x| x.with_status(Status::BadRequest))
.await?;
debug!("s = {:?}", s);
let x = find_ids(conn, &s).await?;
(serde_json::to_string(&x).unwrap(), None)
};
emit_doc_response(resource.0, resource.1).await
}
#[delete("/?<activityId>&<agent>&<registration>&<stateId>")]
async fn delete(
c: Headers,
activityId: &str,
agent: &str,
registration: Option<&str>,
stateId: Option<&str>,
db: &State<DB>,
user: User,
) -> Result<Status, MyError> {
debug!("----- delete ----- {}", user);
user.can_use_xapi()?;
let conn = db.pool();
if let Some(sid) = stateId {
delete_one(conn, c, activityId, agent, registration, sid).await
} else {
delete_many(conn, activityId, agent, registration).await
}
}
async fn get_state(
conn: &PgPool,
s: &SingleResourceParams<'_>,
) -> Result<(String, DateTime<Utc>), MyError> {
let (x, updated) = find(conn, s).await?;
match x {
None => Err(MyError::HTTP {
status: Status::NotFound,
info: format!("State ({s}) not found").into(),
}),
Some(y) => Ok((y, updated)),
}
}
async fn delete_one(
conn: &PgPool,
c: Headers,
activity_iri: &str,
agent: &str,
registration: Option<&str>,
state_id: &str,
) -> Result<Status, MyError> {
let s = as_single(conn, activity_iri, agent, registration, state_id)
.map_err(|x| x.with_status(Status::BadRequest))
.await?;
debug!("s = {:?}", s);
let (doc, _) = get_state(conn, &s).await?;
let etag = etag_from_str(&doc);
match eval_preconditions!(&etag, c) {
s if s != Status::Ok => Err(MyError::HTTP {
status: s,
info: "Failed pre-condition(s)".into(),
}),
_ => {
remove(conn, &s).await?;
Ok(Status::NoContent)
}
}
}
async fn delete_many(
conn: &PgPool,
activity_iri: &str,
agent: &str,
registration: Option<&str>,
) -> Result<Status, MyError> {
let s = as_single(conn, activity_iri, agent, registration, "")
.map_err(|x| x.with_status(Status::BadRequest))
.await?;
debug!("s = {:?}", s);
remove_many(conn, &s).await?;
Ok(Status::NoContent)
}