Skip to main content

perspective_migrate/
validate.rs

1// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
3// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
4// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
5// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
6// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7// ┃ Copyright (c) 2017, the Perspective Authors.                              ┃
8// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9// ┃ This file is part of the Perspective library, distributed under the terms ┃
10// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
13use std::collections::BTreeMap;
14use std::fmt;
15
16use perspective_client::config::viewer::{
17    DerivedTableConfig, Layout, OptionalUpdate, PanelViewerConfig, TableRef, ViewerConfig,
18    ViewerConfigInitial, ViewerConfigUpdate, WorkspaceConfig, WorkspaceConfigUpdate,
19    validate_palette,
20};
21use perspective_client::config::{ViewConfig, ViewConfigUpdate};
22use serde::de::DeserializeOwned;
23use serde::{Deserialize, Serialize};
24use serde_json::Value;
25
26use crate::Kind;
27use crate::json::escape;
28
29/// Which shape of a config `validate` requires.
30#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
31#[serde(rename_all = "lowercase")]
32pub enum Form {
33    /// The field-wise update a `restore()` call accepts.
34    #[default]
35    Update,
36
37    /// The complete token a `save()` call emits.
38    Complete,
39}
40
41/// Where and why a config is not valid for the current schema.
42#[derive(Clone, Debug, PartialEq, Serialize)]
43pub struct ValidateError {
44    /// A JSON pointer to the offending value, `""` for the root.
45    pub pointer: String,
46
47    /// The deserialization or consistency error at `pointer`.
48    pub message: String,
49}
50
51impl fmt::Display for ValidateError {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        write!(f, "invalid config at `{}`: {}", self.pointer, self.message)
54    }
55}
56
57impl std::error::Error for ValidateError {}
58
59/// Check that `value` is a valid `kind` config of the given `form` for the
60/// current schema, without migrating it.
61pub fn validate(kind: Kind, form: Form, value: &Value) -> Result<(), ValidateError> {
62    match (kind, form) {
63        (Kind::View, Form::Update) => {
64            shape::<ViewConfigUpdate>(value)?;
65        },
66        (Kind::View, Form::Complete) => {
67            shape::<ViewConfig>(value)?;
68        },
69        (Kind::Viewer, Form::Update) => {
70            shape::<ViewerConfigUpdate>(value)?;
71        },
72        (Kind::Viewer, Form::Complete) => {
73            shape::<ViewerConfig<String, TableRef>>(value)?;
74        },
75        (Kind::Panel, Form::Update) => {
76            shape::<ViewerConfigInitial>(value)?;
77        },
78        (Kind::Panel, Form::Complete) => {
79            shape::<PanelViewerConfig<String, String>>(value)?;
80        },
81        (Kind::Workspace, Form::Update) => {
82            let workspace = shape::<WorkspaceConfigUpdate>(value)?;
83            if let (Some(layout), Some(panels)) = (&workspace.layout, &workspace.panels) {
84                layout_places(layout, panels.keys().cloned().collect())?;
85            }
86
87            if let OptionalUpdate::Update(palette) = &workspace.palette {
88                palette_canonical(palette)?;
89            }
90
91            if let Some(derived) = &workspace.derived_tables {
92                derived_acyclic(derived)?;
93            }
94        },
95        (Kind::Workspace, Form::Complete) => {
96            let workspace = shape::<WorkspaceConfig>(value)?;
97            if let Some(layout) = &workspace.layout {
98                layout_places(layout, workspace.panels.keys().cloned().collect())?;
99            }
100
101            palette_canonical(&workspace.palette)?;
102            derived_acyclic(&workspace.derived_tables)?;
103        },
104    }
105
106    Ok(())
107}
108
109fn shape<T: DeserializeOwned>(value: &Value) -> Result<T, ValidateError> {
110    serde_path_to_error::deserialize(value).map_err(|error| ValidateError {
111        pointer: pointer_of(error.path()),
112        message: error.inner().to_string(),
113    })
114}
115
116fn pointer_of(path: &serde_path_to_error::Path) -> String {
117    path.iter()
118        .filter_map(|segment| match segment {
119            serde_path_to_error::Segment::Map { key } => Some(format!("/{}", escape(key))),
120            serde_path_to_error::Segment::Seq { index } => Some(format!("/{index}")),
121            serde_path_to_error::Segment::Enum { .. } | serde_path_to_error::Segment::Unknown => {
122                None
123            },
124        })
125        .collect()
126}
127
128fn layout_places(layout: &Layout, ids: Vec<String>) -> Result<(), ValidateError> {
129    layout
130        .validate(&ids)
131        .map(|_| ())
132        .map_err(|message| ValidateError {
133            pointer: "/layout".to_owned(),
134            message,
135        })
136}
137
138fn palette_canonical(palette: &BTreeMap<String, String>) -> Result<(), ValidateError> {
139    validate_palette(palette.clone())
140        .map(|_| ())
141        .map_err(|message| ValidateError {
142            pointer: "/palette".to_owned(),
143            message,
144        })
145}
146
147fn derived_acyclic(derived: &BTreeMap<String, DerivedTableConfig>) -> Result<(), ValidateError> {
148    for start in derived.keys() {
149        let mut seen = vec![start.as_str()];
150        let mut next = derived.get(start).map(|x| x.table.as_str());
151        while let Some(name) = next {
152            if seen.contains(&name) {
153                return Err(ValidateError {
154                    pointer: format!("/derived_tables/{}", escape(start)),
155                    message: format!("`derived_tables` cycle through `{name}`"),
156                });
157            }
158
159            seen.push(name);
160            next = derived.get(name).map(|x| x.table.as_str());
161        }
162    }
163
164    Ok(())
165}