use crate::{
BasicEmitter, CommentPreservingConstructor, CommentedValue, Constructor, Emitter, Limits,
Result, RoundTripConstructor, SafeConstructor, Schema, SchemaValidator, Value,
};
use std::io::{Read, Write};
#[derive(Debug, Clone)]
pub struct YamlConfig {
pub loader_type: LoaderType,
pub pure: bool,
pub preserve_quotes: bool,
pub default_flow_style: Option<bool>,
pub allow_duplicate_keys: bool,
pub encoding: String,
pub explicit_start: Option<bool>,
pub explicit_end: Option<bool>,
pub width: Option<usize>,
pub allow_unicode: bool,
pub indent: IndentConfig,
pub preserve_comments: bool,
pub limits: Limits,
pub safe_mode: bool,
pub strict_mode: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoaderType {
Safe,
Base,
RoundTrip,
Full,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IndentConfig {
pub indent: usize,
pub map_indent: Option<usize>,
pub sequence_indent: Option<usize>,
pub sequence_dash_offset: usize,
}
impl Default for YamlConfig {
fn default() -> Self {
Self {
loader_type: LoaderType::Safe,
pure: true,
preserve_quotes: false,
default_flow_style: None,
allow_duplicate_keys: false,
encoding: "utf-8".to_string(),
explicit_start: None,
explicit_end: None,
width: Some(80),
allow_unicode: true,
indent: IndentConfig::default(),
preserve_comments: false,
limits: Limits::default(),
safe_mode: false,
strict_mode: false,
}
}
}
impl YamlConfig {
pub fn secure() -> Self {
Self {
loader_type: LoaderType::Safe,
pure: true,
preserve_quotes: false,
default_flow_style: None,
allow_duplicate_keys: false,
encoding: "utf-8".to_string(),
explicit_start: None,
explicit_end: None,
width: Some(80),
allow_unicode: true,
indent: IndentConfig::default(),
preserve_comments: false,
limits: Limits::strict(),
safe_mode: true,
strict_mode: true,
}
}
}
impl Default for IndentConfig {
fn default() -> Self {
Self {
indent: 2,
map_indent: None,
sequence_indent: None,
sequence_dash_offset: 0,
}
}
}
#[derive(Debug, Clone)]
pub struct Yaml {
config: YamlConfig,
}
impl Yaml {
pub fn new() -> Self {
Self {
config: YamlConfig::default(),
}
}
pub fn with_loader(loader_type: LoaderType) -> Self {
let mut config = YamlConfig::default();
config.loader_type = loader_type;
Self { config }
}
pub const fn with_config(config: YamlConfig) -> Self {
Self { config }
}
pub const fn config(&self) -> &YamlConfig {
&self.config
}
pub const fn config_mut(&mut self) -> &mut YamlConfig {
&mut self.config
}
pub fn load_str(&self, input: &str) -> Result<Value> {
self.load(input.as_bytes())
}
pub fn load<R: Read>(&self, mut reader: R) -> Result<Value> {
let mut buffer = String::new();
reader.read_to_string(&mut buffer)?;
self.parse_yaml_string(&buffer)
}
pub fn load_all_str(&self, input: &str) -> Result<Vec<Value>> {
self.load_all(input.as_bytes())
}
pub fn load_all<R: Read>(&self, mut reader: R) -> Result<Vec<Value>> {
let mut buffer = String::new();
reader.read_to_string(&mut buffer)?;
self.parse_yaml_documents(&buffer)
}
pub fn dump_str(&self, value: &Value) -> Result<String> {
let mut buffer = Vec::new();
self.dump(value, &mut buffer)?;
Ok(String::from_utf8(buffer)?)
}
pub fn dump<W: Write>(&self, value: &Value, writer: W) -> Result<()> {
self.emit_yaml_value(value, writer)
}
pub fn dump_all_str(&self, values: &[Value]) -> Result<String> {
let mut buffer = Vec::new();
self.dump_all(values, &mut buffer)?;
Ok(String::from_utf8(buffer)?)
}
pub fn dump_all<W: Write>(&self, values: &[Value], mut writer: W) -> Result<()> {
for (i, value) in values.iter().enumerate() {
if i > 0 {
writeln!(writer, "---")?;
}
self.dump(value, &mut writer)?;
}
Ok(())
}
pub fn load_str_with_comments(&self, input: &str) -> Result<CommentedValue> {
if !self.config.preserve_comments || self.config.loader_type != LoaderType::RoundTrip {
let value = self.load_str(input)?;
return Ok(CommentedValue::new(value));
}
self.parse_yaml_string_with_comments(input)
}
pub fn dump_str_with_comments(&self, value: &CommentedValue) -> Result<String> {
let mut buffer = Vec::new();
self.dump_with_comments(value, &mut buffer)?;
Ok(String::from_utf8(buffer)?)
}
pub fn dump_with_comments<W: Write>(&self, value: &CommentedValue, writer: W) -> Result<()> {
self.emit_commented_value(value, writer)
}
pub fn validate_with_schema(&self, value: &Value, schema: &Schema) -> Result<()> {
let validator = SchemaValidator::new(schema.clone());
validator.validate_with_report(value)
}
pub fn load_str_with_schema(&self, input: &str, schema: &Schema) -> Result<Value> {
let value = self.load_str(input)?;
self.validate_with_schema(&value, schema)?;
Ok(value)
}
pub fn load_all_str_with_schema(&self, input: &str, schema: &Schema) -> Result<Vec<Value>> {
let values = self.load_all_str(input)?;
for value in &values {
self.validate_with_schema(value, schema)?;
}
Ok(values)
}
fn parse_yaml_string(&self, input: &str) -> Result<Value> {
match self.config.loader_type {
LoaderType::Safe => {
let mut constructor =
SafeConstructor::with_limits(input.to_string(), self.config.limits.clone());
(constructor.construct()?).map_or_else(|| Ok(Value::Null), Ok)
}
_ => {
let mut constructor =
SafeConstructor::with_limits(input.to_string(), self.config.limits.clone());
(constructor.construct()?).map_or_else(|| Ok(Value::Null), Ok)
}
}
}
fn parse_yaml_documents(&self, input: &str) -> Result<Vec<Value>> {
let mut constructor =
SafeConstructor::with_limits(input.to_string(), self.config.limits.clone());
let mut documents = Vec::new();
while constructor.check_data() {
if let Some(doc) = constructor.construct()? {
documents.push(doc);
} else {
break;
}
}
if documents.is_empty() {
documents.push(Value::Null);
}
Ok(documents)
}
fn emit_yaml_value<W: Write>(&self, value: &Value, writer: W) -> Result<()> {
let mut emitter = BasicEmitter::with_indent(self.config.indent.indent);
emitter.emit(value, writer)?;
Ok(())
}
fn parse_yaml_string_with_comments(&self, input: &str) -> Result<CommentedValue> {
let mut constructor =
RoundTripConstructor::with_limits(input.to_string(), self.config.limits.clone());
match constructor.construct_commented()? {
Some(commented_value) => Ok(commented_value),
None => Ok(CommentedValue::new(Value::Null)),
}
}
fn emit_commented_value<W: Write>(&self, value: &CommentedValue, writer: W) -> Result<()> {
let mut emitter = BasicEmitter::with_indent(self.config.indent.indent);
emitter.emit_commented_value_public(value, writer)?;
Ok(())
}
fn emit_yaml_documents<W: Write>(&self, values: &[Value], mut writer: W) -> Result<()> {
for (i, value) in values.iter().enumerate() {
if i > 0 {
writeln!(writer, "---")?;
}
self.emit_yaml_value(value, &mut writer)?;
}
Ok(())
}
}
impl Default for Yaml {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_yaml_creation() {
let yaml = Yaml::new();
assert_eq!(yaml.config().loader_type, LoaderType::Safe);
let yaml_rt = Yaml::with_loader(LoaderType::RoundTrip);
assert_eq!(yaml_rt.config().loader_type, LoaderType::RoundTrip);
}
#[test]
fn test_basic_scalar_parsing() {
let yaml = Yaml::new();
assert_eq!(yaml.load_str("null").unwrap(), Value::Null);
assert_eq!(yaml.load_str("true").unwrap(), Value::Bool(true));
assert_eq!(yaml.load_str("false").unwrap(), Value::Bool(false));
assert_eq!(yaml.load_str("42").unwrap(), Value::Int(42));
assert_eq!(yaml.load_str("3.14").unwrap(), Value::Float(3.14));
assert_eq!(
yaml.load_str("hello").unwrap(),
Value::String("hello".to_string())
);
assert_eq!(
yaml.load_str("\"quoted\"").unwrap(),
Value::String("quoted".to_string())
);
}
#[test]
fn test_basic_scalar_dumping() {
let yaml = Yaml::new();
assert_eq!(yaml.dump_str(&Value::Null).unwrap().trim(), "null");
assert_eq!(yaml.dump_str(&Value::Bool(true)).unwrap().trim(), "true");
assert_eq!(yaml.dump_str(&Value::Int(42)).unwrap().trim(), "42");
assert_eq!(yaml.dump_str(&Value::Float(3.14)).unwrap().trim(), "3.14");
assert_eq!(
yaml.dump_str(&Value::String("hello".to_string()))
.unwrap()
.trim(),
"hello"
);
}
#[test]
fn test_multi_document() {
let yaml = Yaml::new();
let input = "doc1\n---\ndoc2\n---\ndoc3";
let docs = yaml.load_all_str(input).unwrap();
assert_eq!(docs.len(), 3);
assert_eq!(docs[0], Value::String("doc1".to_string()));
assert_eq!(docs[1], Value::String("doc2".to_string()));
assert_eq!(docs[2], Value::String("doc3".to_string()));
}
#[test]
fn test_config_modification() {
let mut yaml = Yaml::new();
yaml.config_mut().loader_type = LoaderType::Full;
yaml.config_mut().allow_unicode = false;
assert_eq!(yaml.config().loader_type, LoaderType::Full);
assert!(!yaml.config().allow_unicode);
}
}