#![doc = include_str!("../../../doc/Resources.md")]
pub mod about;
pub mod activities;
pub mod activity_profile;
pub mod agent_profile;
pub mod agents;
pub mod state;
pub mod statement;
pub mod stats;
pub mod users;
pub mod verbs;
use crate::{
MyError,
lrs::{Headers, server::get_consistent_thru},
};
use chrono::{DateTime, SecondsFormat, Utc};
use etag::EntityTag;
use rocket::{
Responder,
http::{Header, Status, hyper::header},
serde::json::Json,
};
use serde::Serialize;
use tracing::debug;
use xapi_data::DataError;
#[derive(Responder)]
#[response(status = 200, content_type = "json")]
pub(crate) struct WithResource<T> {
inner: Json<T>,
etag: Header<'static>,
last_modified: Header<'static>,
}
#[derive(Responder)]
#[response(status = 200, content_type = "json")]
pub(crate) struct WithDocumentOrIDs {
inner: String,
etag: Header<'static>,
last_modified: Header<'static>,
}
#[derive(Responder)]
pub(crate) struct WithETag {
inner: Status,
etag: Header<'static>,
}
pub(crate) fn etag_from_str(s: &str) -> EntityTag {
EntityTag::from_data(s.as_bytes())
}
pub(crate) fn compute_etag<T>(res: &T) -> Result<EntityTag, MyError>
where
T: ?Sized + Serialize,
{
let json = serde_json::to_string(res).map_err(|x| MyError::Data(DataError::JSON(x)))?;
Ok(etag_from_str(&json))
}
pub(crate) async fn do_emit_response<T: Serialize>(
c: Headers,
resource: T,
timestamp: Option<DateTime<Utc>>,
) -> Result<WithResource<T>, MyError> {
let etag = compute_etag(&resource)?;
debug!("Etag = '{}'", etag);
let last_modified = if let Some(x) = timestamp {
x.to_rfc3339_opts(SecondsFormat::Millis, true)
} else {
get_consistent_thru()
.await
.to_rfc3339_opts(SecondsFormat::Millis, true)
};
debug!("Last-Modified = '{}'", last_modified);
let response = Ok(WithResource {
inner: Json(resource),
etag: Header::new(header::ETAG.as_str(), etag.to_string()),
last_modified: Header::new(header::LAST_MODIFIED.as_str(), last_modified),
});
if !c.has_conditionals() {
debug!("Request has no If-xxx headers");
return response;
}
if c.has_if_match() {
if c.pass_if_match(&etag) {
debug!("ETag passed If-Match pre-condition");
return response;
}
return Err(MyError::HTTP {
status: Status::PreconditionFailed,
info: "ETag failed If-Match pre-condition".into(),
});
}
if c.pass_if_none_match(&etag) {
debug!("ETag passed If-None-Match pre-condition");
return response;
}
Err(MyError::HTTP {
status: Status::NotModified,
info: "ETag failed If-None-Match pre-condition".into(),
})
}
#[macro_export]
macro_rules! emit_response {
( $headers:expr, $resource:expr => $T:ident, $timestamp:expr ) => {
$crate::lrs::resources::do_emit_response::<$T>($headers, $resource, Some($timestamp)).await
};
( $headers:expr, $resource:expr => $T:ident ) => {
$crate::lrs::resources::do_emit_response::<$T>($headers, $resource, None).await
};
}
pub(crate) async fn emit_doc_response(
resource: String,
timestamp: Option<DateTime<Utc>>,
) -> Result<WithDocumentOrIDs, MyError> {
let etag = etag_from_str(&resource);
debug!("etag = '{}'", etag);
let last_modified = if let Some(x) = timestamp {
x.to_rfc3339_opts(SecondsFormat::Millis, true)
} else {
get_consistent_thru()
.await
.to_rfc3339_opts(SecondsFormat::Millis, true)
};
Ok(WithDocumentOrIDs {
inner: resource,
etag: Header::new(header::ETAG.as_str(), etag.to_string()),
last_modified: Header::new(header::LAST_MODIFIED.as_str(), last_modified),
})
}
#[macro_export]
macro_rules! eval_preconditions {
( $etag: expr, $headers: expr ) => {
if !$headers.has_conditionals() {
tracing::debug!("Request has no If-xxx headers");
Status::Ok
} else if $headers.has_if_match() {
if $headers.pass_if_match($etag) {
tracing::debug!("ETag passed If-Match pre-condition");
Status::Ok
} else {
tracing::debug!("ETag failed If-Match pre-condition");
Status::PreconditionFailed
}
} else if $headers.pass_if_none_match($etag) {
tracing::debug!("ETag passed If-None-Match pre-condition");
Status::Ok
} else {
tracing::debug!("ETag failed If-None-Match pre-condition");
Status::PreconditionFailed
}
};
}
pub(crate) fn no_content(etag: &EntityTag) -> WithETag {
WithETag {
inner: Status::NoContent,
etag: Header::new(header::ETAG.as_str(), etag.to_string()),
}
}