1use crate::content_codec::{self, ContentFormat};
2use crate::data_views::{
3 self, DataView, DataViewReference, DataViewSpec, DataViewSummary, DataViewUpdate,
4};
5use crate::ops::{DeleteOutcome, ExportOutcome, MutationPlan};
6use elasticctl_core::{Error, ErrorKind, Result, Transport};
7use serde::Serialize;
8use serde_json::{Map, Value, json};
9use std::collections::{BTreeMap, BTreeSet};
10use std::path::Path;
11
12mod defaults;
13mod delete;
14mod import;
15pub use defaults::{DefaultPlan, apply_default, plan_default_set, plan_default_unset};
16pub use delete::{DataViewDeletePlan, DataViewDeleteTarget, apply_delete, plan_delete};
17pub use import::{
18 DataViewImportPlan, DataViewImportReport, DataViewPatch, apply_import, build_patch, plan_import,
19};
20
21#[derive(Debug, Clone, Default, PartialEq, Eq)]
22pub struct DataViewFilter {
23 pub search: Option<String>,
24}
25
26#[derive(Debug, Clone, PartialEq, Serialize)]
27pub struct DataViewList {
28 pub total: usize,
29 pub data_views: Vec<DataViewSummary>,
30}
31
32pub(crate) fn select_by_id_or_name<'a, T>(
33 entries: &'a [T],
34 selector: &str,
35 id: impl Fn(&T) -> Option<&str>,
36 name: impl Fn(&T) -> Option<&str>,
37) -> Result<&'a T> {
38 if let Some(entry) = entries.iter().find(|entry| id(entry) == Some(selector)) {
39 return Ok(entry);
40 }
41
42 let matches: Vec<&T> = entries
43 .iter()
44 .filter(|entry| name(entry) == Some(selector))
45 .collect();
46 match matches.as_slice() {
47 [] => Err(Error::new(
48 ErrorKind::NotFound,
49 format!("no data view with id or name '{selector}'"),
50 )),
51 [entry] => Ok(entry),
52 _ => Err(Error::new(
53 ErrorKind::Conflict,
54 format!("data view '{selector}' is ambiguous"),
55 )),
56 }
57}
58
59pub fn resolve_from_summaries(
60 views: &[DataViewSummary],
61 selector: &str,
62) -> Result<DataViewSummary> {
63 Ok(select_by_id_or_name(
64 views,
65 selector,
66 |view| Some(view.id.as_str()),
67 |view| view.name.as_deref(),
68 )?
69 .clone())
70}
71
72pub async fn resolve(transport: &Transport, selector: &str) -> Result<DataViewSummary> {
73 let views = data_views::list(transport).await?;
74 resolve_from_summaries(&views, selector)
75}
76
77pub async fn list_op(transport: &Transport, filter: &DataViewFilter) -> Result<DataViewList> {
78 let needle = filter.search.as_ref().map(|search| search.to_lowercase());
79 let mut data_views: Vec<_> = data_views::list(transport)
80 .await?
81 .into_iter()
82 .filter(|view| {
83 needle.as_ref().is_none_or(|needle| {
84 view.id.to_lowercase().contains(needle)
85 || view
86 .name
87 .as_ref()
88 .is_some_and(|name| name.to_lowercase().contains(needle))
89 || view.title.to_lowercase().contains(needle)
90 })
91 })
92 .collect();
93 data_views.sort_by(|left, right| left.id.cmp(&right.id));
94 Ok(DataViewList {
95 total: data_views.len(),
96 data_views,
97 })
98}
99
100pub async fn get_op(transport: &Transport, selector: &str) -> Result<DataView> {
101 let view = resolve(transport, selector).await?;
102 data_views::get(transport, &view.id).await
103}
104
105pub fn normalize(data_view: &Value) -> Result<DataViewSpec> {
106 let source = data_view
107 .as_object()
108 .ok_or_else(|| Error::new(ErrorKind::Http, "decoding data view: expected object"))?;
109 let mut portable = Map::new();
110
111 for key in ["id", "title"] {
112 if let Some(value) = source.get(key) {
113 portable.insert(key.to_string(), canonicalize(value));
114 }
115 }
116 for key in [
117 "name",
118 "timeFieldName",
119 "sourceFilters",
120 "fieldFormats",
121 "runtimeFieldMap",
122 "fieldAttrs",
123 "type",
124 ] {
125 if let Some(value) = source.get(key) {
126 portable.insert(key.to_string(), canonicalize(value));
127 }
128 }
129 for key in ["allowNoIndex", "allowHidden"] {
130 portable.insert(
131 key.to_string(),
132 source
133 .get(key)
134 .map(canonicalize)
135 .unwrap_or(Value::Bool(false)),
136 );
137 }
138 if let Some(type_meta) = source.get("typeMeta")
139 && !type_meta.is_null()
140 && !matches!(type_meta, Value::Object(values) if values.is_empty())
141 {
142 portable.insert("typeMeta".to_string(), canonicalize(type_meta));
143 }
144 if let Some(fields) = source.get("fields") {
145 let fields = match fields {
146 Value::Object(fields) => {
147 let mut scripted: Vec<_> = fields
148 .iter()
149 .filter_map(|(name, field)| {
150 (field.get("scripted").and_then(Value::as_bool) == Some(true))
151 .then_some(name.as_str())
152 })
153 .collect();
154 scripted.sort_unstable();
155 if !scripted.is_empty() {
156 return Err(Error::new(
157 ErrorKind::Unsupported,
158 format!(
159 "legacy scripted fields are unsupported: {}",
160 scripted.join(", ")
161 ),
162 ));
163 }
164 Value::Object(Map::new())
165 }
166 other => canonicalize(other),
167 };
168 portable.insert("fields".to_string(), canonicalize(&fields));
169 }
170
171 DataViewSpec::try_from(canonicalize(&Value::Object(portable))).map_err(|error| {
172 Error::new(
173 ErrorKind::Http,
174 format!("decoding data view: {}", error.message),
175 )
176 })
177}
178
179fn canonicalize(value: &Value) -> Value {
180 match value {
181 Value::Object(values) => {
182 let mut keys: Vec<_> = values.keys().collect();
183 keys.sort_unstable();
184 Value::Object(
185 keys.into_iter()
186 .map(|key| (key.clone(), canonicalize(&values[key])))
187 .collect(),
188 )
189 }
190 Value::Array(values) => Value::Array(values.iter().map(canonicalize).collect()),
191 _ => value.clone(),
192 }
193}
194
195pub fn validate(path: &Path) -> Result<Vec<DataViewSpec>> {
196 let body = std::fs::read_to_string(path).map_err(|error| {
197 Error::new(
198 ErrorKind::Error,
199 format!("reading {}: {error}", path.display()),
200 )
201 })?;
202 let mut specs = content_codec::decode_sequence::<DataViewSpec>(
203 &body,
204 ContentFormat::from_path(path),
205 "data view",
206 )?;
207
208 let mut seen = BTreeSet::new();
209 let mut duplicates = BTreeSet::new();
210 for spec in &specs {
211 spec.validate()?;
212 if !seen.insert(spec.id.as_str()) {
213 duplicates.insert(spec.id.as_str());
214 }
215 }
216 if !duplicates.is_empty() {
217 return Err(Error::new(
218 ErrorKind::Error,
219 format!(
220 "duplicate data view ids: {}",
221 duplicates.into_iter().collect::<Vec<_>>().join(", ")
222 ),
223 ));
224 }
225
226 specs.sort_by(|left, right| left.id.cmp(&right.id));
227 Ok(specs)
228}
229
230fn invalid_plan(message: impl Into<String>) -> Result<()> {
231 Err(Error::new(ErrorKind::Error, message))
232}
233
234pub async fn export(
235 transport: &Transport,
236 selectors: &[String],
237 format: ContentFormat,
238) -> Result<ExportOutcome> {
239 let summaries = data_views::list(transport).await?;
240 let selected = if selectors.is_empty() {
241 summaries
242 } else {
243 selectors
244 .iter()
245 .map(|selector| resolve_from_summaries(&summaries, selector))
246 .collect::<Result<Vec<_>>>()?
247 };
248 let selected: BTreeMap<_, _> = selected
249 .into_iter()
250 .map(|summary| (summary.id.clone(), summary))
251 .collect();
252 let expected = selected.len();
253 let mut specs = Vec::with_capacity(expected);
254 for (id, _) in selected {
255 let detail = data_views::get(transport, &id).await?;
256 let spec = normalize(&Value::Object(detail.data_view))?;
257 if spec.id != id {
258 return Err(Error::new(
259 ErrorKind::Http,
260 format!("data view export was short: expected id '{id}'"),
261 ));
262 }
263 specs.push(spec);
264 }
265 if specs.len() != expected {
266 return Err(Error::new(
267 ErrorKind::Http,
268 format!(
269 "data view export was short: expected {expected}, got {}",
270 specs.len()
271 ),
272 ));
273 }
274 specs.sort_by(|left, right| left.id.cmp(&right.id));
275 let body = content_codec::encode_sequence(&specs, format)?;
276 Ok(ExportOutcome {
277 body,
278 exported: specs.len() as u64,
279 missing: Vec::new(),
280 })
281}