Skip to main content

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}