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