nccl/lib.rs
1//! A simple configuration language.
2//!
3//! Nccl is an easy way to add minimal configuration to your crate without
4//! having to deal with complicated interfaces, obnoxious syntax, or outdated
5//! languages. Nccl makes it easy for a user to pick up your configuration.
6//! It's as easy as five cents.
7//!
8//! Nccl was motivated by the fact that other configuration languages are too
9//! complicated for both end-users and developers. Everyone loves types, but
10//! in a configuration language, there's too much room for interpretation
11//! There are no types here.
12//!
13//! This is a nice approach for smaller, non-semantic configurations; you
14//! shouldn't be accidentally implementing a DSL with nccl, and if you are, it
15//! should feel painful.
16//!
17//! For interacting with a parsed configuration, see [`config::Config`].
18//!
19//! ## Syntax
20//!
21//! The single most important feature:
22//!
23//! ```text
24//! key
25//! value
26//! ```
27//!
28//! ```rust
29//! let source = r#"
30//! key
31//! value
32//! "#;
33//! let config = nccl::parse_config(&source).unwrap();
34//! assert_eq!(config["key"].value(), Some("value"));
35//! ```
36//!
37//! Results in `config["key"].value() == Some("value")`. Note that there is no
38//! semantic difference between a key and a value, so `config.value() == Some("key")`.
39//! For most of this document and the library source code, the words key, value, and
40//! node are used almost interchangeably.
41//!
42//! There are no types:
43//!
44//! ```rust
45//! let source = r#"
46//! threads
47//! 16
48//! is this a problem?
49//! no 🇳🇴
50//! end of the world
51//! 2012-12-21"#;
52//! let config = nccl::parse_config(&source).unwrap();
53//! assert_eq!(config["is this a problem?"].value(), Some("no 🇳🇴"));
54//! ```
55//!
56//! Interpret the data however you want.
57//!
58//! Indentation is significant. Each top-level key-value pair must use the same
59//! type of indentation for each sub-value. You may mix indentation per-file.
60//!
61//! ```rust
62//! let source = r#"
63//! ## tab
64//! a
65//! 1
66//!
67//! ## single space
68//! b
69//! 2
70//! "#;
71//! let config = nccl::parse_config(&source).unwrap();
72//! assert_eq!(config["a"].value(), Some("1"));
73//! assert_eq!(config["b"].value(), Some("2"));
74//! ```
75//!
76//! Comments exist on their own line or after a quoted value, otherwise they
77//! become part of the value.
78//!
79//! ```rust
80//! let source = r#"
81//! hello # this is part of the key!
82//! ## this is not
83//! world
84//! "y'all" # this isn't either
85//! "#;
86//! let config = nccl::parse_config(&source).unwrap();
87//! assert!(config.has_value("hello # this is part of the key!"));
88//! assert!(!config["hello # this is part of the key!"].has_value("# this is not"));
89//! assert!(config["hello # this is part of the key!"].has_value("y'all"));
90//! ```
91//!
92//! Duplicate keys have their values merged.
93//!
94//! ```rust
95//! let source = r#"
96//! oh christmas tree
97//! o tannenbaum
98//!
99//! oh christmas tree
100//! o tannenbaum
101//! five golden rings
102//! wait wrong song
103//! "#;
104//! let config = nccl::parse_config(&source).unwrap();
105//! assert_eq!(
106//! vec!["o tannenbaum", "five golden rings", "wait wrong song"],
107//! config["oh christmas tree"].values().collect::<Vec<_>>()
108//! );
109//! ```
110//!
111//! Results in one key "oh christmas tree" with three values. This property
112//! enables [`parse_config_with`] to merge two configurations together. Say if
113//! you wanted to enable an end user to be able to override some default values,
114//! first you would parse the user's configuration, and then parse the default
115//! on top of that. [`config::Config::value`] always returns the first value,
116//! which would be the user's value.
117//!
118//! Values can have quotes if you want escape codes or multiple lines.
119//! Supported escape sequences are newlines, carriage returns, both quotes, and
120//! line breaks.
121//!
122//! ```rust
123//! let source = r#"
124//! ## both single and double quotes work
125//! i can
126//! ## with parse_quoted(), expands to the rust string "show\nyou"
127//! 'show\nyou'
128//! ## backslash followed by newline replaces all following whitespace
129//! ## except newlines with one space character. expands to "the world"
130//! "the \
131//! world"
132//!
133//! ## results in a single value for jingle = jangle
134//! jingle
135//! jangle
136//! "jangle"
137//! 'jangle'
138//! "#;
139//! let config = nccl::parse_config(&source).unwrap();
140//! assert_eq!(1, config["jingle"].values().count());
141//! assert_eq!(
142//! Ok(vec![String::from("show\nyou"), String::from("the world")]),
143//! config["i can"]
144//! .children()
145//! .map(|value| value.parse_quoted())
146//! .collect::<Result<Vec<_>, _>>()
147//! );
148//! ```
149
150#![allow(clippy::tabs_in_doc_comments)]
151
152pub mod config;
153pub mod parser;
154pub mod scanner;
155
156pub use config::Config;
157
158use scanner::{Span, TokenKind};
159
160use std::str::Utf8Error;
161use std::string::FromUtf8Error;
162
163/// Parse a nccl configuration
164///
165/// e.g.
166/// ```
167/// # use nccl::*;
168/// // config.nccl:
169/// // server
170/// // domain
171/// // example.com
172/// // www.example.com
173/// // port
174/// // 80
175/// // 443
176/// // root
177/// // /var/www/html
178///
179/// // read the config file
180/// let content = std::fs::read_to_string("examples/config.nccl").unwrap();
181///
182/// // parse it
183/// let config = parse_config(&content).unwrap();
184///
185/// // look ma, no types!
186/// assert_eq!(config["server"]["root"].value(), Some("/var/www/html"));
187/// ```
188pub fn parse_config(content: &str) -> Result<Config, NcclError> {
189 let mut scanner = scanner::Scanner::new(content);
190 parser::parse(&mut scanner)
191}
192
193/// Parse a new nccl configuration on top of another
194///
195/// e.g.
196/// ```
197/// # use nccl::*;
198/// // user.nccl:
199/// // beans
200/// // four
201///
202/// // default.nccl:
203/// // frog
204/// // yes
205/// // beans
206/// // none
207///
208/// // result:
209/// // frog
210/// // yes
211/// // beans
212/// // four
213/// // none
214///
215/// // first get the user config
216/// let user = std::fs::read_to_string("examples/user.nccl").unwrap();
217/// let user_config = parse_config(&user).unwrap();
218///
219/// // then merge the default config on top of the user config
220/// let default = std::fs::read_to_string("examples/default.nccl").unwrap();
221/// let combined_config = parse_config_with(&user_config, &default).unwrap();
222///
223/// // with value(), the first key inserted is returned. since we read the user
224/// // config first, the user-supplied value is first, overriding the default.
225/// assert_eq!(combined_config["beans"].value(), Some("four"));
226/// // "beans" now has two values
227/// assert_eq!(combined_config["beans"].values().count(), 2);
228///
229/// // and the unmodified key remains
230/// assert_eq!(combined_config["frog"].value(), Some("yes"));
231/// ```
232pub fn parse_config_with<'a>(
233 config: &Config<'a>,
234 content: &'a str,
235) -> Result<Config<'a>, NcclError> {
236 let mut scanner = scanner::Scanner::new(content);
237 parser::parse_with(&mut scanner, config)
238}
239
240#[derive(Debug, PartialEq)]
241/// Errors that may occur while parsing
242pub enum NcclError {
243 /// An unexpected token was encountered.
244 UnexpectedToken {
245 /// The location of the token.
246 span: Span,
247 /// The kind of token we expected.
248 expected: TokenKind,
249 /// The kind of token we got.
250 got: TokenKind,
251 },
252 /// The string was not terminated before the end of the file.
253 UnterminatedString {
254 /// The line the string starts on.
255 start: usize,
256 },
257 /// There were non-comment characters after a quoted string.
258 TrailingCharacters {
259 /// The line the string ends on.
260 line: usize,
261 },
262 /// The escape code in the file was unknown.
263 ScanUnknownEscape {
264 /// The line of the code.
265 line: usize,
266 /// The column of the code.
267 column: usize,
268 /// The code itself.
269 escape: char,
270 },
271 /// The escape literal in the key was unknown. See [`crate::config::Config::parse_quoted`].
272 ParseUnknownEscape {
273 /// The escape code.
274 escape: char,
275 },
276 /// A utf-8 string could not be constructed.
277 Utf8 {
278 /// The error.
279 err: Utf8Error,
280 },
281}
282
283impl std::fmt::Display for NcclError {
284 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
285 match self {
286 NcclError::UnexpectedToken {
287 span,
288 expected,
289 got,
290 } => write!(
291 f,
292 "expected {:?}, got {:?} at {}:{}",
293 expected, got, span.line, span.column,
294 ),
295 NcclError::UnterminatedString { start } => {
296 write!(f, "unterminated string starting on line {}", start)
297 }
298 NcclError::TrailingCharacters { line } => {
299 write!(f, "characters after string on line {}", line)
300 }
301 NcclError::ScanUnknownEscape {
302 escape,
303 line,
304 column,
305 } => write!(f, "unknown escape {:?} at {}:{}", escape, line, column),
306 NcclError::ParseUnknownEscape { escape } => write!(f, "unknown escape {:?}", escape),
307 NcclError::Utf8 { err } => write!(f, "{}", err),
308 }
309 }
310}
311
312impl From<Utf8Error> for NcclError {
313 fn from(err: Utf8Error) -> Self {
314 NcclError::Utf8 { err }
315 }
316}
317
318impl From<FromUtf8Error> for NcclError {
319 fn from(err: FromUtf8Error) -> Self {
320 NcclError::Utf8 {
321 err: err.utf8_error(),
322 }
323 }
324}
325
326#[cfg(test)]
327mod test {
328 use super::*;
329 use std::fs::read_to_string;
330
331 #[test]
332 fn index() {
333 let content = read_to_string("examples/config.nccl").unwrap();
334 let config = parse_config(&content).unwrap();
335 assert_eq!(config["server"]["root"].value(), Some("/var/www/html"));
336 }
337
338 #[test]
339 fn values() {
340 let content = read_to_string("examples/config.nccl").unwrap();
341 let config = parse_config(&content).unwrap();
342 assert_eq!(
343 vec![80, 443],
344 config["server"]["port"]
345 .values()
346 .map(|port| port.parse::<u16>())
347 .collect::<Result<Vec<u16>, _>>()
348 .unwrap()
349 );
350 }
351
352 #[test]
353 fn value() {
354 let content = read_to_string("examples/long.nccl").unwrap();
355 let config = parse_config(&content).unwrap();
356 assert_eq!(config["bool too"].value().unwrap(), "false");
357 }
358
359 #[test]
360 fn duplicates() {
361 let content = read_to_string("examples/duplicates.nccl").unwrap();
362 let config = parse_config(&content).unwrap();
363 assert_eq!(
364 config["something"].values().collect::<Vec<_>>(),
365 vec!["with", "duplicates"]
366 );
367 }
368
369 #[test]
370 fn duplicates2() {
371 let content1 = read_to_string("examples/duplicates.nccl").unwrap();
372 let config1 = parse_config(&content1).unwrap();
373
374 let content2 = read_to_string("examples/duplicates2.nccl").unwrap();
375 let config2 = parse_config_with(&config1, &content2).unwrap();
376
377 assert_eq!(2, config2["something"].values().collect::<Vec<_>>().len());
378 }
379
380 #[test]
381 fn duplicates3() {
382 let content1 = read_to_string("examples/dup3.nccl").unwrap();
383 let config1 = parse_config(&content1).unwrap();
384
385 assert_eq!(
386 vec!["oh christmas tree", "o tannenbaum", "five golden rings"],
387 config1["oh christmas tree"].values().collect::<Vec<_>>()
388 );
389 }
390
391 #[test]
392 fn inherit() {
393 let sc = read_to_string("examples/inherit.nccl").unwrap();
394 let uc = read_to_string("examples/inherit2.nccl").unwrap();
395
396 let schema = parse_config(&sc).unwrap();
397 let user = parse_config_with(&schema, &uc).unwrap();
398
399 assert_eq!(3, user["hello"]["world"].values().collect::<Vec<_>>().len());
400 assert_eq!(
401 3,
402 user["sandwich"]["meat"].values().collect::<Vec<_>>().len()
403 );
404 }
405
406 #[test]
407 fn comments() {
408 let config = r#"x
409# comment
410 something
411 # comment again
412 bingo
413
414does this work?
415 who knows
416# I sure don't
417 is this a child?
418"#;
419 let config = parse_config(config).unwrap();
420
421 assert_eq!(config["x"]["something"].value().unwrap(), "bingo");
422 assert!(config["does this work?"].has_value("who knows"));
423 assert!(config["does this work?"].has_value("is this a child?"));
424 }
425
426 #[test]
427 fn all_of_em() {
428 let source = read_to_string("examples/all-of-em.nccl").unwrap();
429 let mut scanner = scanner::Scanner::new(&source);
430 let config = parser::parse(&mut scanner).unwrap();
431 assert_eq!(
432 Ok(vec![
433 String::from("i # j"),
434 String::from("k"),
435 String::from("m")
436 ]),
437 config["h"]
438 .children()
439 .map(|config| config.parse_quoted())
440 .collect::<Result<Vec<_>, _>>()
441 );
442 assert_eq!(Some(scanner::QuoteKind::Double), config["h"]["k"].quotes);
443 assert_eq!(Some(scanner::QuoteKind::Single), config["h"]["m"].quotes);
444 }
445
446 #[test]
447 fn escapes() {
448 let config = read_to_string("examples/escapes.nccl").unwrap();
449 let config = parse_config(&config).unwrap();
450 assert_eq!(
451 config["hello"].child().unwrap().parse_quoted().unwrap(),
452 "people of the earth\nhow's it doing?\""
453 );
454 assert_eq!(
455 config["hello"].child().unwrap().quotes,
456 Some(scanner::QuoteKind::Double)
457 );
458 }
459
460 #[test]
461 fn quote() {
462 let config = read_to_string("examples/quote.nccl").unwrap();
463 let config = parse_config(&config).unwrap();
464 assert_eq!(config["howdy"].values().collect::<Vec<_>>(), vec!["hello"]);
465 }
466
467 #[test]
468 fn fuzz() {
469 let dir = std::fs::read_dir("examples/fuzz/scan").unwrap();
470 for entry in dir {
471 let entry = entry.unwrap();
472 if entry
473 .path()
474 .file_name()
475 .unwrap()
476 .to_str()
477 .unwrap()
478 .starts_with("err")
479 {
480 println!("check scan bad: {}", entry.path().display());
481 let source = std::fs::read_to_string(entry.path()).unwrap();
482 let result = scanner::Scanner::new(&source).scan_all();
483 println!(" {:?}", result);
484 result.unwrap_err();
485 } else {
486 println!("check scan good: {}", entry.path().display());
487 let source = std::fs::read_to_string(entry.path()).unwrap();
488 let result = scanner::Scanner::new(&source).scan_all();
489 result.unwrap();
490 }
491 }
492
493 let dir = std::fs::read_dir("examples/fuzz/parse").unwrap();
494 for entry in dir {
495 let entry = entry.unwrap();
496 if entry
497 .path()
498 .file_name()
499 .unwrap()
500 .to_str()
501 .unwrap()
502 .starts_with("err")
503 {
504 println!("check parse bad: {}", entry.path().display());
505 let source = std::fs::read_to_string(entry.path()).unwrap();
506 let result = parse_config(&source);
507 println!(" {:#?}", result);
508 result.unwrap_err();
509 } else {
510 println!("check parse good: {}", entry.path().display());
511 let source = std::fs::read_to_string(entry.path()).unwrap();
512 let result = parse_config(&source);
513 println!(" {:#?}", result);
514 result.unwrap();
515 }
516 }
517 }
518}