#![allow(non_snake_case)]
use crate::{
DataError, MyError,
data::Activity,
db::{
activity::{find_activity_id, insert_activity_iri},
activity_profile::{find, find_ids, remove, upsert},
},
eval_preconditions,
lrs::{
DB, Headers, User, WithDocumentOrIDs, emit_doc_response, etag_from_str, no_content,
resources::WithETag,
},
};
use chrono::{DateTime, Utc};
use iri_string::types::IriStr;
use rocket::{State, delete, get, http::Status, post, put, routes};
use serde_json::{Map, Value};
use sqlx::PgPool;
use std::mem;
use tracing::{debug, info};
#[doc(hidden)]
pub fn routes() -> Vec<rocket::Route> {
routes![put, post, delete, get]
}
#[put("/?<activityId>&<profileId>", data = "<doc>")]
async fn put(
c: Headers,
activityId: &str,
profileId: &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(),
});
}
let activity_iri = IriStr::new(activityId)
.map_err(|x| MyError::Data(DataError::IRI(x)).with_status(Status::BadRequest))?;
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 activity_id = insert_activity_iri(conn, activity_iri).await?;
debug!("activity_id = {}", activity_id);
let (x, _) = find(conn, activity_id, profileId).await?;
match x {
None => {
upsert(conn, activity_id, profileId, 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 Activity Profile documents are identical");
Ok(no_content(&etag))
} else {
upsert(conn, activity_id, profileId, doc).await?;
let etag = etag_from_str(doc);
Ok(no_content(&etag))
}
}
}
}
}
}
}
#[post("/?<activityId>&<profileId>", data = "<doc>")]
async fn post(
c: Headers,
activityId: &str,
profileId: &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(),
});
}
let activity_iri = IriStr::new(activityId)
.map_err(|x| MyError::Data(DataError::IRI(x)).with_status(Status::BadRequest))?;
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 activity_id = insert_activity_iri(conn, activity_iri).await?;
debug!("activity_id = {}", activity_id);
let (x, _) = find(conn, activity_id, profileId).await?;
match x {
None => {
upsert(conn, activity_id, profileId, 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 Activity Profile 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, activity_id, profileId, &merged).await?;
let etag = etag_from_str(&merged);
Ok(no_content(&etag))
}
}
}
#[delete("/?<activityId>&<profileId>")]
async fn delete(
c: Headers,
activityId: &str,
profileId: &str,
db: &State<DB>,
user: User,
) -> Result<Status, MyError> {
debug!("----- delete ----- {}", user);
let _ = user.can_use_xapi();
let activity_iri = IriStr::new(activityId)
.map_err(|x| MyError::Data(DataError::IRI(x)).with_status(Status::BadRequest))?;
let conn = db.pool();
let x = find_activity_id(conn, activity_iri).await?;
match x {
None => {
info!("No such Activity ({})", activity_iri);
Ok(Status::NoContent)
}
Some(activity_id) => {
let document = match get_profile(conn, activity_id, profileId).await {
Ok((x, _)) => x,
Err(x) => match x {
MyError::HTTP { status, .. } => match status.code {
404 => return Ok(Status::NoContent),
_ => return Err(x),
},
_ => return Err(x),
},
};
let etag = etag_from_str(&document);
match eval_preconditions!(&etag, c) {
s if s != Status::Ok => Err(MyError::HTTP {
status: s,
info: "Failed pre-condition(s)".into(),
}),
_ => {
remove(conn, activity_id, profileId).await?;
Ok(Status::NoContent)
}
}
}
}
}
#[get("/?<activityId>&<profileId>&<since>")]
async fn get(
activityId: &str,
profileId: 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 activity = Activity::from_iri_str(activityId)
.map_err(|x| MyError::Data(x).with_status(Status::BadRequest))?;
let x = find_activity_id(conn, activity.id()).await?;
match x {
None => Err(MyError::HTTP {
status: Status::BadRequest,
info: format!("No such Activity ({})", activity.id()).into(),
}),
Some(activity_id) => {
let resource = if let Some(z_profile_id) = profileId {
if since.is_some() {
return Err(MyError::HTTP {
status: Status::BadRequest,
info: "Either `profileId` or `since` should be specified; not both".into(),
});
} else {
get_profile(conn, activity_id, z_profile_id).await?
}
} else {
let (x, last_updated) = get_ids(conn, activity_id, since).await?;
(serde_json::to_string(&x).unwrap(), last_updated)
};
emit_doc_response(resource.0, Some(resource.1)).await
}
}
}
async fn get_profile(
conn: &PgPool,
activity_id: i32,
profile_id: &str,
) -> Result<(String, DateTime<Utc>), MyError> {
let (x, updated) = find(conn, activity_id, profile_id).await?;
match x {
None => Err(MyError::HTTP {
status: Status::NotFound,
info: format!(
"No profile found for activity ({activity_id}), and profile ({profile_id})"
)
.into(),
}),
Some(doc) => Ok((doc, updated)),
}
}
async fn get_ids(
conn: &PgPool,
activity_id: i32,
since: Option<&str>,
) -> Result<(Vec<String>, DateTime<Utc>), MyError> {
let since = if let Some(z_datetime) = since {
let x = DateTime::parse_from_rfc3339(z_datetime)
.map_err(|x| MyError::Data(DataError::Time(x)).with_status(Status::BadRequest))?;
Some(x.with_timezone(&Utc))
} else {
None
};
find_ids(conn, activity_id, since).await
}