Skip to main content

sofie_http/
context.rs

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    /// Every `Read` and commit is held to it.
19    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    /// A list's refused filter is then a 422; a page shows it beside the filter.
34    #[cfg(feature = "listing")]
35    pub(crate) prefers_json: bool,
36}
37
38// Kept off the per-area `RequestContext`, so each app compiles the body parse once.
39impl 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
81/// Answers a GET, a screen or an island: a handler resolved in it never writes.
82pub enum Reading {}
83
84/// Answers every other method: the unit of work commits its handler's outcome.
85pub 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    /// A decoded path segment that is not a record's id, such as a page's slug.
113    #[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> {}