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}