Skip to main content

fiberplane_models/notebooks/
front_matter.rs

1use crate::front_matter_schemas::SerializableEqFloat;
2pub use crate::labels::Label;
3use crate::timestamps::*;
4use base64uuid::Base64Uuid;
5#[cfg(feature = "fp-bindgen")]
6use fp_bindgen::prelude::Serializable;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use std::{collections::BTreeMap, str::FromStr};
10use strum_macros::Display;
11use thiserror::Error;
12use typed_builder::TypedBuilder;
13
14/// A JSON object which may or may not contain well known keys.
15/// More information in the [RFC](https://www.notion.so/fiberplane/RFC-58-Front-matter-Specialization-Front-matter-a9b3b51614ee48a19ec416c02a9fd647)
16///
17/// The values stored in the FrontMatter can follow a schema to contain type information and be validated at
18/// runtime by the corresponding [`FrontMatterSchemaEntry`](crate::front_matter_schemas::FrontMatterSchemaEntry).
19///
20/// See [`FrontMatterValue`] for more details on validation.
21pub type FrontMatter = BTreeMap<String, FrontMatterValue>;
22
23/// Known variants of front-matter values for runtime validation.
24///
25/// Front matter values can hold extra type information to allow the API and
26/// the operational transform operations to validate values before storing them.
27///
28/// The usual pattern to use these values is to use the `validate_value` method
29/// of `FrontMatterSchemaEntry` to check if the value has the expected type:
30///
31/// ```rust
32/// # use fiberplane_models::front_matter_schemas::{FrontMatterSchemaEntry, FrontMatterNumberSchema};
33/// # use serde_json::json;
34/// // An existing schema to check values against
35/// let schema = FrontMatterSchemaEntry::builder()
36///     .key("foo")
37///     .schema(FrontMatterNumberSchema::builder()
38///         .display_name("A number field that accepts single numbers")
39///         .build())
40///     .build();
41///     
42/// // A value that came from an API boundary
43/// let good_value_from_api = json!(42);
44/// assert!(schema.validate_value(good_value_from_api.clone()).is_ok());
45///
46/// // Another value (that has the wrong type)
47/// let bad_value_from_api = json!("2022-10-08T13:29:00.78Z");
48/// assert!(schema.validate_value(bad_value_from_api).is_err());
49/// ```
50///
51/// This can be used to validate the obtained value format.
52#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, Display)]
53#[cfg_attr(
54    feature = "fp-bindgen",
55    derive(Serializable),
56    fp(rust_module = "fiberplane_models::notebooks::front_matter")
57)]
58#[non_exhaustive]
59#[serde(untagged)]
60pub enum FrontMatterValue {
61    /// A timestamp front matter value
62    DateTime(FrontMatterDateTimeValue),
63    /// A list-of-timestamps front matter value
64    DateTimeList(FrontMatterDateTimeList),
65
66    /// A user front matter value
67    User(FrontMatterUserValue),
68    /// A list-of-users front matter value
69    UserList(FrontMatterUserList),
70
71    /// A string front matter value
72    String(FrontMatterStringValue),
73    /// A list-of-strings front matter value
74    StringList(FrontMatterStringList),
75
76    /// A number front matter value
77    Number(FrontMatterNumberValue),
78    /// A list-of-numbers front matter value
79    NumberList(FrontMatterNumberList),
80}
81
82/// Error from validating a JSON object as a correct front matter value
83#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, Error)]
84#[cfg_attr(
85    feature = "fp-bindgen",
86    derive(Serializable),
87    fp(rust_module = "fiberplane_models::notebooks::front_matter")
88)]
89#[non_exhaustive]
90#[serde(rename_all = "snake_case", tag = "type")]
91pub enum FrontMatterValidationError {
92    /// Impossible to deserialize the data
93    #[error("unexpected format: {message}")]
94    Format { message: String },
95
96    /// Obtained the wrong variant
97    #[error("unexpected variant: expected {expected} but got {got}")]
98    Variant { got: String, expected: String },
99}
100
101impl FrontMatterValidationError {
102    pub(crate) fn wrong_variant(got: &str, expected: &str) -> Self {
103        Self::Variant {
104            got: got.to_string(),
105            expected: expected.to_string(),
106        }
107    }
108}
109
110impl From<serde_json::Value> for FrontMatterValue {
111    fn from(value: serde_json::Value) -> Self {
112        serde_json::from_value(value)
113            .expect("with the untagged variant, all serde_json::Value are deserializable.")
114    }
115}
116
117impl TryFrom<&str> for FrontMatterValue {
118    type Error = FrontMatterValidationError;
119
120    fn try_from(value: &str) -> Result<Self, Self::Error> {
121        serde_json::from_str(value).map_err(|err| FrontMatterValidationError::Format {
122            message: err.to_string(),
123        })
124    }
125}
126
127impl FrontMatterValue {
128    /// Get a string description of the type of the value
129    pub fn get_type(&self) -> &'static str {
130        match self {
131            FrontMatterValue::Number(_) => "number",
132            FrontMatterValue::NumberList(_) => "number_list",
133            FrontMatterValue::String(_) => "string",
134            FrontMatterValue::StringList(_) => "string_list",
135            FrontMatterValue::DateTime(_) => "date_time",
136            FrontMatterValue::DateTimeList(_) => "date_time_list",
137            FrontMatterValue::User(_) => "user",
138            FrontMatterValue::UserList(_) => "user_list",
139        }
140    }
141}
142
143impl TryFrom<serde_json::Value> for FrontMatterNumberValue {
144    type Error = FrontMatterValidationError;
145
146    fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
147        if let Value::Number(num) = value {
148            return Ok(Self(
149                num.as_f64()
150                    .ok_or_else(|| FrontMatterValidationError::Format {
151                        message: "invalid number".to_string(),
152                    })?
153                    .into(),
154            ));
155        }
156
157        Err(FrontMatterValidationError::wrong_variant(
158            "untyped", "number",
159        ))
160    }
161}
162
163impl From<f64> for FrontMatterNumberValue {
164    fn from(value: f64) -> Self {
165        Self(value.into())
166    }
167}
168
169impl TryFrom<serde_json::Value> for FrontMatterNumberList {
170    type Error = FrontMatterValidationError;
171
172    fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
173        if let Value::Array(array) = value {
174            return Ok(Self(
175                array
176                    .into_iter()
177                    .map(FrontMatterNumberValue::try_from)
178                    .collect::<Result<Vec<_>, _>>()?,
179            ));
180        }
181
182        Err(FrontMatterValidationError::wrong_variant(
183            "'not a list'",
184            "number_list",
185        ))
186    }
187}
188
189impl TryFrom<serde_json::Value> for FrontMatterStringValue {
190    type Error = FrontMatterValidationError;
191
192    fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
193        if let Value::String(strr) = value {
194            return Ok(Self(strr));
195        }
196
197        Err(FrontMatterValidationError::wrong_variant(
198            "untyped", "string",
199        ))
200    }
201}
202
203impl From<&str> for FrontMatterStringValue {
204    fn from(value: &str) -> Self {
205        Self(value.to_string())
206    }
207}
208
209impl From<String> for FrontMatterStringValue {
210    fn from(value: String) -> Self {
211        Self(value)
212    }
213}
214
215impl TryFrom<serde_json::Value> for FrontMatterStringList {
216    type Error = FrontMatterValidationError;
217
218    fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
219        if let Value::Array(array) = value {
220            return Ok(Self(
221                array
222                    .into_iter()
223                    .map(FrontMatterStringValue::try_from)
224                    .collect::<Result<Vec<_>, _>>()?,
225            ));
226        }
227
228        Err(FrontMatterValidationError::wrong_variant(
229            "'not a list'",
230            "string_list",
231        ))
232    }
233}
234
235impl TryFrom<serde_json::Value> for FrontMatterDateTimeValue {
236    type Error = FrontMatterValidationError;
237
238    fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
239        if let Value::String(strr) = value {
240            return Ok(Self(strr.parse().map_err(|err| {
241                FrontMatterValidationError::Format {
242                    message: format!("invalid timestamp: {err}"),
243                }
244            })?));
245        }
246
247        Err(FrontMatterValidationError::wrong_variant(
248            "untyped",
249            "date_time",
250        ))
251    }
252}
253
254impl TryFrom<&str> for FrontMatterDateTimeValue {
255    type Error = FrontMatterValidationError;
256
257    fn try_from(value: &str) -> Result<Self, Self::Error> {
258        let value = Timestamp::parse(value).map_err(|err| FrontMatterValidationError::Format {
259            message: err.to_string(),
260        })?;
261        Ok(Self(value))
262    }
263}
264
265impl FromStr for FrontMatterDateTimeValue {
266    type Err = FrontMatterValidationError;
267
268    fn from_str(s: &str) -> Result<Self, Self::Err> {
269        Self::try_from(s)
270    }
271}
272
273impl TryFrom<serde_json::Value> for FrontMatterDateTimeList {
274    type Error = FrontMatterValidationError;
275
276    fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
277        if let Value::Array(array) = value {
278            return Ok(Self(
279                array
280                    .into_iter()
281                    .map(FrontMatterDateTimeValue::try_from)
282                    .collect::<Result<Vec<_>, _>>()?,
283            ));
284        }
285
286        Err(FrontMatterValidationError::wrong_variant(
287            "'not a list'",
288            "date_time_list",
289        ))
290    }
291}
292
293impl TryFrom<serde_json::Value> for FrontMatterUserValue {
294    type Error = FrontMatterValidationError;
295
296    fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
297        serde_json::from_value(value).map_err(|err| FrontMatterValidationError::Format {
298            message: format!("invalid user: {err}"),
299        })
300    }
301}
302
303impl TryFrom<&str> for FrontMatterUserValue {
304    type Error = FrontMatterValidationError;
305
306    fn try_from(value: &str) -> Result<Self, Self::Error> {
307        let value =
308            Base64Uuid::parse_str(value).map_err(|err| FrontMatterValidationError::Format {
309                message: err.to_string(),
310            })?;
311        Ok(Self::from(value))
312    }
313}
314
315impl FromStr for FrontMatterUserValue {
316    type Err = FrontMatterValidationError;
317
318    fn from_str(s: &str) -> Result<Self, Self::Err> {
319        Self::try_from(s)
320    }
321}
322
323impl TryFrom<serde_json::Value> for FrontMatterUserList {
324    type Error = FrontMatterValidationError;
325
326    fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
327        if let Value::Array(array) = value {
328            return Ok(Self(
329                array
330                    .into_iter()
331                    .map(FrontMatterUserValue::try_from)
332                    .collect::<Result<Vec<_>, _>>()?,
333            ));
334        }
335
336        Err(FrontMatterValidationError::wrong_variant(
337            "'not a list'",
338            "user_list",
339        ))
340    }
341}
342
343impl From<FrontMatterUserValue> for FrontMatterValue {
344    fn from(v: FrontMatterUserValue) -> Self {
345        Self::User(v)
346    }
347}
348
349impl From<FrontMatterDateTimeValue> for FrontMatterValue {
350    fn from(v: FrontMatterDateTimeValue) -> Self {
351        Self::DateTime(v)
352    }
353}
354
355impl From<FrontMatterStringValue> for FrontMatterValue {
356    fn from(v: FrontMatterStringValue) -> Self {
357        Self::String(v)
358    }
359}
360
361impl From<FrontMatterNumberValue> for FrontMatterValue {
362    fn from(v: FrontMatterNumberValue) -> Self {
363        Self::Number(v)
364    }
365}
366
367impl From<FrontMatterUserList> for FrontMatterValue {
368    fn from(v: FrontMatterUserList) -> Self {
369        Self::UserList(v)
370    }
371}
372
373impl From<FrontMatterDateTimeList> for FrontMatterValue {
374    fn from(v: FrontMatterDateTimeList) -> Self {
375        Self::DateTimeList(v)
376    }
377}
378
379impl From<FrontMatterStringList> for FrontMatterValue {
380    fn from(v: FrontMatterStringList) -> Self {
381        Self::StringList(v)
382    }
383}
384
385impl From<FrontMatterNumberList> for FrontMatterValue {
386    fn from(v: FrontMatterNumberList) -> Self {
387        Self::NumberList(v)
388    }
389}
390
391#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
392#[cfg_attr(
393    feature = "fp-bindgen",
394    derive(Serializable),
395    fp(rust_module = "fiberplane_models::notebooks::front_matter")
396)]
397#[non_exhaustive]
398#[repr(transparent)]
399pub struct FrontMatterNumberValue(pub SerializableEqFloat);
400
401impl std::ops::DerefMut for FrontMatterNumberValue {
402    fn deref_mut(&mut self) -> &mut Self::Target {
403        &mut self.0
404    }
405}
406
407impl std::ops::Deref for FrontMatterNumberValue {
408    type Target = SerializableEqFloat;
409
410    fn deref(&self) -> &Self::Target {
411        &self.0
412    }
413}
414
415#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
416#[cfg_attr(
417    feature = "fp-bindgen",
418    derive(Serializable),
419    fp(rust_module = "fiberplane_models::notebooks::front_matter")
420)]
421#[non_exhaustive]
422#[repr(transparent)]
423pub struct FrontMatterNumberList(pub Vec<FrontMatterNumberValue>);
424
425impl std::ops::DerefMut for FrontMatterNumberList {
426    fn deref_mut(&mut self) -> &mut Self::Target {
427        &mut self.0
428    }
429}
430
431impl std::ops::Deref for FrontMatterNumberList {
432    type Target = Vec<FrontMatterNumberValue>;
433
434    fn deref(&self) -> &Self::Target {
435        &self.0
436    }
437}
438
439#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
440#[cfg_attr(
441    feature = "fp-bindgen",
442    derive(Serializable),
443    fp(rust_module = "fiberplane_models::notebooks::front_matter")
444)]
445#[non_exhaustive]
446#[repr(transparent)]
447pub struct FrontMatterStringValue(pub String);
448
449impl std::ops::DerefMut for FrontMatterStringValue {
450    fn deref_mut(&mut self) -> &mut Self::Target {
451        &mut self.0
452    }
453}
454
455impl std::ops::Deref for FrontMatterStringValue {
456    type Target = String;
457
458    fn deref(&self) -> &Self::Target {
459        &self.0
460    }
461}
462
463#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
464#[cfg_attr(
465    feature = "fp-bindgen",
466    derive(Serializable),
467    fp(rust_module = "fiberplane_models::notebooks::front_matter")
468)]
469#[non_exhaustive]
470#[repr(transparent)]
471pub struct FrontMatterStringList(pub Vec<FrontMatterStringValue>);
472
473impl std::ops::DerefMut for FrontMatterStringList {
474    fn deref_mut(&mut self) -> &mut Self::Target {
475        &mut self.0
476    }
477}
478
479impl std::ops::Deref for FrontMatterStringList {
480    type Target = Vec<FrontMatterStringValue>;
481
482    fn deref(&self) -> &Self::Target {
483        &self.0
484    }
485}
486
487#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
488#[cfg_attr(
489    feature = "fp-bindgen",
490    derive(Serializable),
491    fp(rust_module = "fiberplane_models::notebooks::front_matter")
492)]
493#[non_exhaustive]
494#[repr(transparent)]
495pub struct FrontMatterDateTimeValue(pub Timestamp);
496
497impl std::ops::DerefMut for FrontMatterDateTimeValue {
498    fn deref_mut(&mut self) -> &mut Self::Target {
499        &mut self.0
500    }
501}
502
503impl std::ops::Deref for FrontMatterDateTimeValue {
504    type Target = Timestamp;
505
506    fn deref(&self) -> &Self::Target {
507        &self.0
508    }
509}
510
511impl<T: Into<Timestamp>> From<T> for FrontMatterDateTimeValue {
512    fn from(value: T) -> Self {
513        Self(value.into())
514    }
515}
516
517#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
518#[cfg_attr(
519    feature = "fp-bindgen",
520    derive(Serializable),
521    fp(rust_module = "fiberplane_models::notebooks::front_matter")
522)]
523#[non_exhaustive]
524#[repr(transparent)]
525pub struct FrontMatterDateTimeList(pub Vec<FrontMatterDateTimeValue>);
526
527impl std::ops::DerefMut for FrontMatterDateTimeList {
528    fn deref_mut(&mut self) -> &mut Self::Target {
529        &mut self.0
530    }
531}
532
533impl std::ops::Deref for FrontMatterDateTimeList {
534    type Target = Vec<FrontMatterDateTimeValue>;
535
536    fn deref(&self) -> &Self::Target {
537        &self.0
538    }
539}
540
541#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
542#[cfg_attr(
543    feature = "fp-bindgen",
544    derive(Serializable),
545    fp(rust_module = "fiberplane_models::notebooks::front_matter")
546)]
547#[non_exhaustive]
548#[serde(rename_all = "snake_case")]
549pub struct FrontMatterUserValue {
550    #[builder(setter(into))]
551    pub id: Base64Uuid,
552    #[builder(setter(into))]
553    pub name: String,
554}
555
556impl std::ops::DerefMut for FrontMatterUserValue {
557    fn deref_mut(&mut self) -> &mut Self::Target {
558        &mut self.id
559    }
560}
561
562impl std::ops::Deref for FrontMatterUserValue {
563    type Target = Base64Uuid;
564
565    fn deref(&self) -> &Self::Target {
566        &self.id
567    }
568}
569
570impl From<Base64Uuid> for FrontMatterUserValue {
571    fn from(value: Base64Uuid) -> Self {
572        Self {
573            id: value,
574            name: String::new(),
575        }
576    }
577}
578
579#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
580#[cfg_attr(
581    feature = "fp-bindgen",
582    derive(Serializable),
583    fp(rust_module = "fiberplane_models::notebooks::front_matter")
584)]
585#[non_exhaustive]
586#[repr(transparent)]
587pub struct FrontMatterUserList(pub Vec<FrontMatterUserValue>);
588
589impl std::ops::DerefMut for FrontMatterUserList {
590    fn deref_mut(&mut self) -> &mut Self::Target {
591        &mut self.0
592    }
593}
594
595impl std::ops::Deref for FrontMatterUserList {
596    type Target = Vec<FrontMatterUserValue>;
597
598    fn deref(&self) -> &Self::Target {
599        &self.0
600    }
601}