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