lrcat/
lron.rs

1/*
2 This Source Code Form is subject to the terms of the Mozilla Public
3 License, v. 2.0. If a copy of the MPL was not distributed with this
4 file, You can obtain one at http://mozilla.org/MPL/2.0/.
5*/
6
7//! lron stands for Lightroom Object Notation, specific to Lightroom
8//! that is found throughout the catalog database to store arbitrary
9//! but structured data.
10//!
11//! lron looks like plist (before XML) or JSON, but doesn't match
12//! either syntax.
13//!
14//! Note: I couldn't figure out what this format was called, so I
15//! couldn't reuse an existing parser. If you have a better idea,
16//! please, let me know.
17//!
18//! Note2: The crate [`agprefs`](https://crates.io/crates/agprefs)
19//! call it `agprefs`.
20//!
21//! It has the form
22//! ```json
23//! name = {
24//!   object = {
25//!     x = 1.3,
26//!     string = "some text",
27//!   },
28//! }
29//! ```
30//!
31//! The text is parsed using peg.
32//!
33//! You obtain the expression from the text by the following:
34//! ```
35//! use lrcat::lron;
36//!
37//! let lron_text = "name = {}"; // load the text in the string
38//!
39//! if let Ok(object) = lron::Object::from_string(lron_text) {
40//!     // do your stuff with it
41//! }
42//! ```
43
44/// Lron Value
45#[derive(Clone, Debug, PartialEq)]
46pub enum Value {
47    Dict(Vec<Object>),
48    Str(String),
49    ZStr(String),
50    Int(i32),
51    Float(f64),
52    Bool(bool),
53}
54
55impl Value {
56    /// Try to convert the value into a number of type T.  This is
57    /// because number are untyped in Lron, and the parser will manage
58    /// float or int.  Instead of having a generic Number type, it's
59    /// better this way.
60    pub fn to_number<T>(&self) -> Option<T>
61    where
62        T: std::convert::From<i32> + std::convert::From<f64>,
63    {
64        match *self {
65            Self::Int(i) => Some(i.into()),
66            Self::Float(f) => Some(f.into()),
67            _ => None,
68        }
69    }
70}
71
72/// A key/value pair.
73#[derive(Clone, Debug, PartialEq)]
74pub struct Pair {
75    pub key: String,
76    pub value: Value,
77}
78
79/// Lron Object
80#[derive(Clone, Debug, PartialEq)]
81pub enum Object {
82    Dict(Vec<Object>),
83    Pair(Pair),
84    Str(String),
85    ZStr(String),
86    Int(i32),
87}
88
89/// Alias result type for parsing a Lron object.
90type Result<T> = std::result::Result<T, peg::error::ParseError<peg::str::LineCol>>;
91
92impl Object {
93    /// Create an object from a string
94    pub fn from_string(s: &str) -> Result<Object> {
95        lron::root(s)
96    }
97}
98
99// lron stand for Lightroom Object Notation
100// Some sort of JSON specific to Lightroom
101//
102// lron data syntax is defined in this PEG grammar.
103peg::parser! {grammar lron() for str {
104
105use std::str::FromStr;
106
107pub rule root() -> Object
108        = key:identifier() _() "=" _() value:array() _()
109    { Object::Pair(Pair{key, value: Value::Dict(value)}) }
110
111rule array() -> Vec<Object>
112        = "{" _() v:(object() ** (_() "," _())) _()(",")? _() "}" { v }
113
114rule object() -> Object
115        = a:array() { Object::Dict(a) } /
116        p:pair() { Object::Pair(p) } /
117        s:string_literal() { Object::Str(s) } /
118        z:zstr() { Object::ZStr(z) } /
119        n:int() { Object::Int(n) }
120
121rule pair() -> Pair
122        = key:identifier() _() "=" _() value:value() { Pair { key, value } } /
123        "[" key:string_literal() "]" _() "=" _() value:value()
124    { Pair { key, value } }
125
126rule value() -> Value
127        = i:int() { Value::Int(i) } /
128        b:bool() { Value::Bool(b) } /
129        f:float() { Value::Float(f) } /
130        s:string_literal() { Value::Str(s) } /
131        a:array() { Value::Dict(a) } /
132        z:zstr() { Value::ZStr(z) }
133
134rule int() -> i32
135        = n:$("-"? ['0'..='9']+) !"." { i32::from_str(n).unwrap() } / expected!("integer")
136
137rule bool() -> bool
138        = "true" { true } / "false" { false }
139
140rule float() -> f64
141        = f:$("-"? ['0'..='9']+ "." ['0'..='9']+) { f64::from_str(f).unwrap() } / expected!("floating point")
142
143rule identifier() -> String
144        = s:$(['a'..='z' | 'A'..='Z' | '0'..='9' | '_']+) { s.to_owned() } / expected!("identifier")
145
146// String escape, either literal EOL or quotes.
147rule escape() -> &'static str
148        = "\\\"" { "\"" } / "\\\n" { "\n" }
149
150// String literal can be escaped.
151rule string_literal() -> String
152        = "\"" s:((escape() / $(!['"'][_]))*) "\"" { s.join("") }
153
154rule zstr() -> String
155        = "ZSTR" _() s:string_literal() { s }
156
157rule _() = quiet!{[' ' | '\r' | '\n' | '\t']*}
158
159}}
160
161#[test]
162fn test_parser() {
163    const DATA: &str = include_str!("../data/test_lron");
164    let r = Object::from_string(DATA);
165
166    assert!(r.is_ok());
167    let o = r.unwrap();
168
169    assert!(matches!(o, Object::Pair(_)));
170    if let Object::Pair(ref p) = o {
171        assert_eq!(p.key, "s");
172        assert!(matches!(p.value, Value::Dict(_)));
173
174        if let Value::Dict(ref d) = p.value {
175            assert_eq!(d.len(), 2);
176            assert!(matches!(d[0], Object::Dict(_)));
177            if let Object::Dict(ref d) = d[0] {
178                assert_eq!(d.len(), 5);
179                assert!(matches!(d[0], Object::Pair(_)));
180                assert!(matches!(d[1], Object::Pair(_)));
181                assert!(matches!(d[2], Object::Pair(_)));
182                assert!(matches!(d[3], Object::Pair(_)));
183                assert!(matches!(d[4], Object::Pair(_)));
184                if let Object::Pair(ref p) = d[4] {
185                    assert_eq!(p.key, "someOther");
186                    if let Value::Str(value) = &p.value {
187                        let r2 = Object::from_string(value);
188                        assert!(r2.is_ok());
189                    }
190                    assert_eq!(
191                        p.value,
192                        Value::Str(
193                            "anObject = {\n\
194                         key = \"lr\",\n\
195                         }\n"
196                            .to_owned()
197                        )
198                    );
199                }
200            }
201            assert!(matches!(d[1], Object::Pair(_)));
202            if let Object::Pair(ref p) = d[1] {
203                assert_eq!(p.key, "combine");
204                assert_eq!(p.value, Value::Str("intersect".to_owned()));
205            }
206        }
207    } else {
208        unreachable!();
209    }
210}