sofie_http/gate/optional/resource/
handlers.rs1use serde_json::{Map, Value};
2use sofie_core::{Error, Result};
3use sofie_db::{
4 Aggregate, Context, Create, Id, Input, Outcome, Plan, Read, ReadOnly, Reply, ReplyData,
5 Resolve, Stamp, delete_softly,
6};
7use std::marker::PhantomData;
8
9use sofie_values::{Control, FormField, FormFields};
10
11use super::{Look, ShownField};
12use crate::gate::optional::listing::Listing;
13use crate::{Area, RequestContext};
14
15pub(super) struct Shown(Map<String, Value>);
17
18impl Outcome for Shown {
19 fn reply(self) -> Result<Reply> {
20 Ok(Reply::Json(Value::Object(self.0)))
21 }
22}
23
24impl ReadOnly for Shown {}
25
26pub(crate) struct Record<R>(R);
28
29impl<A: Area, M, R: Aggregate> Resolve<RequestContext<A, M>> for Record<R> {
30 async fn resolve(context: &mut RequestContext<A, M>) -> Result<Self> {
31 sofie_db::bind(context).await.map(Self)
32 }
33}
34
35pub(crate) struct Parameters(Map<String, Value>);
36
37impl<A: Area, M> Resolve<RequestContext<A, M>> for Parameters {
38 fn resolve(context: &mut RequestContext<A, M>) -> impl Future<Output = Result<Self>> + Send {
39 let parameters = context
40 .matched
41 .parameters
42 .iter()
43 .map(|(name, value)| (name.clone(), Value::String(value.clone())))
44 .collect();
45 std::future::ready(Ok(Self(parameters)))
46 }
47}
48
49pub(super) struct Posted<I>(I);
51
52impl<A: Area, I: Input + FormFields + Send> Resolve<RequestContext<A>> for Posted<I> {
53 fn resolve(context: &mut RequestContext<A>) -> impl Future<Output = Result<Self>> + Send {
54 let posted = context.input().and_then(|mut input| {
55 bind_parents(&mut input, &context.matched.parameters, I::FIELDS);
56 I::from_json(input).map(Self)
57 });
58 std::future::ready(posted)
59 }
60}
61
62pub(super) struct Edited<I>(Value, PhantomData<fn() -> I>);
64
65impl<A: Area, I: FormFields> Resolve<RequestContext<A>> for Edited<I> {
66 fn resolve(context: &mut RequestContext<A>) -> impl Future<Output = Result<Self>> + Send {
67 let edited = context.input().map(|mut input| {
68 bind_parents(&mut input, &context.matched.parameters, I::FIELDS);
69 Self(input, PhantomData)
70 });
71 std::future::ready(edited)
72 }
73}
74
75fn bind_parents(input: &mut Value, parameters: &[(String, String)], fields: &[FormField]) {
76 let Value::Object(posted) = input else {
77 return;
78 };
79 for (name, value) in parameters {
80 if posted.contains_key(name) || fields.iter().any(|field| field.name == name) {
81 posted.insert(name.clone(), Value::String(value.clone()));
82 }
83 }
84}
85
86pub trait OwnerOf<A: Area>: Sized + Send {
87 fn of(context: &RequestContext<A>) -> Result<Self>;
89}
90
91impl<A: Area> OwnerOf<A> for () {
92 fn of(_context: &RequestContext<A>) -> Result<Self> {
93 Ok(())
94 }
95}
96
97impl<A: Area<Principal = T>, T: 'static> OwnerOf<A> for Id<T> {
98 fn of(context: &RequestContext<A>) -> Result<Self> {
99 context
100 .session()
101 .principal_in(A::NAME)
102 .map(Id::from_uuid)
103 .ok_or(Error::NotFound)
104 }
105}
106
107pub(super) struct Owner<O>(O);
108
109impl<A: Area, O: OwnerOf<A>> Resolve<RequestContext<A>> for Owner<O> {
110 fn resolve(context: &mut RequestContext<A>) -> impl Future<Output = Result<Self>> + Send {
111 std::future::ready(O::of(context).map(Self))
112 }
113}
114
115pub(super) struct Saved<R> {
117 record: R,
118 unique: &'static [(&'static str, &'static str)],
119}
120
121const TAKEN: &str = "is already taken";
122
123impl<R: Aggregate> Outcome for Saved<R> {
124 fn plan(&self, plan: &mut Plan<'_>) -> Result<()> {
125 for (index, field) in self.unique {
126 plan.unique(index, field, TAKEN);
127 }
128 self.record.plan(plan)
129 }
130
131 fn committed(&mut self, stamp: Stamp) {
132 self.record.committed(stamp);
133 }
134
135 fn reply(self) -> Result<Reply> {
136 self.record.reply()
137 }
138}
139
140pub(super) async fn list<R: Aggregate + ReplyData>(
141 read: Read,
142 listing: Listing<R>,
143 parameters: Parameters,
144) -> Result<Shown> {
145 let mut shown = parameters.0;
146 shown.insert("list".to_owned(), listing.page(&read).await?.to_json()?);
147 Ok(Shown(shown))
148}
149
150pub(super) async fn show<R: Aggregate + ReplyData>(
151 Record(record): Record<R>,
152 read: Read,
153 parameters: Parameters,
154 fields: &'static [ShownField],
155) -> Result<Shown> {
156 let mut shown = parameters.0;
157 let value = record
158 .to_json()
159 .map_err(|error| Error::internal(format!("serializing a {}", R::NAME), error))?;
160 let display = display(&value, fields);
161 if !display.is_empty() {
162 shown.insert("display".to_owned(), Value::Object(display));
163 }
164 shown.insert("record".to_owned(), value);
165 #[cfg(feature = "history")]
166 shown.insert(
167 "history".to_owned(),
168 super::history::changes(&read, &record).await?,
169 );
170 let _ = read;
171 Ok(Shown(shown))
172}
173
174fn display(record: &Value, fields: &[ShownField]) -> Map<String, Value> {
177 fields
178 .iter()
179 .filter_map(|field| {
180 let value = record.get(field.name)?;
181 let text = match field.look {
182 Look::Percentage => {
183 format!("{} %", sofie_values::percent_text(value.as_i64()?))
184 }
185 Look::Joined => joined_text(value),
186 _ => return None,
187 };
188 Some((field.name.to_owned(), Value::String(text)))
189 })
190 .collect()
191}
192
193fn joined_text(value: &Value) -> String {
194 match value {
195 Value::Null => String::new(),
196 Value::String(text) => text.clone(),
197 Value::Array(values) => values.iter().map(joined_text).collect::<Vec<_>>().join(" "),
198 Value::Object(parts) => parts
199 .values()
200 .map(joined_text)
201 .collect::<Vec<_>>()
202 .join(" "),
203 Value::Bool(_) | Value::Number(_) => value.to_string(),
204 }
205}
206
207pub(super) async fn blank(parameters: Parameters) -> Result<Shown> {
208 Ok(Shown(parameters.0))
209}
210
211pub(super) async fn create<R: Aggregate + Create>(
212 posted: Posted<R::Input>,
213 owner: Owner<R::Owner>,
214 unique: &'static [(&'static str, &'static str)],
215) -> Result<Saved<R>> {
216 let record = R::create(posted.0, owner.0)?;
217 Ok(Saved { record, unique })
218}
219
220pub(super) async fn update<R: Aggregate + ReplyData + sofie_db::Update>(
221 mut record: R,
222 edited: Edited<R::Input>,
223 unique: &'static [(&'static str, &'static str)],
224) -> Result<Saved<R>>
225where
226 R::Input: FormFields,
227{
228 let Edited(mut posted, _) = edited;
229 let mut stored = record
230 .to_json()
231 .map_err(|error| Error::internal(format!("serializing a {}", R::NAME), error))?;
232 if let Value::Object(fields) = &mut stored {
233 fields.extend(record.sensitive_json());
234 }
235 keep_blank_secrets(&mut posted, &stored, R::Input::FIELDS);
236 record.update(R::Input::from_json(posted)?)?;
237 Ok(Saved { record, unique })
238}
239
240fn keep_blank_secrets(posted: &mut Value, stored: &Value, fields: &[FormField]) {
243 let (Value::Object(posted), Value::Object(stored)) = (posted, stored) else {
244 return;
245 };
246 for field in fields.iter().filter(|field| is_secret(field)) {
247 let blank = posted
248 .get(field.name)
249 .is_none_or(|value| value.is_null() || value.as_str() == Some(""));
250 if let (true, Some(kept)) = (blank, stored.get(field.name)) {
251 posted.insert(field.name.to_owned(), kept.clone());
252 }
253 }
254}
255
256pub(super) fn is_secret(field: &FormField) -> bool {
257 field.input.control == Control::Password
258}
259
260pub(super) async fn delete<R: Aggregate>(mut record: R) -> Result<R> {
261 delete_softly(&mut record)?;
262 Ok(record)
263}
264
265#[cfg(test)]
266mod tests {
267 use serde_json::json;
268 use sofie_values::Control;
269
270 use super::*;
271
272 #[test]
273 fn a_post_is_bound_to_the_parents_its_path_names_even_when_it_leaves_them_out() {
274 const ROLE: FormField = FormField {
275 name: "role",
276 input: sofie_values::Input::new(Control::Input),
277 required: false,
278 constraints: &[],
279 };
280 let parameters = [
281 ("role".to_owned(), "path-role".to_owned()),
282 ("role_grant".to_owned(), "grant".to_owned()),
283 ];
284 for posted in [
285 json!({"permission": "a"}),
286 json!({"role": "other", "permission": "a"}),
287 ] {
288 let mut posted = posted;
289
290 bind_parents(&mut posted, ¶meters, &[ROLE]);
291
292 assert_eq!(posted, json!({"role": "path-role", "permission": "a"}));
293 }
294 }
295}