Skip to main content

sofie_http/gate/optional/resource/
handlers.rs

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