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