ron_edit/
lib.rs

1//! Nom parser trying to preserve all information
2//!
3//! Probably the "AST" structure will be reworked quite a bit.
4use std::fmt::{self, Display, Formatter};
5
6use derive_more::Display;
7use macro_rules_attribute::{apply, attribute_alias};
8use nom::branch::*;
9use nom::bytes::complete::*;
10use nom::character::complete::*;
11use nom::combinator::*;
12use nom::error::{context, VerboseError};
13use nom::multi::*;
14use nom::sequence::*;
15use nom::{Finish, Parser};
16#[cfg(test)]
17use serde::Serialize;
18use unicode_ident::{is_xid_continue, is_xid_start};
19pub use value::{Char, Int, List, Map, MapItem, NamedField, Str, Struct, Tuple, Value};
20
21type Error<'a> = VerboseError<&'a str>;
22type IResult<'a, T> = nom::IResult<&'a str, T, Error<'a>>;
23
24mod value;
25
26attribute_alias! {
27    #[apply(ast)] =
28        #[cfg_attr(test, derive(Serialize))]
29        #[derive(Debug, PartialEq)];
30}
31
32#[apply(ast)]
33pub struct File<'s> {
34    pub extentions: Vec<WsLead<'s, Attribute<'s>>>,
35    pub value: WsLead<'s, Value<'s>>,
36    pub trailing_ws: Whitespace<'s>,
37}
38
39impl Display for File<'_> {
40    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
41        self.extentions.iter().try_for_each(|e| write!(f, "{e}"))?;
42        write!(f, "{}{}", self.value, self.trailing_ws)
43    }
44}
45
46impl<'s> TryFrom<&'s str> for File<'s> {
47    type Error = String;
48
49    fn try_from(s: &'s str) -> Result<Self, Self::Error> {
50        match file(s).finish() {
51            Ok(("", file)) => Ok(file),
52            Ok((rem, _)) => Err(format!("unexpected: {rem}")),
53            Err(e) => Err(e.to_string()),
54        }
55    }
56}
57
58#[apply(ast)]
59#[derive(Display)]
60#[display(fmt = "#{after_pound}!\
61           {after_exclamation}[{after_bracket}enable{after_enable}({extentions}){after_paren}]")]
62pub struct Attribute<'s> {
63    pub after_pound: Whitespace<'s>,
64    pub after_exclamation: Whitespace<'s>,
65    pub after_bracket: Whitespace<'s>,
66    pub after_enable: Whitespace<'s>,
67    pub extentions: Separated<'s, &'s str>,
68    pub after_paren: Whitespace<'s>,
69}
70
71#[apply(ast)]
72pub struct WsLead<'s, T> {
73    pub leading: Whitespace<'s>,
74    pub content: T,
75}
76impl<T: Display> Display for WsLead<'_, T> {
77    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
78        write!(f, "{}{}", self.leading, self.content)
79    }
80}
81fn ws_lead<'a, T>(
82    t: impl Parser<&'a str, T, Error<'a>>,
83) -> impl Parser<&'a str, WsLead<'a, T>, Error<'a>> {
84    map(pair(ws, t), |(leading, content)| WsLead {
85        leading,
86        content,
87    })
88}
89
90#[apply(ast)]
91#[derive(Default)]
92pub struct WsFollowed<'s, T> {
93    pub content: T,
94    pub following: Whitespace<'s>,
95}
96impl<T: Display> Display for WsFollowed<'_, T> {
97    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
98        write!(f, "{}{}", self.content, self.following)
99    }
100}
101fn ws_followed<'a, T>(
102    t: impl Parser<&'a str, T, Error<'a>>,
103) -> impl Parser<&'a str, WsFollowed<'a, T>, Error<'a>> {
104    map(pair(t, ws), |(content, following)| WsFollowed {
105        content,
106        following,
107    })
108}
109
110#[apply(ast)]
111#[derive(Default)]
112pub struct WsWrapped<'s, T> {
113    pub leading: Whitespace<'s>,
114    pub content: T,
115    pub following: Whitespace<'s>,
116}
117impl<T: Display> Display for WsWrapped<'_, T> {
118    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
119        write!(f, "{}{}{}", self.leading, self.content, self.following)
120    }
121}
122fn ws_wrapped<'a, T>(
123    t: impl Parser<&'a str, T, Error<'a>>,
124) -> impl Parser<&'a str, WsWrapped<'a, T>, Error<'a>> {
125    map(tuple((ws, t, ws)), |(leading, content, following)| {
126        WsWrapped {
127            leading,
128            content,
129            following,
130        }
131    })
132}
133
134#[apply(ast)]
135pub enum Ws<'s> {
136    LineComment(&'s str),
137    Space(&'s str),
138    BlockComment(&'s str),
139}
140impl Display for Ws<'_> {
141    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
142        match self {
143            Ws::LineComment(c) => write!(f, "//{c}"),
144            Ws::Space(s) => write!(f, "{s}"),
145            Ws::BlockComment(c) => write!(f, "/*{c}*/"),
146        }
147    }
148}
149impl Ws<'_> {
150    pub fn is_comment(&self) -> bool {
151        !matches!(self, Self::Space(_))
152    }
153
154    pub fn is_line_comment(&self) -> bool {
155        matches!(self, Self::LineComment(_))
156    }
157}
158
159#[apply(ast)]
160#[derive(Default)]
161pub struct Whitespace<'s>(pub Vec<Ws<'s>>);
162
163impl Display for Whitespace<'_> {
164    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
165        self.0.iter().try_for_each(|ws| write!(f, "{ws}"))
166    }
167}
168
169#[apply(ast)]
170pub struct Separated<'s, T> {
171    pub values: Vec<WsWrapped<'s, T>>,
172    /// When formatting with [`Display`] comma is only shown when
173    /// [`Self::values`] is not empty.
174    pub trailing_comma: bool,
175    pub trailing_ws: Whitespace<'s>,
176}
177
178impl<T: Display> Display for Separated<'_, T> {
179    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
180        let Self {
181            values,
182            trailing_comma,
183            trailing_ws,
184        } = self;
185        let Some((last, rest)) = values.split_last() else {
186            return write!(f, "{trailing_ws}")
187        };
188        rest.iter().try_for_each(|i| write!(f, "{i},"))?;
189        write!(f, "{last}")?;
190        if *trailing_comma {
191            write!(f, ",")?;
192        }
193        write!(f, "{trailing_ws}")
194    }
195}
196
197#[apply(ast)]
198pub struct ExtentionIdent {}
199
200fn file(input: &str) -> IResult<File> {
201    map(
202        tuple((
203            context("attributes", many0(ws_lead(extention_attr))),
204            context("value", ws_lead(value::value)),
205            ws,
206        )),
207        |(extentions, value, trailing_ws)| File {
208            extentions,
209            value,
210            trailing_ws,
211        },
212    )(input)
213}
214
215#[allow(unused)]
216fn dbg_dmp<'a, O>(
217    context: impl Display,
218    mut p: impl Parser<&'a str, O, Error<'a>>,
219) -> impl FnMut(&'a str) -> IResult<O> {
220    move |s: &str| match p.parse(s) {
221        Err(nom::Err::Error(c)) => {
222            println!("{}: Parsing Error: {} at:\n{:?}", context, c, s);
223            Err(nom::Err::Error(c))
224        }
225        Err(e) => {
226            println!("{}: Error({}) at:\n{:?}", context, e, s);
227            Err(e)
228        }
229        a => a,
230    }
231}
232
233fn extention_attr(input: &str) -> IResult<Attribute> {
234    map(
235        tuple((
236            char('#'),
237            ws,
238            char('!'),
239            ws,
240            char('['),
241            ws,
242            tag("enable"),
243            ws,
244            char('('),
245            separated(ident),
246            char(')'),
247            ws,
248            char(']'),
249        )),
250        |(
251            _,
252            after_pound,
253            _,
254            after_exclamation,
255            _,
256            after_bracket,
257            _,
258            after_ident,
259            _,
260            extentions,
261            _,
262            after_paren,
263            _,
264        )| {
265            Attribute {
266                after_pound,
267                after_exclamation,
268                extentions,
269                after_bracket,
270                after_enable: after_ident,
271                after_paren,
272            }
273        },
274    )(input)
275}
276
277fn ident(input: &str) -> IResult<&str> {
278    alt((
279        recognize(pair(
280            tag("r#"),
281            cut(many1(alt((take_while1(is_xid_continue), is_a("+-."))))),
282        )),
283        recognize(pair(
284            // TODO make issue about adding a take_one(fn(char)->bool)
285            alt((tag("_"), take_while_m_n(1, 1, is_xid_start))),
286            take_while(is_xid_continue),
287        )),
288    ))(input)
289}
290
291fn block_comment(input: &str) -> IResult<Ws> {
292    map(
293        delimited(
294            tag("/*"),
295            recognize(many0(alt((
296                is_not("*/"),
297                recognize(block_comment),
298                take_until1("*/"),
299            )))),
300            tag("*/"),
301        ),
302        Ws::BlockComment,
303    )(input)
304}
305
306fn ws(input: &str) -> IResult<Whitespace> {
307    map(
308        many0(alt((
309            map(is_a("\n\t\r "), Ws::Space),
310            map(
311                preceded(tag("//"), recognize(pair(is_not("\n"), line_ending))),
312                Ws::LineComment,
313            ),
314            block_comment,
315        ))),
316        Whitespace,
317    )(input)
318}
319
320fn separated<'a, T, P: Parser<&'a str, T, Error<'a>>>(
321    p: P,
322) -> impl FnMut(&'a str) -> IResult<Separated<T>> {
323    context(
324        "separated",
325        map(
326            tuple((
327                separated_list0(char(','), ws_wrapped(p)),
328                opt(char(',')),
329                ws,
330            )),
331            |(values, trailing_comma, trailing_ws)| Separated {
332                values,
333                trailing_comma: trailing_comma.is_some(),
334                trailing_ws,
335            },
336        ),
337    )
338}
339
340struct FormatOption<'a, T>(&'a Option<T>);
341impl<T: Display> Display for FormatOption<'_, T> {
342    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
343        match self.0 {
344            Some(t) => write!(f, "{t}"),
345            None => Ok(()),
346        }
347    }
348}
349
350#[cfg(test)]
351mod test {
352    use include_dir::include_dir;
353
354    use super::*;
355
356    const INPUTS: include_dir::Dir = include_dir!("$CARGO_MANIFEST_DIR/tests/input");
357
358    #[test]
359    fn whitespace() {
360        assert_eq!(
361            block_comment("/* a */ ").finish().unwrap(),
362            (" ", Ws::BlockComment(" a ".into()))
363        );
364        assert_eq!(
365            block_comment("/* a /* inner */ */ ").finish().unwrap(),
366            (" ", Ws::BlockComment(" a /* inner */ ".into()))
367        );
368        assert_eq!(
369            ws("/* *//* a /* inner */ */").finish().unwrap(),
370            (
371                "",
372                Whitespace(vec![
373                    Ws::BlockComment(" ".into()),
374                    Ws::BlockComment(" a /* inner */ ".into())
375                ])
376            )
377        );
378    }
379
380    #[test]
381    fn ensure_valid_ron() {
382        for file in INPUTS.entries() {
383            let _: ron::Value = ron::de::from_bytes(file.as_file().unwrap().contents()).unwrap();
384        }
385    }
386
387    #[test]
388    fn parse() {
389        for file in INPUTS.entries() {
390            let contents = file.as_file().unwrap().contents_utf8().unwrap();
391            let ron: File = contents.try_into().map_err(|e| eprintln!("{e}")).unwrap();
392
393            assert_eq!(ron.to_string(), contents);
394
395            insta::with_settings!({snapshot_path => "../tests/snapshots"},{
396                insta::assert_ron_snapshot!(file.path().display().to_string(), ron);
397            })
398        }
399    }
400}