Skip to main content

sofie_http/
client_page.rs

1//! A client area's answers: the page shell holding the first screen's envelope, the envelope
2//! alone for the area's runtime, and a refused form's errors in the request's language.
3
4use http::StatusCode;
5use serde_json::{Map, Value, json};
6use sofie_core::{FieldError, Result};
7use sofie_db::Reply;
8use sofie_engine::{ClientBundle, ClientLinks, ClientShell};
9
10use crate::answer::{self, JSON, Refusal, Response};
11use crate::caller::Caller;
12use crate::language::Language;
13use crate::screen::{ScreenPage, new_nonce, with_policy};
14
15/// The loader's JSON is the screen's data as `application/json` answers it.
16fn envelope(screen: &ScreenPage<'_>, reply: Result<Reply>) -> Result<Value> {
17    let (mut data, typed) = match reply? {
18        Reply::Json(data) => (data, None),
19        typed => (typed.to_json()?, Some(typed)),
20    };
21    screen.withhold(&mut data);
22    let rendering = screen.rendering();
23    #[cfg(feature = "listing")]
24    rendering.translate_listing_errors(&mut data);
25    let instants = typed
26        .as_ref()
27        .and_then(Reply::shown)
28        .map_or(Value::Null, |shown| {
29            sofie_engine::instant_fields(shown, &data)
30        });
31    Ok(json!({
32        "screen": screen.template.name(),
33        "title": screen.title,
34        "language": screen.language.tag(),
35        "data": data,
36        "instants": instants,
37        "view": rendering.client_view(),
38        "navigation": screen.navigation_entries(),
39    }))
40}
41
42pub(crate) fn enveloped(screen: &ScreenPage<'_>, reply: Result<Reply>) -> Response {
43    let answered = envelope(screen, reply)
44        .map(|envelope| answer::response(StatusCode::OK, Some(JSON), envelope.to_string()));
45    answer::or_failure(answered, Caller::Screen)
46}
47
48pub(crate) fn shell(
49    screen: &ScreenPage<'_>,
50    bundle: &ClientBundle,
51    reply: Result<Reply>,
52) -> Result<Response> {
53    let envelope = envelope(screen, reply)?;
54    let nonce = new_nonce()?;
55    let links = links(bundle);
56    let html = ClientShell {
57        title: screen.title,
58        lang: screen.language.tag(),
59        nonce: &nonce,
60        envelope: &envelope,
61        links: &links,
62        #[cfg(feature = "islands")]
63        islands: screen.islands.as_ref(),
64        #[cfg(not(feature = "islands"))]
65        islands: None,
66    }
67    .render();
68    with_policy(html, screen, &nonce)
69}
70
71fn links(bundle: &ClientBundle) -> ClientLinks {
72    #[cfg(all(feature = "development", debug_assertions))]
73    if let Some(links) = crate::development::client_links(bundle.area) {
74        return links;
75    }
76    bundle.links()
77}
78
79/// Field messages and the notice are translated as a refused page would show them; each error
80/// keeps its key and arguments, as a 422 answers them.
81pub(crate) fn refused(refusal: &Refusal, language: Language<'_>) -> Response {
82    let errors: Vec<Value> = match refusal {
83        Refusal::Fields(fields) => fields
84            .iter()
85            .map(|error| field_error(error, language))
86            .collect(),
87        Refusal::Message(..) => Vec::new(),
88    };
89    let body = json!({ "errors": errors, "notice": refusal.messages(language) });
90    answer::response(refusal.status(), Some(JSON), body.to_string())
91}
92
93fn field_error(error: &FieldError, language: Language<'_>) -> Value {
94    let arguments: Map<String, Value> = error
95        .arguments
96        .iter()
97        .map(|(name, value)| (name.to_string(), Value::from(value.as_str())))
98        .collect();
99    json!({
100        "field": error.field,
101        "key": error.key,
102        "arguments": arguments,
103        "message": language.field_error(error),
104    })
105}
106
107impl crate::Screens {
108    /// The bundles the app's build script compiled, one per client area.
109    pub fn clients(&mut self, bundles: Vec<ClientBundle>) -> &mut Self {
110        self.clients = bundles;
111        self
112    }
113
114    pub(crate) fn client(&self, area: &str) -> Option<&ClientBundle> {
115        self.clients.iter().find(|bundle| bundle.area == area)
116    }
117}
118
119impl crate::assets::Assets {
120    pub(crate) fn with_clients(mut self, screens: &crate::Screens) -> Self {
121        for bundle in &screens.clients {
122            for asset in [bundle.script, bundle.stylesheet] {
123                self.served.push((asset.path(), asset));
124            }
125        }
126        self
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn a_refused_form_s_errors_keep_their_key_and_arguments() {
136        let error =
137            FieldError::new("code", "validation.max_length", "must be at most 4").with("max", "4");
138
139        let response = refused(&Refusal::Fields(vec![error]), Language::plain());
140
141        let body: Value =
142            serde_json::from_slice(&response.into_body().into_inner().unwrap()).unwrap();
143        assert_eq!(
144            body["errors"],
145            json!([{
146                "field": "code",
147                "key": "validation.max_length",
148                "arguments": { "max": "4" },
149                "message": "must be at most 4",
150            }])
151        );
152    }
153}