Skip to main content

elasticctl_api/value_lists/
codec.rs

1use std::path::Path;
2
3use elasticctl_core::Result;
4use serde_json::Value;
5
6use super::address::rewrite_bare_range;
7use super::{Container, LocalPair, ValueListType, ValueSet, canonical_value, invalid};
8
9pub fn decode_container(yaml: &str) -> Result<Container> {
10    let value: serde_yaml_ng::Value = serde_yaml_ng::from_str(yaml)
11        .map_err(|e| invalid(format!("invalid container YAML: {e}")))?;
12    let value = serde_json::to_value(value)
13        .map_err(|e| invalid(format!("invalid container object: {e}")))?;
14    let object = value
15        .as_object()
16        .ok_or_else(|| invalid("container must be an object"))?;
17    for key in object.keys() {
18        if !matches!(
19            key.as_str(),
20            "id" | "type" | "name" | "description" | "meta"
21        ) {
22            return Err(invalid(format!("unknown container key {key:?}")));
23        }
24    }
25    for key in ["id", "type", "name", "description"] {
26        if !object.get(key).is_some_and(Value::is_string) {
27            return Err(invalid(format!(
28                "container key {key:?} is required and must be a string"
29            )));
30        }
31    }
32    if object.get("meta").is_some_and(|meta| !meta.is_object()) {
33        return Err(invalid("container key \"meta\" must be an object"));
34    }
35    serde_json::from_value(value).map_err(|e| invalid(format!("invalid container: {e}")))
36}
37
38pub fn encode_container(container: &Container) -> Result<String> {
39    serde_yaml_ng::to_string(container).map_err(|e| invalid(format!("encode container: {e}")))
40}
41
42pub fn decode_values(kind: &ValueListType, text: &str) -> Result<ValueSet> {
43    kind.require_supported()?;
44    let mut values = ValueSet::new();
45    for (index, line) in text.split_terminator('\n').enumerate() {
46        let value = line
47            .strip_suffix('\r')
48            .unwrap_or(line)
49            .trim_matches([' ', '\t']);
50        let fail = |message| invalid(format!("line {}: value {value:?}: {message}", index + 1));
51        if value.is_empty() {
52            return Err(fail("empty value".to_owned()));
53        }
54        if value.chars().any(char::is_control) {
55            return Err(fail("control character".to_owned()));
56        }
57        canonical_value(kind, value).map_err(|e| fail(e.message))?;
58        values.insert(rewrite_bare_range(kind, value));
59    }
60    Ok(values)
61}
62
63pub fn encode_values(values: &ValueSet) -> String {
64    let mut text = String::new();
65    for value in values {
66        text.push_str(value);
67        text.push('\n');
68    }
69    text
70}
71
72pub fn read_pair(path: &Path) -> Result<LocalPair> {
73    if !matches!(
74        path.extension().and_then(|e| e.to_str()),
75        Some("yaml" | "txt")
76    ) {
77        return Err(invalid("value-list pair path must end in .yaml or .txt"));
78    }
79    let stem = path
80        .file_stem()
81        .and_then(|s| s.to_str())
82        .ok_or_else(|| invalid("value-list file stem must be UTF-8"))?;
83    let parent = path
84        .parent()
85        .filter(|p| !p.as_os_str().is_empty())
86        .unwrap_or(Path::new("."));
87    let metadata = std::fs::symlink_metadata(parent)
88        .map_err(|e| invalid(format!("read directory {}: {e}", parent.display())))?;
89    if !metadata.is_dir() {
90        return Err(invalid(format!(
91            "{} must be a real directory",
92            parent.display()
93        )));
94    }
95    let read = |extension: &str| -> Result<Vec<u8>> {
96        let file = path.with_extension(extension);
97        let metadata = std::fs::symlink_metadata(&file)
98            .map_err(|e| invalid(format!("read {}: {e}", file.display())))?;
99        if !metadata.is_file() {
100            return Err(invalid(format!(
101                "{} must be a regular file",
102                file.display()
103            )));
104        }
105        std::fs::read(&file).map_err(|e| invalid(format!("read {}: {e}", file.display())))
106    };
107    let (yaml, values) = (read("yaml")?, read("txt")?);
108    let result = super::validate_local_pair(stem, &yaml, Some(&values));
109    match result.pair {
110        Some(pair) => Ok(pair),
111        None => Err(result
112            .findings
113            .refusals
114            .into_iter()
115            .next()
116            .unwrap_or_else(|| invalid("invalid value-list pair"))),
117    }
118}