//! Our 'key: value' config format. use std::io::Write; use anyhow::{bail, format_err, Error}; use serde::{Deserialize, Serialize}; use serde_json::Value; use proxmox_schema::{ObjectSchemaType, Schema}; type Object = serde_json::Map; fn object_schema(schema: &'static Schema) -> Result<&'static dyn ObjectSchemaType, Error> { Ok(match schema { Schema::Object(schema) => schema, Schema::AllOf(schema) => schema, _ => bail!("invalid schema for config, must be an object schema"), }) } /// Parse a full string representing a config file. pub fn from_str Deserialize<'de>>( input: &str, schema: &'static Schema, ) -> Result { Ok(serde_json::from_value(value_from_str(input, schema)?)?) } /// Parse a full string representing a config file. pub fn value_from_str(input: &str, schema: &'static Schema) -> Result { let schema = object_schema(schema)?; let mut config = Object::new(); for (lineno, line) in input.lines().enumerate() { let line = line.trim(); if line.starts_with('#') || line.is_empty() { continue; } parse_line(&mut config, line, schema) .map_err(|err| format_err!("line {}: {}", lineno, err))?; } Ok(Value::Object(config)) } /// Parse a single `key: value` line from a config file. fn parse_line( config: &mut Object, line: &str, schema: &'static dyn ObjectSchemaType, ) -> Result<(), Error> { if line.starts_with('#') || line.is_empty() { return Ok(()); } let colon = line .find(':') .ok_or_else(|| format_err!("missing colon to separate key from value"))?; if colon == 0 { bail!("empty key not allowed"); } let key = &line[..colon]; let value = line[(colon + 1)..].trim_start(); parse_key_value(config, key, value, schema) } /// Lookup the key in the schema, parse the value and insert it into the config object. fn parse_key_value( config: &mut Object, key: &str, value: &str, schema: &'static dyn ObjectSchemaType, ) -> Result<(), Error> { let schema = match schema.lookup(key) { Some((_optional, schema)) => Some(schema), None if schema.additional_properties() => None, None => bail!( "invalid key '{}' and schema does not allow additional properties", key ), }; let value = parse_value(value, schema)?; config.insert(key.to_owned(), value); Ok(()) } /// For this we can just reuse the schema's "parse_simple_value". /// /// "Additional" properties (`None` schema) will simply become strings. /// /// Note that this does not handle Object or Array types at all, so if we want to support them /// natively without going over a `String` type, we can add this here. fn parse_value(value: &str, schema: Option<&'static Schema>) -> Result { match schema { None => Ok(Value::String(value.to_owned())), Some(schema) => schema.parse_simple_value(value), } } /// Parse a string as a property string into a deserializable type. This is just a short wrapper /// around deserializing the s pub fn from_property_string(input: &str, schema: &'static Schema) -> Result where T: for<'de> Deserialize<'de>, { Ok(serde_json::from_value(schema.parse_property_string(input)?)?) } /// Serialize a data structure using a 'key: value' config file format. pub fn to_bytes(value: &T, schema: &'static Schema) -> Result, Error> { value_to_bytes(&serde_json::to_value(value)?, schema) } /// Serialize a json value using a 'key: value' config file format. pub fn value_to_bytes(value: &Value, schema: &'static Schema) -> Result, Error> { let schema = object_schema(schema)?; schema.verify_json(value)?; let object = value .as_object() .ok_or_else(|| format_err!("value must be an object"))?; let mut out = Vec::new(); object_to_writer(&mut out, object)?; Ok(out) } /// Note: the object must have already been verified at this point. fn object_to_writer(output: &mut dyn Write, object: &Object) -> Result<(), Error> { for (key, value) in object.iter() { match value { Value::Null => continue, // delete this entry Value::Bool(v) => writeln!(output, "{}: {}", key, v)?, Value::String(v) => { if v.as_bytes().contains(&b'\n') { bail!("value for {} contains newlines", key); } writeln!(output, "{}: {}", key, v)? } Value::Number(v) => writeln!(output, "{}: {}", key, v)?, Value::Array(_) => bail!("arrays are not supported in config files"), Value::Object(_) => bail!("complex objects are not supported in config files"), } } Ok(()) } #[test] fn test() { use proxmox_schema::ApiType; // let's just reuse some schema we actually have available: use crate::config::node::NodeConfig; const NODE_CONFIG: &str = "\ acme: account=pebble\n\ acmedomain0: test1.invalid.local,plugin=power\n\ acmedomain1: test2.invalid.local\n\ "; let data: NodeConfig = from_str(NODE_CONFIG, &NodeConfig::API_SCHEMA) .expect("failed to parse simple node config"); let config = to_bytes(&data, &NodeConfig::API_SCHEMA) .expect("failed to serialize node config"); assert_eq!(config, NODE_CONFIG.as_bytes()); }