xapi_rs/lrs/resources/
mod.rs1#![doc = include_str!("../../../doc/Resources.md")]
4
5pub mod about;
6pub mod activities;
7pub mod activity_profile;
8pub mod agent_profile;
9pub mod agents;
10pub mod state;
11pub mod statement;
12pub mod stats;
13pub mod users;
14pub mod verbs;
15
16use crate::{
17 DataError, MyError,
18 lrs::{Headers, server::get_consistent_thru},
19};
20use chrono::{DateTime, SecondsFormat, Utc};
21use etag::EntityTag;
22use rocket::{
23 Responder,
24 http::{Header, Status, hyper::header},
25 serde::json::Json,
26};
27use serde::Serialize;
28use tracing::debug;
29
30#[derive(Responder)]
34#[response(status = 200, content_type = "json")]
35pub(crate) struct WithResource<T> {
36 inner: Json<T>,
37 etag: Header<'static>,
38 last_modified: Header<'static>,
39}
40
41#[derive(Responder)]
42#[response(status = 200, content_type = "json")]
43pub(crate) struct WithDocumentOrIDs {
44 inner: String,
45 etag: Header<'static>,
46 last_modified: Header<'static>,
47}
48
49#[derive(Responder)]
51pub(crate) struct WithETag {
52 inner: Status,
53 etag: Header<'static>,
54}
55
56pub(crate) fn etag_from_str(s: &str) -> EntityTag {
59 EntityTag::from_data(s.as_bytes())
60}
61
62pub(crate) fn compute_etag<T>(res: &T) -> Result<EntityTag, MyError>
67where
68 T: ?Sized + Serialize,
69{
70 let json = serde_json::to_string(res).map_err(|x| MyError::Data(DataError::JSON(x)))?;
72 Ok(etag_from_str(&json))
73}
74
75pub(crate) async fn do_emit_response<T: Serialize>(
82 c: Headers,
83 resource: T,
84 timestamp: Option<DateTime<Utc>>,
85) -> Result<WithResource<T>, MyError> {
86 let etag = compute_etag(&resource)?;
87 debug!("Etag = '{}'", etag);
88
89 let last_modified = if let Some(x) = timestamp {
90 x.to_rfc3339_opts(SecondsFormat::Millis, true)
91 } else {
92 get_consistent_thru()
93 .await
94 .to_rfc3339_opts(SecondsFormat::Millis, true)
95 };
96 debug!("Last-Modified = '{}'", last_modified);
97
98 let response = Ok(WithResource {
99 inner: Json(resource),
100 etag: Header::new(header::ETAG.as_str(), etag.to_string()),
101 last_modified: Header::new(header::LAST_MODIFIED.as_str(), last_modified),
102 });
103
104 if !c.has_conditionals() {
105 debug!("Request has no If-xxx headers");
106 return response;
107 }
108
109 if c.has_if_match() {
110 if c.pass_if_match(&etag) {
111 debug!("ETag passed If-Match pre-condition");
112 return response;
113 }
114
115 return Err(MyError::HTTP {
116 status: Status::PreconditionFailed,
117 info: "ETag failed If-Match pre-condition".into(),
118 });
119 }
120
121 if c.pass_if_none_match(&etag) {
122 debug!("ETag passed If-None-Match pre-condition");
123 return response;
124 }
125
126 Err(MyError::HTTP {
127 status: Status::NotModified,
128 info: "ETag failed If-None-Match pre-condition".into(),
129 })
130}
131
132#[macro_export]
140macro_rules! emit_response {
141 ( $headers:expr, $resource:expr => $T:ident, $timestamp:expr ) => {
142 $crate::lrs::resources::do_emit_response::<$T>($headers, $resource, Some($timestamp)).await
143 };
144
145 ( $headers:expr, $resource:expr => $T:ident ) => {
146 $crate::lrs::resources::do_emit_response::<$T>($headers, $resource, None).await
147 };
148}
149
150pub(crate) async fn emit_doc_response(
157 resource: String,
158 timestamp: Option<DateTime<Utc>>,
159) -> Result<WithDocumentOrIDs, MyError> {
160 let etag = etag_from_str(&resource);
161 debug!("etag = '{}'", etag);
162 let last_modified = if let Some(x) = timestamp {
163 x.to_rfc3339_opts(SecondsFormat::Millis, true)
164 } else {
165 get_consistent_thru()
166 .await
167 .to_rfc3339_opts(SecondsFormat::Millis, true)
168 };
169
170 Ok(WithDocumentOrIDs {
171 inner: resource,
172 etag: Header::new(header::ETAG.as_str(), etag.to_string()),
173 last_modified: Header::new(header::LAST_MODIFIED.as_str(), last_modified),
174 })
175}
176
177#[macro_export]
185macro_rules! eval_preconditions {
186 ( $etag: expr, $headers: expr ) => {
187 if !$headers.has_conditionals() {
188 tracing::debug!("Request has no If-xxx headers");
189 Status::Ok
190 } else if $headers.has_if_match() {
191 if $headers.pass_if_match($etag) {
192 tracing::debug!("ETag passed If-Match pre-condition");
193 Status::Ok
194 } else {
195 tracing::debug!("ETag failed If-Match pre-condition");
196 Status::PreconditionFailed
197 }
198 } else if $headers.pass_if_none_match($etag) {
199 tracing::debug!("ETag passed If-None-Match pre-condition");
200 Status::Ok
201 } else {
202 tracing::debug!("ETag failed If-None-Match pre-condition");
203 Status::PreconditionFailed
204 }
205 };
206}
207
208pub(crate) fn no_content(etag: &EntityTag) -> WithETag {
211 WithETag {
212 inner: Status::NoContent,
213 etag: Header::new(header::ETAG.as_str(), etag.to_string()),
214 }
215}