perspective_migrate/
validate.rs1use 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#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
31#[serde(rename_all = "lowercase")]
32pub enum Form {
33 #[default]
35 Update,
36
37 Complete,
39}
40
41#[derive(Clone, Debug, PartialEq, Serialize)]
43pub struct ValidateError {
44 pub pointer: String,
46
47 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
59pub 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}