Skip to main content

elasticctl_api/
data_views_ops.rs

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}