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