fiberplane_models/notebooks/
operations.rs

1use crate::data_sources::SelectedDataSource;
2use crate::formatting::Formatting;
3use crate::notebooks::{Cell, FrontMatter, Label};
4use crate::timestamps::TimeRange;
5#[cfg(feature = "fp-bindgen")]
6use fp_bindgen::prelude::*;
7use serde::{Deserialize, Serialize};
8use typed_builder::TypedBuilder;
9
10/// An operation is the representation for a mutation to be performed to a notebook.
11///
12/// Operations are intended to be atomic (they should either be performed in their entirety or not
13/// at all), while also capturing the intent of the user.
14///
15/// For more information, please see RFC 8:
16///   https://www.notion.so/fiberplane/RFC-8-Notebook-Operations-f9d18676d0d9437d81de30faa219deb4
17#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
18#[cfg_attr(
19    feature = "fp-bindgen",
20    derive(Serializable),
21    fp(rust_module = "fiberplane_models::notebooks::operations")
22)]
23#[non_exhaustive]
24#[serde(tag = "type", rename_all = "snake_case")]
25pub enum Operation {
26    MoveCells(MoveCellsOperation),
27    ReplaceCells(ReplaceCellsOperation),
28    ReplaceText(ReplaceTextOperation),
29    UpdateNotebookTimeRange(UpdateNotebookTimeRangeOperation),
30    UpdateNotebookTitle(UpdateNotebookTitleOperation),
31    SetSelectedDataSource(SetSelectedDataSourceOperation),
32    AddLabel(AddLabelOperation),
33    ReplaceLabel(ReplaceLabelOperation),
34    RemoveLabel(RemoveLabelOperation),
35    UpdateFrontMatter(UpdateFrontMatterOperation),
36    ClearFrontMatter(ClearFrontMatterOperation),
37}
38
39/// Moves one or more cells.
40#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
41#[cfg_attr(
42    feature = "fp-bindgen",
43    derive(Serializable),
44    fp(rust_module = "fiberplane_models::notebooks::operations")
45)]
46#[non_exhaustive]
47#[serde(rename_all = "camelCase")]
48pub struct MoveCellsOperation {
49    /// IDs of all the cells to be moved.
50    ///
51    /// These must be adjacent and given in the order they appear in the notebook.
52    pub cell_ids: Vec<String>,
53
54    /// Index the cells will be moved from. This is the index of the first cell before the move.
55    pub from_index: u32,
56
57    /// Index the cells will be moved to. This is the index of the first cell after the move.
58    pub to_index: u32,
59}
60
61/// Replaces one or more cells at once.
62///
63/// Note: This operation is relatively coarse and can be (ab)used to perform
64/// `ReplaceText` operations as well. In order to preserve intent as much as
65/// possible, please use `ReplaceText` where possible.
66///
67/// Note: This operation may not be used to move cells, other than the necessary
68/// corrections in cell indices that account for newly inserted and removed
69/// cells. Attempts to move cells to other indices will cause validation to
70/// fail.
71#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, TypedBuilder)]
72#[cfg_attr(
73    feature = "fp-bindgen",
74    derive(Serializable),
75    fp(rust_module = "fiberplane_models::notebooks::operations")
76)]
77#[non_exhaustive]
78#[serde(rename_all = "camelCase")]
79pub struct ReplaceCellsOperation {
80    /// Vector of the new cells, including their new indices.
81    ///
82    /// Indices of the new cells must be ordered incrementally to form a single,
83    /// cohesive range of cells.
84    ///
85    /// Note that "new" does not imply "newly inserted". If a cell with the same
86    /// ID is part of the `old_cells` field, it will merely be updated. Only
87    /// cells in the `new_cells` field that are not part of the `old_cells` will
88    /// be newly inserted.
89    #[builder(default)]
90    #[serde(default, skip_serializing_if = "Vec::is_empty")]
91    pub new_cells: Vec<CellWithIndex>,
92
93    /// Vector of the old cells, including their old indices.
94    ///
95    /// Indices of the old cells must be ordered incrementally to form a single,
96    /// cohesive range of cells.
97    ///
98    /// Note that "old" does not imply "removed". If a cell with the same
99    /// ID is part of the `new_cells` field, it will merely be updated. Only
100    /// cells in the `old_cells` field that are not part of the `new_cells` will
101    /// be removed.
102    #[builder(default)]
103    #[serde(default, skip_serializing_if = "Vec::is_empty")]
104    pub old_cells: Vec<CellWithIndex>,
105
106    /// Offset at which to split the first of the old cells.
107    ///
108    /// In this context, splitting means that the text of the cell in the
109    /// notebook is split in two at the split offset. The first part is kept,
110    /// while the second part (which must match the cell's text in `old_cells`)
111    /// is replaced with the text given in the first of the `new_cells`.
112    ///
113    /// If `None`, no cell is split.
114    #[builder(default)]
115    #[serde(default, skip_serializing_if = "Option::is_none")]
116    pub split_offset: Option<u32>,
117
118    /// Offset from which to merge the remainder of the last old cell.
119    ///
120    /// In this context, merging means that the text of the new cell is merged
121    /// from two parts. The first part comes the last of the `new_cells`, while
122    /// the second part is what remains of the cell in the notebook after the
123    /// merge offset.
124    ///
125    /// If `None`, no cells are merged.
126    #[builder(default)]
127    #[serde(default, skip_serializing_if = "Option::is_none")]
128    pub merge_offset: Option<u32>,
129
130    /// Optional cells which are updated as a result of the replacing of other
131    /// cells. This is intended to be used for cells that reference the
132    /// `new_cells` and which now need to be updated as a result of the
133    /// operation being applied to those cells.
134    ///
135    /// These referencing cells may also be newly inserted if they are not
136    /// included in the `old_referencing_cells`.
137    ///
138    /// Indices of new referencing cells do not need to form a cohesive range,
139    /// but they should still be ordered in ascending order.
140    #[builder(default)]
141    #[serde(default, skip_serializing_if = "Vec::is_empty")]
142    pub new_referencing_cells: Vec<CellWithIndex>,
143
144    /// Optional cells which are updated as a result of the replacing of other
145    /// cells. This is intended to be used for cells that reference the
146    /// `old_cells` and which now need to be updated as a result of the
147    /// operation being applied to those cells.
148    ///
149    /// These referencing cells may also be removed if they are not included in
150    /// the `new_referencing_cells`.
151    ///
152    /// Indices of old referencing cells do not need to form a cohesive range,
153    /// but they should still be ordered in ascending order.
154    #[builder(default)]
155    #[serde(default, skip_serializing_if = "Vec::is_empty")]
156    pub old_referencing_cells: Vec<CellWithIndex>,
157}
158
159impl ReplaceCellsOperation {
160    /// Returns all the new cell IDs, including the ones in the
161    /// `new_referencing_cells` field.
162    pub fn all_newly_inserted_cells(&self) -> impl Iterator<Item = &CellWithIndex> {
163        self.newly_inserted_cells()
164            .chain(self.newly_inserted_referencing_cells())
165    }
166
167    /// Returns all the old cell IDs, including the ones in the
168    /// `old_referencing_cells` field.
169    pub fn all_old_cells(&self) -> impl Iterator<Item = &CellWithIndex> {
170        self.old_cells
171            .iter()
172            .chain(self.old_referencing_cells.iter())
173    }
174
175    /// Returns all the old removed cell IDs, including the ones from the
176    /// `old_referencing_cells` field.
177    pub fn all_old_removed_cells(&self) -> impl Iterator<Item = &CellWithIndex> {
178        self.old_removed_cells()
179            .chain(self.old_removed_referencing_cells())
180    }
181
182    /// Returns all newly inserted cells, excluding referencing cells.
183    pub fn newly_inserted_cells(&self) -> impl Iterator<Item = &CellWithIndex> {
184        self.new_cells.iter().filter(move |new_cell| {
185            !self
186                .old_cells
187                .iter()
188                .any(|old_cell| old_cell.id() == new_cell.id())
189        })
190    }
191
192    /// Returns all newly inserted referencing cells.
193    pub fn newly_inserted_referencing_cells(&self) -> impl Iterator<Item = &CellWithIndex> {
194        self.new_referencing_cells.iter().filter(move |new_cell| {
195            !self
196                .old_referencing_cells
197                .iter()
198                .any(|old_cell| old_cell.id() == new_cell.id())
199        })
200    }
201
202    /// Returns all old cells that will be removed, excluding referencing cells.
203    pub fn old_removed_cells(&self) -> impl Iterator<Item = &CellWithIndex> {
204        self.old_cells.iter().filter(move |old_cell| {
205            !self
206                .new_cells
207                .iter()
208                .any(|new_cell| new_cell.id() == old_cell.id())
209        })
210    }
211
212    /// Returns all old referencing cells that will be removed.
213    pub fn old_removed_referencing_cells(&self) -> impl Iterator<Item = &CellWithIndex> {
214        self.old_referencing_cells.iter().filter(move |old_cell| {
215            !self
216                .new_referencing_cells
217                .iter()
218                .any(|new_cell| new_cell.id() == old_cell.id())
219        })
220    }
221}
222
223/// Replaces the part of the content in any content type cell or the title of a graph cell.
224#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
225#[cfg_attr(
226    feature = "fp-bindgen",
227    derive(Serializable),
228    fp(rust_module = "fiberplane_models::notebooks::operations")
229)]
230#[non_exhaustive]
231#[serde(rename_all = "camelCase")]
232pub struct ReplaceTextOperation {
233    /// ID of the cell whose text we're modifying.
234    #[builder(setter(into))]
235    pub cell_id: String,
236
237    /// Field to update the text of.
238    #[builder(default, setter(into))]
239    pub field: Option<String>,
240
241    /// Starting offset where we will be replacing the text.
242    ///
243    /// Please be aware this offset refers to the position of a Unicode Scalar Value (non-surrogate
244    /// codepoint) in the cell text, which may require additional effort to determine correctly.
245    pub offset: u32,
246
247    /// The new text value we're inserting.
248    #[builder(default, setter(into))]
249    pub new_text: String,
250
251    /// Optional formatting that we wish to apply to the new text.
252    ///
253    /// Offsets in the formatting are relative to the start of the new text.
254    #[builder(default)]
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub new_formatting: Option<Formatting>,
257
258    /// The old text that we're replacing.
259    #[builder(default, setter(into))]
260    pub old_text: String,
261
262    /// Optional formatting that was applied to the old text. This should be **all** the formatting
263    /// annotations that were *inside* the `old_text` before this operation was applied. However,
264    /// it is at the operation's discretion whether or not to include annotations that are at the
265    /// old text's boundaries.
266    ///
267    /// Offsets in the formatting are relative to the start of the old text.
268    #[builder(default)]
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub old_formatting: Option<Formatting>,
271}
272
273/// Updates the notebook time range.
274#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
275#[cfg_attr(
276    feature = "fp-bindgen",
277    derive(Serializable),
278    fp(rust_module = "fiberplane_models::notebooks::operations")
279)]
280#[non_exhaustive]
281#[serde(rename_all = "camelCase")]
282pub struct UpdateNotebookTimeRangeOperation {
283    pub old_time_range: TimeRange,
284    pub time_range: TimeRange,
285}
286
287/// Updates the notebook title.
288#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
289#[cfg_attr(
290    feature = "fp-bindgen",
291    derive(Serializable),
292    fp(rust_module = "fiberplane_models::notebooks::operations")
293)]
294#[non_exhaustive]
295#[serde(rename_all = "camelCase")]
296pub struct UpdateNotebookTitleOperation {
297    pub old_title: String,
298    pub title: String,
299}
300
301#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
302#[cfg_attr(
303    feature = "fp-bindgen",
304    derive(Serializable),
305    fp(rust_module = "fiberplane_models::notebooks::operations")
306)]
307#[non_exhaustive]
308#[serde(rename_all = "camelCase")]
309pub struct SetSelectedDataSourceOperation {
310    #[builder(setter(into))]
311    pub provider_type: String,
312    #[builder(default)]
313    #[serde(default, skip_serializing_if = "Option::is_none")]
314    pub old_selected_data_source: Option<SelectedDataSource>,
315    #[builder(default)]
316    #[serde(default, skip_serializing_if = "Option::is_none")]
317    pub new_selected_data_source: Option<SelectedDataSource>,
318}
319
320#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, TypedBuilder)]
321#[cfg_attr(
322    feature = "fp-bindgen",
323    derive(Serializable),
324    fp(rust_module = "fiberplane_models::notebooks::operations")
325)]
326#[non_exhaustive]
327#[serde(rename_all = "camelCase")]
328pub struct CellWithIndex {
329    pub cell: Cell,
330    pub index: u32,
331}
332
333impl CellWithIndex {
334    pub fn new(cell: Cell, index: u32) -> CellWithIndex {
335        CellWithIndex { cell, index }
336    }
337
338    pub fn formatting(&self) -> Option<&Formatting> {
339        self.cell.formatting()
340    }
341
342    pub fn id(&self) -> &str {
343        self.cell.id()
344    }
345
346    pub fn text(&self) -> Option<&str> {
347        self.cell.text()
348    }
349}
350
351/// Add an label to an notebook.
352#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
353#[cfg_attr(
354    feature = "fp-bindgen",
355    derive(Serializable),
356    fp(rust_module = "fiberplane_models::notebooks::operations")
357)]
358#[non_exhaustive]
359#[serde(rename_all = "camelCase")]
360pub struct AddLabelOperation {
361    /// The new label
362    pub label: Label,
363}
364
365/// Replace an label in an notebook.
366#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
367#[cfg_attr(
368    feature = "fp-bindgen",
369    derive(Serializable),
370    fp(rust_module = "fiberplane_models::notebooks::operations")
371)]
372#[non_exhaustive]
373#[serde(rename_all = "camelCase")]
374pub struct ReplaceLabelOperation {
375    // The previous label
376    pub old_label: Label,
377
378    // The new label
379    pub new_label: Label,
380}
381
382/// Remove an label in an notebook.
383#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
384#[cfg_attr(
385    feature = "fp-bindgen",
386    derive(Serializable),
387    fp(rust_module = "fiberplane_models::notebooks::operations")
388)]
389#[non_exhaustive]
390#[serde(rename_all = "camelCase")]
391pub struct RemoveLabelOperation {
392    pub label: Label,
393}
394
395/// Replaces front matter in a notebook
396#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
397#[cfg_attr(
398    feature = "fp-bindgen",
399    derive(Serializable),
400    fp(rust_module = "fiberplane_models::notebooks::operations")
401)]
402#[non_exhaustive]
403#[serde(rename_all = "camelCase")]
404pub struct UpdateFrontMatterOperation {
405    pub old_front_matter: FrontMatter,
406    pub new_front_matter: FrontMatter,
407}
408
409/// Removes front matter in a notebook
410#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
411#[cfg_attr(
412    feature = "fp-bindgen",
413    derive(Serializable),
414    fp(rust_module = "fiberplane_models::notebooks::operations")
415)]
416#[non_exhaustive]
417#[serde(rename_all = "camelCase")]
418pub struct ClearFrontMatterOperation {
419    pub front_matter: FrontMatter,
420}
421
422#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
423#[cfg_attr(
424    feature = "fp-bindgen",
425    derive(Serializable),
426    fp(rust_module = "fiberplane_models::notebooks::operations")
427)]
428#[non_exhaustive]
429#[serde(rename_all = "camelCase")]
430pub struct CellAppendText {
431    #[builder(setter(into))]
432    pub content: String,
433    #[builder(default)]
434    #[serde(default)]
435    pub formatting: Formatting,
436}
437
438#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, TypedBuilder)]
439#[cfg_attr(
440    feature = "fp-bindgen",
441    derive(Serializable),
442    fp(rust_module = "fiberplane_models::notebooks::operations")
443)]
444#[non_exhaustive]
445#[serde(rename_all = "camelCase")]
446pub struct CellReplaceText {
447    /// Starting offset where we will be replacing the text.
448    ///
449    /// Please be aware this offset refers to the position of a Unicode Scalar Value (non-surrogate
450    /// codepoint) in the cell text, which may require additional effort to determine correctly.
451    pub offset: u32,
452
453    /// The new text value we're inserting.
454    #[builder(setter(into))]
455    pub new_text: String,
456
457    /// Optional formatting that we wish to apply to the new text.
458    ///
459    /// Offsets in the formatting are relative to the start of the new text.
460    #[builder(default, setter(into))]
461    #[serde(skip_serializing_if = "Option::is_none")]
462    pub new_formatting: Option<Formatting>,
463
464    /// The old text that we're replacing.
465    #[builder(setter(into))]
466    pub old_text: String,
467
468    /// Optional formatting that was applied to the old text. This should be **all** the formatting
469    /// annotations that were *inside* the `old_text` before this operation was applied. However,
470    /// it is at the operation's discretion whether or not to include annotations that are at the
471    /// old text's boundaries.
472    ///
473    /// Offsets in the formatting are relative to the start of the old text.
474    #[builder(default, setter(into))]
475    #[serde(skip_serializing_if = "Option::is_none")]
476    pub old_formatting: Option<Formatting>,
477}