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