elasticctl_api/value_lists/
codec.rs1use 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}