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 let value = parameter(&self.parameters, name).ok_or(Error::NotFound)?;
42 value.parse().map_err(|_| Error::NotFound)
43 }
44
45 fn input(&mut self) -> Result<serde_json::Value> {
46 let body = self
47 .body
48 .take()
49 .ok_or_else(|| Error::BadRequest("the request body was already read".to_owned()))?;
50 if self.encoding == Encoding::Form {
51 return crate::form::parse(&body).map(serde_json::Value::Object);
52 }
53 if body.is_empty() {
54 return Ok(serde_json::Value::Object(serde_json::Map::new()));
55 }
56 serde_json::from_slice(&body).map_err(|error| {
57 Error::BadRequest(format!("the request body is not valid JSON: {error}"))
58 })
59 }
60}
61
62#[cfg(feature = "engine")]
63impl Matched {
64 pub(crate) fn for_loader(&self) -> Self {
65 Self {
66 body: None,
67 encoding: Encoding::Json,
68 session: self.session.for_loader(),
69 #[cfg(feature = "idempotency")]
70 work: None,
71 ..self.clone()
72 }
73 }
74}
75
76pub(crate) fn parameter<'p>(parameters: &'p [(String, String)], name: &str) -> Option<&'p str> {
77 let (_, value) = parameters.iter().find(|(given, _)| given == name)?;
78 Some(value)
79}
80
81pub enum Reading {}
83
84pub enum Writing {}
86
87pub type ReadContext<A> = RequestContext<A, Reading>;
88
89pub struct RequestContext<A: Area, M = Writing> {
90 db: Db,
91 pub(crate) matched: Matched,
92 kind: PhantomData<fn() -> (A, M)>,
93}
94
95impl<A: Area, M> RequestContext<A, M> {
96 pub(crate) fn new(db: Db, matched: Matched) -> Self {
97 Self {
98 db,
99 matched,
100 kind: PhantomData,
101 }
102 }
103
104 pub(crate) fn session(&self) -> &RequestSession {
105 &self.matched.session
106 }
107
108 pub(crate) fn query(&self) -> Option<&str> {
109 self.matched.query.as_deref()
110 }
111
112 #[must_use]
114 pub fn path_parameter(&self, name: &str) -> Option<&str> {
115 parameter(&self.matched.parameters, name)
116 }
117}
118
119impl<A: Area, M> Context for RequestContext<A, M> {
120 fn db(&self) -> &Db {
121 &self.db
122 }
123
124 fn reach(&self) -> Reach {
125 self.matched.reach
126 }
127
128 fn bound(&self, name: &str) -> Result<Uuid> {
129 self.matched.bound(name)
130 }
131
132 fn input(&mut self) -> Result<serde_json::Value> {
133 self.matched.input()
134 }
135}
136
137impl<A: Area> Writes for RequestContext<A> {}