rust_yaml/
yaml.rs

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