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 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;
29use xapi_data::DataError;
30
31#[derive(Responder)]
35#[response(status = 200, content_type = "json")]
36pub(crate) struct WithResource<T> {
37 inner: Json<T>,
38 etag: Header<'static>,
39 last_modified: Header<'static>,
40}
41
42#[derive(Responder)]
43#[response(status = 200, content_type = "json")]
44pub(crate) struct WithDocumentOrIDs {
45 inner: String,
46 etag: Header<'static>,
47 last_modified: Header<'static>,
48}
49
50#[derive(Responder)]
52pub(crate) struct WithETag {
53 inner: Status,
54 etag: Header<'static>,
55}
56
57pub(crate) fn etag_from_str(s: &str) -> EntityTag {
60 EntityTag::from_data(s.as_bytes())
61}
62
63pub(crate) fn compute_etag<T>(res: &T) -> Result<EntityTag, MyError>
68where
69 T: ?Sized + Serialize,
70{
71 let json = serde_json::to_string(res).map_err(|x| MyError::Data(DataError::JSON(x)))?;
73 Ok(etag_from_str(&json))
74}
75
76pub(crate) async fn do_emit_response<T: Serialize>(
83 c: Headers,
84 resource: T,
85 timestamp: Option<DateTime<Utc>>,
86) -> Result<WithResource<T>, MyError> {
87 let etag = compute_etag(&resource)?;
88 debug!("Etag = '{}'", etag);
89
90 let last_modified = if let Some(x) = timestamp {
91 x.to_rfc3339_opts(SecondsFormat::Millis, true)
92 } else {
93 get_consistent_thru()
94 .await
95 .to_rfc3339_opts(SecondsFormat::Millis, true)
96 };
97 debug!("Last-Modified = '{}'", last_modified);
98
99 let response = Ok(WithResource {
100 inner: Json(resource),
101 etag: Header::new(header::ETAG.as_str(), etag.to_string()),
102 last_modified: Header::new(header::LAST_MODIFIED.as_str(), last_modified),
103 });
104
105 if !c.has_conditionals() {
106 debug!("Request has no If-xxx headers");
107 return response;
108 }
109
110 if c.has_if_match() {
111 if c.pass_if_match(&etag) {
112 debug!("ETag passed If-Match pre-condition");
113 return response;
114 }
115
116 return Err(MyError::HTTP {
117 status: Status::PreconditionFailed,
118 info: "ETag failed If-Match pre-condition".into(),
119 });
120 }
121
122 if c.pass_if_none_match(&etag) {
123 debug!("ETag passed If-None-Match pre-condition");
124 return response;
125 }
126
127 Err(MyError::HTTP {
128 status: Status::NotModified,
129 info: "ETag failed If-None-Match pre-condition".into(),
130 })
131}
132
133#[macro_export]
141macro_rules! emit_response {
142 ( $headers:expr, $resource:expr => $T:ident, $timestamp:expr ) => {
143 $crate::lrs::resources::do_emit_response::<$T>($headers, $resource, Some($timestamp)).await
144 };
145
146 ( $headers:expr, $resource:expr => $T:ident ) => {
147 $crate::lrs::resources::do_emit_response::<$T>($headers, $resource, None).await
148 };
149}
150
151pub(crate) async fn emit_doc_response(
158 resource: String,
159 timestamp: Option<DateTime<Utc>>,
160) -> Result<WithDocumentOrIDs, MyError> {
161 let etag = etag_from_str(&resource);
162 debug!("etag = '{}'", etag);
163 let last_modified = if let Some(x) = timestamp {
164 x.to_rfc3339_opts(SecondsFormat::Millis, true)
165 } else {
166 get_consistent_thru()
167 .await
168 .to_rfc3339_opts(SecondsFormat::Millis, true)
169 };
170
171 Ok(WithDocumentOrIDs {
172 inner: resource,
173 etag: Header::new(header::ETAG.as_str(), etag.to_string()),
174 last_modified: Header::new(header::LAST_MODIFIED.as_str(), last_modified),
175 })
176}
177
178#[macro_export]
186macro_rules! eval_preconditions {
187 ( $etag: expr, $headers: expr ) => {
188 if !$headers.has_conditionals() {
189 tracing::debug!("Request has no If-xxx headers");
190 Status::Ok
191 } else if $headers.has_if_match() {
192 if $headers.pass_if_match($etag) {
193 tracing::debug!("ETag passed If-Match pre-condition");
194 Status::Ok
195 } else {
196 tracing::debug!("ETag failed If-Match pre-condition");
197 Status::PreconditionFailed
198 }
199 } else if $headers.pass_if_none_match($etag) {
200 tracing::debug!("ETag passed If-None-Match pre-condition");
201 Status::Ok
202 } else {
203 tracing::debug!("ETag failed If-None-Match pre-condition");
204 Status::PreconditionFailed
205 }
206 };
207}
208
209pub(crate) fn no_content(etag: &EntityTag) -> WithETag {
212 WithETag {
213 inner: Status::NoContent,
214 etag: Header::new(header::ETAG.as_str(), etag.to_string()),
215 }
216}