#![allow(non_snake_case)]
use crate::{
DataError, MyError,
db::{
actor::find_agent_id_from_str,
agent_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 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("/?<agent>&<profileId>", data = "<doc>")]
async fn put(
c: Headers,
agent: &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(),
});
}
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();
match find_agent_id_from_str(conn, agent).await {
Ok(agent_id) => {
debug!("agent_id = {}", agent_id);
let (x, _) = find(conn, agent_id, profileId).await?;
match x {
None => {
let etag = etag_from_str(doc);
upsert(conn, agent_id, profileId, doc).await?;
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 Agent Profile documents are identical");
Ok(no_content(&etag))
} else {
let etag = etag_from_str(doc);
upsert(conn, agent_id, profileId, doc).await?;
Ok(no_content(&etag))
}
}
}
}
}
}
}
Err(x) => match x {
MyError::Data(_) => Err(x.with_status(Status::BadRequest)),
x => Err(x),
},
}
}
#[post("/?<agent>&<profileId>", data = "<doc>")]
async fn post(
c: Headers,
agent: &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(),
});
}
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();
match find_agent_id_from_str(conn, agent).await {
Ok(agent_id) => {
debug!("agent_id = {}", agent_id);
let (x, _) = find(conn, agent_id, profileId).await?;
match x {
None => {
upsert(conn, agent_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 Agent 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, agent_id, profileId, &merged).await?;
let etag = etag_from_str(&merged);
Ok(no_content(&etag))
}
}
}
Err(x) => match x {
MyError::Data(_) => Err(x.with_status(Status::BadRequest)),
x => Err(x),
},
}
}
#[delete("/?<agent>&<profileId>")]
async fn delete(
c: Headers,
agent: &str,
profileId: &str,
db: &State<DB>,
user: User,
) -> Result<Status, MyError> {
debug!("----- delete ----- {}", user);
let _ = user.can_use_xapi();
let conn = db.pool();
match find_agent_id_from_str(conn, agent).await {
Ok(agent_id) => {
debug!("agent_id = {}", agent_id);
let (document, _) = get_profile(conn, agent_id, profileId).await?;
let etag = etag_from_str(&document);
debug!("etag (LaRS) = {}", etag);
match eval_preconditions!(&etag, c) {
s if s != Status::Ok => Err(MyError::HTTP {
status: s,
info: "Failed pre-condition(s)".into(),
}),
_ => {
remove(conn, agent_id, profileId).await?;
Ok(Status::NoContent)
}
}
}
Err(x) => match x {
MyError::Data(_) => Err(x.with_status(Status::BadRequest)),
x => Err(x),
},
}
}
#[get("/?<agent>&<profileId>&<since>")]
async fn get(
agent: &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();
match find_agent_id_from_str(conn, agent).await {
Ok(agent_id) => {
debug!("agent_id = {}", agent_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, agent_id, z_profile_id).await?
}
} else {
let (x, last_updated) = get_ids(conn, agent_id, since).await?;
(serde_json::to_string(&x).unwrap(), last_updated)
};
debug!("resource = {:?}", resource);
emit_doc_response(resource.0, Some(resource.1)).await
}
Err(x) => match x {
MyError::Data(_) => Err(x.with_status(Status::BadRequest)),
x => Err(x),
},
}
}
async fn get_profile(
conn: &PgPool,
actor_id: i32,
profile_id: &str,
) -> Result<(String, DateTime<Utc>), MyError> {
let (x, updated) = find(conn, actor_id, profile_id).await?;
match x {
None => Err(MyError::HTTP {
status: Status::NotFound,
info: format!("Failed find Agent Profile ({profile_id}) for Actor #{actor_id}").into(),
}),
Some(doc) => Ok((doc, updated)),
}
}
async fn get_ids(
conn: &PgPool,
actor_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, actor_id, since).await
}