Skip to main content

rust_yaml/
yaml.rs

1//! Main YAML API interface
2
3use crate::{
4    BasicEmitter, CommentPreservingConstructor, CommentedValue, Constructor, Emitter, Limits,
5    Result, RoundTripConstructor, SafeConstructor, Schema, SchemaValidator, Value,
6};
7use std::io::{Read, Write};
8
9/// Configuration for YAML processing
10#[derive(Debug, Clone)]
11pub struct YamlConfig {
12    /// Type of loader/dumper to use
13    pub loader_type: LoaderType,
14    /// Whether to use pure Rust implementation (no C extensions)
15    pub pure: bool,
16    /// Whether to preserve quote styles during round-trip
17    pub preserve_quotes: bool,
18    /// Default flow style for output
19    pub default_flow_style: Option<bool>,
20    /// Whether to allow duplicate keys
21    pub allow_duplicate_keys: bool,
22    /// Text encoding to use
23    pub encoding: String,
24    /// Whether to add explicit document start markers
25    pub explicit_start: Option<bool>,
26    /// Whether to add explicit document end markers
27    pub explicit_end: Option<bool>,
28    /// Line width for output formatting
29    pub width: Option<usize>,
30    /// Whether to allow unicode characters
31    pub allow_unicode: bool,
32    /// Indentation settings
33    pub indent: IndentConfig,
34    /// Whether to preserve comments during round-trip operations
35    pub preserve_comments: bool,
36    /// Resource limits for secure processing
37    pub limits: Limits,
38    /// Enable safe mode (restricts dangerous features)
39    pub safe_mode: bool,
40    /// Enable strict mode (fail on ambiguous constructs)
41    pub strict_mode: bool,
42    /// Whether to emit anchors/aliases for shared values during serialization
43    pub emit_anchors: bool,
44}
45
46/// Type of YAML loader/dumper
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum LoaderType {
49    /// Safe loader - only basic YAML types, no code execution
50    Safe,
51    /// Base loader - minimal type set
52    Base,
53    /// Round-trip loader - preserves formatting and comments (future)
54    RoundTrip,
55    /// Full loader - all features including potentially unsafe operations
56    Full,
57}
58
59/// Indentation configuration
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct IndentConfig {
62    /// Base indentation
63    pub indent: usize,
64    /// Map indentation
65    pub map_indent: Option<usize>,
66    /// Sequence indentation
67    pub sequence_indent: Option<usize>,
68    /// Sequence dash offset
69    pub sequence_dash_offset: usize,
70}
71
72impl Default for YamlConfig {
73    fn default() -> Self {
74        Self {
75            loader_type: LoaderType::Safe,
76            pure: true,
77            preserve_quotes: false,
78            default_flow_style: None,
79            allow_duplicate_keys: false,
80            encoding: "utf-8".to_string(),
81            explicit_start: None,
82            explicit_end: None,
83            width: Some(80),
84            allow_unicode: true,
85            indent: IndentConfig::default(),
86            preserve_comments: false,
87            limits: Limits::default(),
88            safe_mode: false,
89            strict_mode: false,
90            emit_anchors: true,
91        }
92    }
93}
94
95impl YamlConfig {
96    /// Creates a secure configuration for untrusted input
97    pub fn secure() -> Self {
98        Self {
99            loader_type: LoaderType::Safe,
100            pure: true,
101            preserve_quotes: false,
102            default_flow_style: None,
103            allow_duplicate_keys: false,
104            encoding: "utf-8".to_string(),
105            explicit_start: None,
106            explicit_end: None,
107            width: Some(80),
108            allow_unicode: true,
109            indent: IndentConfig::default(),
110            preserve_comments: false,
111            limits: Limits::strict(),
112            safe_mode: true,
113            strict_mode: true,
114            emit_anchors: true,
115        }
116    }
117}
118
119impl Default for IndentConfig {
120    fn default() -> Self {
121        Self {
122            indent: 2,
123            map_indent: None,
124            sequence_indent: None,
125            sequence_dash_offset: 0,
126        }
127    }
128}
129
130/// Main YAML processing interface
131#[derive(Debug, Clone)]
132pub struct Yaml {
133    config: YamlConfig,
134}
135
136impl Yaml {
137    /// Create a new YAML processor with default configuration
138    pub fn new() -> Self {
139        Self {
140            config: YamlConfig::default(),
141        }
142    }
143
144    /// Create a new YAML processor with the specified loader type
145    pub fn with_loader(loader_type: LoaderType) -> Self {
146        let mut config = YamlConfig::default();
147        config.loader_type = loader_type;
148        Self { config }
149    }
150
151    /// Create a new YAML processor with custom configuration
152    pub const fn with_config(config: YamlConfig) -> Self {
153        Self { config }
154    }
155
156    /// Get the current configuration
157    pub const fn config(&self) -> &YamlConfig {
158        &self.config
159    }
160
161    /// Get a mutable reference to the configuration
162    pub const fn config_mut(&mut self) -> &mut YamlConfig {
163        &mut self.config
164    }
165
166    /// Load YAML from a string
167    pub fn load_str(&self, input: &str) -> Result<Value> {
168        self.load(input.as_bytes())
169    }
170
171    /// Load YAML from a reader
172    pub fn load<R: Read>(&self, mut reader: R) -> Result<Value> {
173        let mut buffer = String::new();
174        reader.read_to_string(&mut buffer)?;
175
176        // For now, return a placeholder implementation
177        // This will be replaced with the actual parser implementation
178        self.parse_yaml_string(&buffer)
179    }
180
181    /// Load all YAML documents from a string
182    pub fn load_all_str(&self, input: &str) -> Result<Vec<Value>> {
183        self.load_all(input.as_bytes())
184    }
185
186    /// Load all YAML documents from a reader
187    pub fn load_all<R: Read>(&self, mut reader: R) -> Result<Vec<Value>> {
188        let mut buffer = String::new();
189        reader.read_to_string(&mut buffer)?;
190
191        // For now, return a placeholder implementation
192        // This will be replaced with the actual parser implementation
193        self.parse_yaml_documents(&buffer)
194    }
195
196    /// Dump a YAML value to a string
197    pub fn dump_str(&self, value: &Value) -> Result<String> {
198        let mut buffer = Vec::new();
199        self.dump(value, &mut buffer)?;
200        Ok(String::from_utf8(buffer)?)
201    }
202
203    /// Dump a YAML value to a writer
204    pub fn dump<W: Write>(&self, value: &Value, writer: W) -> Result<()> {
205        // For now, return a placeholder implementation
206        // This will be replaced with the actual emitter implementation
207        self.emit_yaml_value(value, writer)
208    }
209
210    /// Dump all YAML documents to a string
211    pub fn dump_all_str(&self, values: &[Value]) -> Result<String> {
212        let mut buffer = Vec::new();
213        self.dump_all(values, &mut buffer)?;
214        Ok(String::from_utf8(buffer)?)
215    }
216
217    /// Dump all YAML documents to a writer
218    pub fn dump_all<W: Write>(&self, values: &[Value], mut writer: W) -> Result<()> {
219        for (i, value) in values.iter().enumerate() {
220            if i > 0 {
221                writeln!(writer, "---")?;
222            }
223            self.dump(value, &mut writer)?;
224        }
225        Ok(())
226    }
227
228    /// Load YAML from a string with comment preservation (RoundTrip mode only)
229    pub fn load_str_with_comments(&self, input: &str) -> Result<CommentedValue> {
230        if !self.config.preserve_comments || self.config.loader_type != LoaderType::RoundTrip {
231            // If not in round-trip mode, parse normally and wrap in CommentedValue
232            let value = self.load_str(input)?;
233            return Ok(CommentedValue::new(value));
234        }
235
236        self.parse_yaml_string_with_comments(input)
237    }
238
239    /// Dump a CommentedValue to a string, preserving comments
240    pub fn dump_str_with_comments(&self, value: &CommentedValue) -> Result<String> {
241        let mut buffer = Vec::new();
242        self.dump_with_comments(value, &mut buffer)?;
243        Ok(String::from_utf8(buffer)?)
244    }
245
246    /// Dump a CommentedValue to a writer, preserving comments
247    pub fn dump_with_comments<W: Write>(&self, value: &CommentedValue, writer: W) -> Result<()> {
248        self.emit_commented_value(value, writer)
249    }
250
251    /// Validate a YAML value against a schema
252    pub fn validate_with_schema(&self, value: &Value, schema: &Schema) -> Result<()> {
253        let validator = SchemaValidator::new(schema.clone());
254        validator.validate_with_report(value)
255    }
256
257    /// Load and validate YAML from a string with schema validation
258    pub fn load_str_with_schema(&self, input: &str, schema: &Schema) -> Result<Value> {
259        let value = self.load_str(input)?;
260        self.validate_with_schema(&value, schema)?;
261        Ok(value)
262    }
263
264    /// Load and validate all YAML documents from a string with schema validation
265    pub fn load_all_str_with_schema(&self, input: &str, schema: &Schema) -> Result<Vec<Value>> {
266        let values = self.load_all_str(input)?;
267        for value in &values {
268            self.validate_with_schema(value, schema)?;
269        }
270        Ok(values)
271    }
272
273    // Placeholder implementations - will be replaced with actual parser/emitter
274
275    fn parse_yaml_string(&self, input: &str) -> Result<Value> {
276        // Use our complete parsing pipeline: Scanner -> Parser -> Composer -> Constructor
277        match self.config.loader_type {
278            LoaderType::Safe => {
279                let mut constructor =
280                    SafeConstructor::with_limits(input.to_string(), self.config.limits.clone());
281                (constructor.construct()?).map_or_else(|| Ok(Value::Null), Ok)
282            }
283            _ => {
284                // For now, all loader types use SafeConstructor
285                // Future versions will implement different constructors
286                let mut constructor =
287                    SafeConstructor::with_limits(input.to_string(), self.config.limits.clone());
288                (constructor.construct()?).map_or_else(|| Ok(Value::Null), Ok)
289            }
290        }
291    }
292
293    fn parse_yaml_documents(&self, input: &str) -> Result<Vec<Value>> {
294        // Use the proper parsing pipeline to handle multi-document streams
295        let mut constructor =
296            SafeConstructor::with_limits(input.to_string(), self.config.limits.clone());
297        let mut documents = Vec::new();
298
299        // Try to construct documents until no more are available
300        while constructor.check_data() {
301            if let Some(doc) = constructor.construct()? {
302                documents.push(doc);
303            } else {
304                break;
305            }
306        }
307
308        if documents.is_empty() {
309            documents.push(Value::Null);
310        }
311
312        Ok(documents)
313    }
314
315    fn emit_yaml_value<W: Write>(&self, value: &Value, writer: W) -> Result<()> {
316        // Use the proper emitter implementation
317        let mut emitter = BasicEmitter::with_indent(self.config.indent.indent);
318        emitter.set_emit_anchors(self.config.emit_anchors);
319        emitter.set_sequence_indent(self.config.indent.sequence_indent);
320        emitter.emit(value, writer)?;
321        Ok(())
322    }
323
324    fn parse_yaml_string_with_comments(&self, input: &str) -> Result<CommentedValue> {
325        // Use the round-trip constructor for comment preservation
326        let mut constructor =
327            RoundTripConstructor::with_limits(input.to_string(), self.config.limits.clone());
328
329        match constructor.construct_commented()? {
330            Some(commented_value) => Ok(commented_value),
331            None => Ok(CommentedValue::new(Value::Null)),
332        }
333    }
334
335    fn emit_commented_value<W: Write>(&self, value: &CommentedValue, writer: W) -> Result<()> {
336        // Use the proper emitter implementation with comment support
337        let mut emitter = BasicEmitter::with_indent(self.config.indent.indent);
338        emitter.set_emit_anchors(self.config.emit_anchors);
339        emitter.set_sequence_indent(self.config.indent.sequence_indent);
340        emitter.emit_commented_value_public(value, writer)?;
341        Ok(())
342    }
343
344    fn emit_yaml_documents<W: Write>(&self, values: &[Value], mut writer: W) -> Result<()> {
345        for (i, value) in values.iter().enumerate() {
346            if i > 0 {
347                writeln!(writer, "---")?;
348            }
349            self.emit_yaml_value(value, &mut writer)?;
350        }
351        Ok(())
352    }
353}
354
355impl Default for Yaml {
356    fn default() -> Self {
357        Self::new()
358    }
359}
360
361#[cfg(test)]
362mod tests {
363    use super::*;
364
365    #[test]
366    fn test_yaml_creation() {
367        let yaml = Yaml::new();
368        assert_eq!(yaml.config().loader_type, LoaderType::Safe);
369
370        let yaml_rt = Yaml::with_loader(LoaderType::RoundTrip);
371        assert_eq!(yaml_rt.config().loader_type, LoaderType::RoundTrip);
372    }
373
374    #[test]
375    fn test_basic_scalar_parsing() {
376        let yaml = Yaml::new();
377
378        assert_eq!(yaml.load_str("null").unwrap(), Value::Null);
379        assert_eq!(yaml.load_str("true").unwrap(), Value::Bool(true));
380        assert_eq!(yaml.load_str("false").unwrap(), Value::Bool(false));
381        assert_eq!(yaml.load_str("42").unwrap(), Value::Int(42));
382        assert_eq!(yaml.load_str("3.14").unwrap(), Value::Float(3.14));
383        assert_eq!(
384            yaml.load_str("hello").unwrap(),
385            Value::String("hello".to_string())
386        );
387        assert_eq!(
388            yaml.load_str("\"quoted\"").unwrap(),
389            Value::String("quoted".to_string())
390        );
391    }
392
393    #[test]
394    fn test_basic_scalar_dumping() {
395        let yaml = Yaml::new();
396
397        assert_eq!(yaml.dump_str(&Value::Null).unwrap().trim(), "null");
398        assert_eq!(yaml.dump_str(&Value::Bool(true)).unwrap().trim(), "true");
399        assert_eq!(yaml.dump_str(&Value::Int(42)).unwrap().trim(), "42");
400        assert_eq!(yaml.dump_str(&Value::Float(3.14)).unwrap().trim(), "3.14");
401        assert_eq!(
402            yaml.dump_str(&Value::String("hello".to_string()))
403                .unwrap()
404                .trim(),
405            "hello"
406        );
407    }
408
409    #[test]
410    fn test_multi_document() {
411        let yaml = Yaml::new();
412        let input = "doc1\n---\ndoc2\n---\ndoc3";
413        let docs = yaml.load_all_str(input).unwrap();
414
415        assert_eq!(docs.len(), 3);
416        assert_eq!(docs[0], Value::String("doc1".to_string()));
417        assert_eq!(docs[1], Value::String("doc2".to_string()));
418        assert_eq!(docs[2], Value::String("doc3".to_string()));
419    }
420
421    #[test]
422    fn test_config_modification() {
423        let mut yaml = Yaml::new();
424        yaml.config_mut().loader_type = LoaderType::Full;
425        yaml.config_mut().allow_unicode = false;
426
427        assert_eq!(yaml.config().loader_type, LoaderType::Full);
428        assert!(!yaml.config().allow_unicode);
429    }
430}