]> git.proxmox.com Git - proxmox-backup.git/blob - src/tools/config.rs
update to first proxmox crate split
[proxmox-backup.git] / src / tools / config.rs
1 //! Our 'key: value' config format.
2
3 use std::io::Write;
4
5 use anyhow::{bail, format_err, Error};
6 use serde::{Deserialize, Serialize};
7 use serde_json::Value;
8
9 use proxmox_schema::{
10 parse_property_string, parse_simple_value, verify_json_object, ObjectSchemaType, Schema,
11 };
12
13 type Object = serde_json::Map<String, Value>;
14
15 fn object_schema(schema: &'static Schema) -> Result<&'static dyn ObjectSchemaType, Error> {
16 Ok(match schema {
17 Schema::Object(schema) => schema,
18 Schema::AllOf(schema) => schema,
19 _ => bail!("invalid schema for config, must be an object schema"),
20 })
21 }
22
23 /// Parse a full string representing a config file.
24 pub fn from_str<T: for<'de> Deserialize<'de>>(
25 input: &str,
26 schema: &'static Schema,
27 ) -> Result<T, Error> {
28 Ok(serde_json::from_value(value_from_str(input, schema)?)?)
29 }
30
31 /// Parse a full string representing a config file.
32 pub fn value_from_str(input: &str, schema: &'static Schema) -> Result<Value, Error> {
33 let schema = object_schema(schema)?;
34
35 let mut config = Object::new();
36
37 for (lineno, line) in input.lines().enumerate() {
38 let line = line.trim();
39 if line.starts_with('#') || line.is_empty() {
40 continue;
41 }
42
43 parse_line(&mut config, line, schema)
44 .map_err(|err| format_err!("line {}: {}", lineno, err))?;
45 }
46
47 Ok(Value::Object(config))
48 }
49
50 /// Parse a single `key: value` line from a config file.
51 fn parse_line(
52 config: &mut Object,
53 line: &str,
54 schema: &'static dyn ObjectSchemaType,
55 ) -> Result<(), Error> {
56 if line.starts_with('#') || line.is_empty() {
57 return Ok(());
58 }
59
60 let colon = line
61 .find(':')
62 .ok_or_else(|| format_err!("missing colon to separate key from value"))?;
63 if colon == 0 {
64 bail!("empty key not allowed");
65 }
66
67 let key = &line[..colon];
68 let value = line[(colon + 1)..].trim_start();
69
70 parse_key_value(config, key, value, schema)
71 }
72
73 /// Lookup the key in the schema, parse the value and insert it into the config object.
74 fn parse_key_value(
75 config: &mut Object,
76 key: &str,
77 value: &str,
78 schema: &'static dyn ObjectSchemaType,
79 ) -> Result<(), Error> {
80 let schema = match schema.lookup(key) {
81 Some((_optional, schema)) => Some(schema),
82 None if schema.additional_properties() => None,
83 None => bail!(
84 "invalid key '{}' and schema does not allow additional properties",
85 key
86 ),
87 };
88
89 let value = parse_value(value, schema)?;
90 config.insert(key.to_owned(), value);
91 Ok(())
92 }
93
94 /// For this we can just reuse the schema's "parse_simple_value".
95 ///
96 /// "Additional" properties (`None` schema) will simply become strings.
97 ///
98 /// Note that this does not handle Object or Array types at all, so if we want to support them
99 /// natively without going over a `String` type, we can add this here.
100 fn parse_value(value: &str, schema: Option<&'static Schema>) -> Result<Value, Error> {
101 match schema {
102 None => Ok(Value::String(value.to_owned())),
103 Some(schema) => parse_simple_value(value, schema),
104 }
105 }
106
107 /// Parse a string as a property string into a deserializable type. This is just a short wrapper
108 /// around deserializing the s
109 pub fn from_property_string<T>(input: &str, schema: &'static Schema) -> Result<T, Error>
110 where
111 T: for<'de> Deserialize<'de>,
112 {
113 Ok(serde_json::from_value(parse_property_string(
114 input, schema,
115 )?)?)
116 }
117
118 /// Serialize a data structure using a 'key: value' config file format.
119 pub fn to_bytes<T: Serialize>(value: &T, schema: &'static Schema) -> Result<Vec<u8>, Error> {
120 value_to_bytes(&serde_json::to_value(value)?, schema)
121 }
122
123 /// Serialize a json value using a 'key: value' config file format.
124 pub fn value_to_bytes(value: &Value, schema: &'static Schema) -> Result<Vec<u8>, Error> {
125 let schema = object_schema(schema)?;
126
127 verify_json_object(value, schema)?;
128
129 let object = value
130 .as_object()
131 .ok_or_else(|| format_err!("value must be an object"))?;
132
133 let mut out = Vec::new();
134 object_to_writer(&mut out, object)?;
135 Ok(out)
136 }
137
138 /// Note: the object must have already been verified at this point.
139 fn object_to_writer(output: &mut dyn Write, object: &Object) -> Result<(), Error> {
140 for (key, value) in object.iter() {
141 match value {
142 Value::Null => continue, // delete this entry
143 Value::Bool(v) => writeln!(output, "{}: {}", key, v)?,
144 Value::String(v) => writeln!(output, "{}: {}", key, v)?,
145 Value::Number(v) => writeln!(output, "{}: {}", key, v)?,
146 Value::Array(_) => bail!("arrays are not supported in config files"),
147 Value::Object(_) => bail!("complex objects are not supported in config files"),
148 }
149 }
150 Ok(())
151 }
152
153 #[test]
154 fn test() {
155 use proxmox_schema::ApiType;
156
157 // let's just reuse some schema we actually have available:
158 use crate::config::node::NodeConfig;
159
160 const NODE_CONFIG: &str = "\
161 acme: account=pebble\n\
162 acmedomain0: test1.invalid.local,plugin=power\n\
163 acmedomain1: test2.invalid.local\n\
164 ";
165
166 let data: NodeConfig = from_str(NODE_CONFIG, &NodeConfig::API_SCHEMA)
167 .expect("failed to parse simple node config");
168
169 let config = to_bytes(&data, &NodeConfig::API_SCHEMA)
170 .expect("failed to serialize node config");
171
172 assert_eq!(config, NODE_CONFIG.as_bytes());
173 }