Skip to main content

elasticctl_api/
data_views.rs

1use crate::compose::require_path_segment;
2use elasticctl_core::{Error, ErrorKind, Result, Transport};
3use serde::{Deserialize, Deserializer, Serialize, de::DeserializeOwned};
4use serde_json::{Map, Value, json};
5
6const BASE: &str = "/api/data_views";
7
8#[derive(Debug, Clone, PartialEq, Serialize)]
9#[serde(rename_all = "camelCase", deny_unknown_fields)]
10pub struct DataViewSpec {
11    pub id: String,
12    pub title: String,
13    #[serde(default, skip_serializing_if = "Option::is_none")]
14    pub name: Option<String>,
15    #[serde(default, skip_serializing_if = "Option::is_none")]
16    pub time_field_name: Option<String>,
17    #[serde(default)]
18    pub allow_no_index: bool,
19    #[serde(default)]
20    pub allow_hidden: bool,
21    #[serde(default, skip_serializing_if = "Vec::is_empty")]
22    pub source_filters: Vec<Value>,
23    #[serde(default, skip_serializing_if = "Map::is_empty")]
24    pub field_formats: Map<String, Value>,
25    #[serde(default, skip_serializing_if = "Map::is_empty")]
26    pub runtime_field_map: Map<String, Value>,
27    #[serde(default, skip_serializing_if = "Map::is_empty")]
28    pub field_attrs: Map<String, Value>,
29    #[serde(default, skip_serializing_if = "Map::is_empty")]
30    pub fields: Map<String, Value>,
31    #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
32    pub view_type: Option<String>,
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    pub type_meta: Option<Map<String, Value>>,
35}
36
37#[derive(Deserialize)]
38#[serde(rename_all = "camelCase", deny_unknown_fields)]
39struct RawDataViewSpec {
40    id: String,
41    title: String,
42    #[serde(default)]
43    name: Option<String>,
44    #[serde(default)]
45    time_field_name: Option<String>,
46    #[serde(default)]
47    allow_no_index: bool,
48    #[serde(default)]
49    allow_hidden: bool,
50    #[serde(default)]
51    source_filters: Vec<Value>,
52    #[serde(default)]
53    field_formats: Map<String, Value>,
54    #[serde(default)]
55    runtime_field_map: Map<String, Value>,
56    #[serde(default)]
57    field_attrs: Map<String, Value>,
58    #[serde(default)]
59    fields: Map<String, Value>,
60    #[serde(rename = "type", default)]
61    view_type: Option<String>,
62    #[serde(default)]
63    type_meta: Option<Value>,
64}
65
66impl DataViewSpec {
67    fn validate_shape(&self) -> Result<()> {
68        if self.id.trim().is_empty() {
69            return Err(Error::new(
70                ErrorKind::Error,
71                "data view id must not be empty",
72            ));
73        }
74        if self.title.trim().is_empty() {
75            return Err(Error::new(
76                ErrorKind::Error,
77                "data view title must not be empty",
78            ));
79        }
80        validate_object_values(&self.field_attrs, "fieldAttrs")?;
81        validate_scripted_fields(&self.fields)?;
82        Ok(())
83    }
84
85    pub fn validate(&self) -> Result<()> {
86        self.validate_shape()?;
87        if !self.fields.is_empty() {
88            return Err(Error::new(
89                ErrorKind::Unsupported,
90                format!(
91                    "legacy scripted fields are unsupported: {}",
92                    sorted_field_names(&self.fields).join(", ")
93                ),
94            ));
95        }
96        Ok(())
97    }
98}
99
100impl DataViewSpec {
101    fn from_raw(raw: RawDataViewSpec) -> Result<Self> {
102        let type_meta = match raw.type_meta {
103            Some(Value::Object(map)) if map.is_empty() => None,
104            Some(Value::Object(map)) => Some(map),
105            Some(_) => return Err(Error::new(ErrorKind::Error, "typeMeta must be an object")),
106            None => None,
107        };
108        let spec = Self {
109            id: raw.id,
110            title: raw.title,
111            name: raw.name,
112            time_field_name: raw.time_field_name,
113            allow_no_index: raw.allow_no_index,
114            allow_hidden: raw.allow_hidden,
115            source_filters: raw.source_filters,
116            field_formats: raw.field_formats,
117            runtime_field_map: raw.runtime_field_map,
118            field_attrs: raw.field_attrs,
119            fields: raw.fields,
120            view_type: raw.view_type,
121            type_meta,
122        };
123        spec.validate_shape()?;
124        Ok(spec)
125    }
126}
127
128fn validate_object_values(values: &Map<String, Value>, path: &str) -> Result<()> {
129    for (name, value) in values {
130        if !value.is_object() {
131            return Err(Error::new(
132                ErrorKind::Error,
133                format!("{path}.{name} must be an object"),
134            ));
135        }
136    }
137    Ok(())
138}
139
140fn validate_scripted_fields(fields: &Map<String, Value>) -> Result<()> {
141    for (name, field) in fields {
142        let path = format!("fields.{name}");
143        let field = field
144            .as_object()
145            .ok_or_else(|| Error::new(ErrorKind::Error, format!("{path} must be an object")))?;
146        if field.get("scripted").and_then(Value::as_bool) != Some(true) {
147            return Err(Error::new(
148                ErrorKind::Error,
149                format!("{path}.scripted must be true"),
150            ));
151        }
152    }
153    Ok(())
154}
155
156fn sorted_field_names(fields: &Map<String, Value>) -> Vec<String> {
157    let mut names: Vec<_> = fields.keys().cloned().collect();
158    names.sort();
159    names
160}
161
162impl TryFrom<Value> for DataViewSpec {
163    type Error = Error;
164
165    fn try_from(value: Value) -> Result<Self> {
166        let spec: Self = serde_json::from_value(value).map_err(|error| {
167            Error::new(ErrorKind::Error, format!("decoding data view: {error}"))
168        })?;
169        spec.validate()?;
170        Ok(spec)
171    }
172}
173
174impl<'de> Deserialize<'de> for DataViewSpec {
175    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
176    where
177        D: Deserializer<'de>,
178    {
179        let raw = RawDataViewSpec::deserialize(deserializer)?;
180        Self::from_raw(raw).map_err(serde::de::Error::custom)
181    }
182}
183
184#[derive(Debug, Clone, Default, PartialEq, Serialize)]
185#[serde(rename_all = "camelCase")]
186pub struct DataViewUpdate {
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub allow_no_index: Option<bool>,
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub field_formats: Option<Map<String, Value>>,
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub fields: Option<Map<String, Value>>,
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub name: Option<String>,
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub runtime_field_map: Option<Map<String, Value>>,
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub source_filters: Option<Vec<Value>>,
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub time_field_name: Option<String>,
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub title: Option<String>,
203    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
204    pub view_type: Option<String>,
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub type_meta: Option<Map<String, Value>>,
207}
208
209#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
210#[serde(rename_all = "camelCase")]
211pub struct DataViewSummary {
212    pub id: String,
213    pub title: String,
214    #[serde(default)]
215    pub name: Option<String>,
216    #[serde(default)]
217    pub time_field_name: Option<String>,
218}
219
220#[derive(Debug, Clone, PartialEq)]
221pub struct DataView {
222    pub data_view: Map<String, Value>,
223}
224
225#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
226#[serde(deny_unknown_fields)]
227pub struct DataViewReference {
228    pub id: String,
229    #[serde(rename = "type")]
230    pub object_type: String,
231}
232
233#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
234#[serde(rename_all = "camelCase", deny_unknown_fields)]
235pub struct DeleteStatus {
236    pub delete_performed: bool,
237    pub remaining_refs: u64,
238}
239
240#[derive(Debug, Clone, PartialEq, Eq)]
241pub struct ReferenceSwap {
242    pub result: Vec<DataViewReference>,
243    pub delete_status: DeleteStatus,
244}
245
246pub async fn list(transport: &Transport) -> Result<Vec<DataViewSummary>> {
247    let body = transport.get(BASE).await?;
248    decode_envelope::<ListEnvelope>(&body, "data views list").map(|envelope| envelope.data_view)
249}
250
251pub async fn get(transport: &Transport, id: &str) -> Result<DataView> {
252    let id = require_path_segment(id)?;
253    decode_data_view(&transport.get(&data_view_path(id)).await?, "data view get")
254}
255
256pub async fn create(transport: &Transport, spec: &DataViewSpec) -> Result<DataView> {
257    spec.validate()?;
258    let body = json!({"data_view": spec, "override": false});
259    decode_data_view(
260        &transport
261            .post(&format!("{BASE}/data_view"), Some(&body))
262            .await?,
263        "data view create",
264    )
265}
266
267pub async fn update(transport: &Transport, id: &str, update: &DataViewUpdate) -> Result<DataView> {
268    let id = require_path_segment(id)?;
269    if let Some(fields) = &update.fields {
270        validate_scripted_fields(fields)?;
271        if !fields.is_empty() {
272            return Err(Error::new(
273                ErrorKind::Unsupported,
274                format!(
275                    "legacy scripted fields are unsupported: {}",
276                    sorted_field_names(fields).join(", ")
277                ),
278            ));
279        }
280    }
281    let body = json!({"data_view": update, "refresh_fields": true});
282    decode_data_view(
283        &transport.post(&data_view_path(id), Some(&body)).await?,
284        "data view update",
285    )
286}
287
288pub async fn update_fields_metadata(
289    transport: &Transport,
290    id: &str,
291    fields: &Map<String, Value>,
292) -> Result<()> {
293    let id = require_path_segment(id)?;
294    let body = json!({"fields": fields});
295    let response = transport
296        .post(&format!("{}/fields", data_view_path(id)), Some(&body))
297        .await?;
298    decode_field_metadata_success(&response, id)
299}
300
301pub async fn delete(transport: &Transport, id: &str) -> Result<()> {
302    let id = require_path_segment(id)?;
303    decode_delete_success(&transport.delete(&data_view_path(id)).await?)
304}
305
306pub async fn get_default(transport: &Transport) -> Result<Option<String>> {
307    let body = transport.get(&format!("{BASE}/default")).await?;
308    match decode_envelope::<DefaultEnvelope>(&body, "data view default")?.data_view_id {
309        Value::String(id) if id.is_empty() => Ok(None),
310        Value::String(id) => Ok(Some(id)),
311        Value::Null => Ok(None),
312        _ => Err(Error::new(
313            ErrorKind::Http,
314            "decoding data view default field `data_view_id`: expected string or null",
315        )),
316    }
317}
318
319pub async fn set_default(transport: &Transport, id: Option<&str>) -> Result<()> {
320    if matches!(id, Some(value) if value.trim().is_empty()) {
321        return Err(Error::new(
322            ErrorKind::Error,
323            "data view default id must not be empty",
324        ));
325    }
326    let body = json!({"data_view_id": id, "force": true});
327    decode_acknowledged(
328        &transport
329            .post(&format!("{BASE}/default"), Some(&body))
330            .await?,
331        "data view default set",
332    )
333}
334
335pub async fn preview_swap(
336    transport: &Transport,
337    from_id: &str,
338    to_id: &str,
339) -> Result<Vec<DataViewReference>> {
340    let body = json!({"fromId": from_id, "toId": to_id});
341    decode_references(
342        &transport
343            .post(&format!("{BASE}/swap_references/_preview"), Some(&body))
344            .await?,
345        "data view reference swap preview",
346    )
347}
348
349pub async fn swap(transport: &Transport, from_id: &str, to_id: &str) -> Result<ReferenceSwap> {
350    let body = json!({"delete": true, "fromId": from_id, "toId": to_id});
351    let response = transport
352        .post(&format!("{BASE}/swap_references"), Some(&body))
353        .await?;
354    let envelope = decode_envelope::<SwapEnvelope>(&response, "data view reference swap")?;
355    Ok(ReferenceSwap {
356        result: envelope.result,
357        delete_status: envelope.delete_status,
358    })
359}
360
361fn data_view_path(id: &str) -> String {
362    format!("{BASE}/data_view/{}", elasticctl_core::urlencode(id))
363}
364
365fn decode_data_view(body: &Value, context: &str) -> Result<DataView> {
366    let envelope = decode_envelope::<DataViewEnvelope>(body, context)?;
367    Ok(DataView {
368        data_view: envelope.data_view,
369    })
370}
371
372fn decode_acknowledged(body: &Value, context: &str) -> Result<()> {
373    if decode_envelope::<AcknowledgedEnvelope>(body, context)?.acknowledged {
374        Ok(())
375    } else {
376        Err(Error::new(
377            ErrorKind::Http,
378            format!("decoding {context} field `acknowledged`: expected true"),
379        ))
380    }
381}
382
383fn decode_field_metadata_success(body: &Value, requested_id: &str) -> Result<()> {
384    let Value::Object(values) = body else {
385        return Err(Error::new(
386            ErrorKind::Http,
387            "decoding data view field metadata update: expected acknowledged or data_view envelope",
388        ));
389    };
390    if values.len() != 1 {
391        return Err(Error::new(
392            ErrorKind::Http,
393            "decoding data view field metadata update: expected exactly one success envelope key",
394        ));
395    }
396    if values.get("acknowledged") == Some(&Value::Bool(true)) {
397        return Ok(());
398    }
399    let Some(Value::Object(data_view)) = values.get("data_view") else {
400        return Err(Error::new(
401            ErrorKind::Http,
402            "decoding data view field metadata update: expected acknowledged: true or data_view object",
403        ));
404    };
405    match data_view.get("id") {
406        Some(Value::String(id)) if !id.is_empty() && id == requested_id => Ok(()),
407        _ => Err(Error::new(
408            ErrorKind::Http,
409            "decoding data view field metadata update: data_view id must be a non-empty request-matching string",
410        )),
411    }
412}
413
414fn decode_delete_success(body: &Value) -> Result<()> {
415    match body {
416        Value::Null => Ok(()),
417        Value::Object(_) => decode_acknowledged(body, "data view delete"),
418        _ => Err(Error::new(
419            ErrorKind::Http,
420            "decoding data view delete: expected an empty response body or acknowledged: true",
421        )),
422    }
423}
424
425fn decode_references(body: &Value, context: &str) -> Result<Vec<DataViewReference>> {
426    decode_envelope::<ReferencePreviewEnvelope>(body, context).map(|envelope| envelope.result)
427}
428
429#[derive(Deserialize)]
430#[serde(deny_unknown_fields)]
431struct ListEnvelope {
432    data_view: Vec<DataViewSummary>,
433}
434
435#[derive(Deserialize)]
436#[serde(deny_unknown_fields)]
437struct DataViewEnvelope {
438    data_view: Map<String, Value>,
439}
440
441#[derive(Deserialize)]
442#[serde(deny_unknown_fields)]
443struct AcknowledgedEnvelope {
444    acknowledged: bool,
445}
446
447#[derive(Deserialize)]
448#[serde(deny_unknown_fields)]
449struct DefaultEnvelope {
450    data_view_id: Value,
451}
452
453#[derive(Deserialize)]
454#[serde(deny_unknown_fields)]
455struct ReferencePreviewEnvelope {
456    result: Vec<DataViewReference>,
457}
458
459#[derive(Deserialize)]
460#[serde(rename_all = "camelCase", deny_unknown_fields)]
461struct SwapEnvelope {
462    result: Vec<DataViewReference>,
463    delete_status: DeleteStatus,
464}
465
466fn decode_envelope<T: DeserializeOwned>(body: &Value, context: &str) -> Result<T> {
467    serde_json::from_value(body.clone())
468        .map_err(|error| Error::new(ErrorKind::Http, format!("decoding {context}: {error}")))
469}