1use std::marker::PhantomData;
2
3use bytes::Bytes;
4use sofie_core::{Error, Result, Uuid};
5use sofie_db::{Context, Db, Reach, Writes};
6
7use crate::Area;
8use crate::session::RequestSession;
9use crate::urlencoded::Encoding;
10
11#[derive(Clone)]
12pub(crate) struct Matched {
13 pub(crate) parameters: Vec<(String, String)>,
14 pub(crate) query: Option<String>,
15 pub(crate) body: Option<Bytes>,
16 pub(crate) encoding: Encoding,
17 pub(crate) session: RequestSession,
18 pub(crate) reach: Reach,
20 pub(crate) deadline: crate::deadline::Deadline,
21 pub(crate) app: crate::AppName,
22 pub(crate) route: Option<&'static str>,
23 #[cfg(feature = "idempotency")]
24 pub(crate) work: Option<sofie_db::Work>,
25 #[cfg(feature = "signed-links")]
26 pub(crate) keys: std::sync::Arc<sofie_auth::SigningKeys>,
27 #[cfg(any(feature = "flags", feature = "permissions"))]
28 pub(crate) gates: crate::gate::RequestGates,
29 #[cfg(feature = "shield")]
30 pub(crate) shield: Option<crate::shield::RequestShield>,
31 #[cfg(feature = "storage")]
32 pub(crate) storage: Option<sofie_storage::Storage>,
33 #[cfg(feature = "listing")]
35 pub(crate) prefers_json: bool,
36}
37
38impl Matched {
40 fn bound(&self, name: &str) -> Result<Uuid> {
41 bound_id(&self.parameters, name)
42 }
43
44 fn input(&mut self) -> Result<serde_json::Value> {
45 let body = self.body.take().ok_or_else(body_already_read)?;
46 if self.encoding == Encoding::Form {
47 return crate::form::parse(&body).map(serde_json::Value::Object);
48 }
49 if body.is_empty() {
50 return Ok(serde_json::Value::Object(serde_json::Map::new()));
51 }
52 serde_json::from_slice(&body).map_err(|error| {
53 Error::BadRequest(format!("the request body is not valid JSON: {error}"))
54 })
55 }
56}
57
58#[cfg(feature = "engine")]
59impl Matched {
60 pub(crate) fn for_loader(&self) -> Self {
61 Self {
62 body: None,
63 encoding: Encoding::Json,
64 session: self.session.for_loader(),
65 #[cfg(feature = "idempotency")]
66 work: None,
67 ..self.clone()
68 }
69 }
70}
71
72fn bound_id(parameters: &[(String, String)], name: &str) -> Result<Uuid> {
75 let value = parameter(parameters, name)
76 .ok_or_else(|| Error::internal_message(format!("the route has no `{name}` parameter")))?;
77 value.parse().map_err(|_| Error::NotFound)
78}
79
80pub(crate) fn body_already_read() -> Error {
82 Error::internal_message("the request body was already read")
83}
84
85pub(crate) fn parameter<'p>(parameters: &'p [(String, String)], name: &str) -> Option<&'p str> {
86 let (_, value) = parameters.iter().find(|(given, _)| given == name)?;
87 Some(value)
88}
89
90pub enum Reading {}
92
93pub enum Writing {}
95
96pub type ReadContext<A> = RequestContext<A, Reading>;
97
98pub struct RequestContext<A: Area, M = Writing> {
99 db: Db,
100 pub(crate) matched: Matched,
101 kind: PhantomData<fn() -> (A, M)>,
102}
103
104impl<A: Area, M> RequestContext<A, M> {
105 pub(crate) fn new(db: Db, matched: Matched) -> Self {
106 Self {
107 db,
108 matched,
109 kind: PhantomData,
110 }
111 }
112
113 pub(crate) fn session(&self) -> &RequestSession {
114 &self.matched.session
115 }
116
117 pub(crate) fn query(&self) -> Option<&str> {
118 self.matched.query.as_deref()
119 }
120
121 #[must_use]
123 pub fn path_parameter(&self, name: &str) -> Option<&str> {
124 parameter(&self.matched.parameters, name)
125 }
126}
127
128impl<A: Area, M> Context for RequestContext<A, M> {
129 fn db(&self) -> &Db {
130 &self.db
131 }
132
133 fn reach(&self) -> Reach {
134 self.matched.reach
135 }
136
137 fn bound(&self, name: &str) -> Result<Uuid> {
138 self.matched.bound(name)
139 }
140
141 fn input(&mut self) -> Result<serde_json::Value> {
142 self.matched.input()
143 }
144}
145
146impl<A: Area> Writes for RequestContext<A> {}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn a_malformed_id_names_nothing_and_a_missing_parameter_is_a_bug() {
154 let parameters = vec![("id".to_owned(), "not-a-uuid".to_owned())];
155
156 assert!(matches!(bound_id(¶meters, "id"), Err(Error::NotFound)));
157 assert!(matches!(
158 bound_id(¶meters, "order"),
159 Err(Error::Internal { .. })
160 ));
161 }
162}