rust_lcm_codegen/
parser.rs

1//! LCM type definition file parser
2use nom::{
3    branch::alt,
4    bytes::complete::{tag, tag_no_case, take_until, take_while1},
5    character::complete::{digit1, hex_digit1, multispace1, not_line_ending, oct_digit1},
6    combinator::{map, map_res, opt, recognize, value, verify},
7    multi::{many0, many1, separated_nonempty_list},
8    sequence::{preceded, separated_pair, terminated, tuple},
9    IResult,
10};
11
12#[derive(Debug, Eq, PartialEq, Copy, Clone)]
13pub enum PrimitiveType {
14    Int8,
15    Int16,
16    Int32,
17    Int64,
18    Float,
19    Double,
20    String,
21    Boolean,
22    Byte,
23}
24
25#[derive(Debug, Eq, PartialEq, Clone)]
26pub enum ArrayDimension {
27    Static { size: u32 },
28    Dynamic { field_name: String },
29}
30
31#[derive(Debug, Eq, PartialEq, Clone)]
32pub struct ArrayType {
33    pub item_type: Box<Type>,
34    pub dimensions: Vec<ArrayDimension>,
35}
36
37#[derive(Debug, Eq, PartialEq, Clone)]
38pub struct StructType {
39    pub namespace: Option<String>,
40    pub name: String,
41}
42
43#[derive(Debug, Eq, PartialEq, Clone)]
44pub enum Type {
45    Primitive(PrimitiveType),
46    Array(ArrayType),
47    Struct(StructType),
48}
49
50#[derive(Debug, Eq, PartialEq, Clone)]
51pub enum ConstValue {
52    Int8(i8),
53    Int16(i16),
54    Int32(i32),
55    Int64(i64),
56    // Keep floats and doubles as strings, since f32/f64 don't have Eq defined on them
57    Float(String),
58    Double(String),
59    Boolean(bool),
60    Byte(u8),
61}
62
63#[derive(Debug, Eq, PartialEq, Clone)]
64pub struct Field {
65    pub name: String,
66    pub ty: Type,
67}
68
69#[derive(Debug, Eq, PartialEq, Clone)]
70pub struct Const {
71    pub name: String,
72    pub ty: PrimitiveType,
73    pub value: ConstValue,
74}
75
76#[derive(Debug, Eq, PartialEq, Clone)]
77pub enum StructMember {
78    Field(Field),
79    Const(Const),
80}
81
82#[derive(Debug, Eq, PartialEq, Clone)]
83pub struct Struct {
84    pub name: String,
85    pub members: Vec<StructMember>,
86}
87
88#[derive(Debug, Eq, PartialEq, Clone)]
89pub struct Schema {
90    pub package: Option<String>,
91    pub structs: Vec<Struct>,
92}
93
94/// Match a C-style comment
95///
96/// ```
97/// # use nom::{Err, error::ErrorKind, Needed};
98/// use rust_lcm_codegen::parser::block_comment;
99///
100/// assert_eq!(block_comment("/* test */"), Ok(("", " test ")));
101/// ```
102pub fn block_comment(input: &str) -> IResult<&str, &str> {
103    let (input, _) = tag("/*")(input)?;
104    let (input, comment) = take_until("*/")(input)?;
105    let (input, _) = tag("*/")(input)?;
106
107    Ok((input, comment))
108}
109
110/// Match a C++-style comment
111///
112/// ```
113/// # use nom::{Err, error::ErrorKind, Needed};
114/// use rust_lcm_codegen::parser::line_comment;
115///
116/// assert_eq!(line_comment("// test\nmore"), Ok(("\nmore", " test")));
117/// ```
118pub fn line_comment(input: &str) -> IResult<&str, &str> {
119    let (input, _) = tag("//")(input)?;
120    let (input, comment) = not_line_ending(input)?;
121
122    Ok((input, comment))
123}
124
125/// Whitespace parser. This includes regular spaces, newlines, and comments.
126pub fn ws(input: &str) -> IResult<&str, ()> {
127    value((), many1(alt((block_comment, line_comment, multispace1))))(input)
128}
129
130/// Match a comma, optionally surrounded by spaces
131///
132/// ```
133/// # use nom::{Err, error::ErrorKind, Needed};
134/// use rust_lcm_codegen::parser::spaced_comma;
135///
136/// assert_eq!(spaced_comma(","), Ok(("", ",")));
137/// assert_eq!(spaced_comma(" ,\t"), Ok(("", " ,\t")));
138/// assert_eq!(spaced_comma("x"), Err(Err::Error(("x", ErrorKind::Tag))));
139/// ```
140pub fn spaced_comma(input: &str) -> IResult<&str, &str> {
141    recognize(tuple((opt(ws), tag(","), opt(ws))))(input)
142}
143
144/// Names that can be used for structs, packages, or fields
145/// ```
146/// # use nom::{Err, error::ErrorKind, Needed};
147/// use rust_lcm_codegen::parser::ident;
148///
149/// assert_eq!(ident("foo"), Ok(("", "foo")));
150/// assert_eq!(ident("foo_bar"), Ok(("", "foo_bar")));
151/// assert_eq!(ident(""), Err(Err::Error(("", ErrorKind::TakeWhile1))));
152/// ```
153pub fn ident(input: &str) -> IResult<&str, &str> {
154    recognize(take_while1(|c: char| c.is_alphanumeric() || c == '_'))(input)
155}
156
157/// Parse an LCM primitive type
158///
159/// ```
160/// # use nom::{Err, error::ErrorKind, Needed};
161/// use rust_lcm_codegen::parser::{primitive_type, PrimitiveType};
162///
163/// assert_eq!(primitive_type("int8_t"), Ok(("", PrimitiveType::Int8)));
164/// assert_eq!(primitive_type("int16_t"), Ok(("", PrimitiveType::Int16)));
165/// assert_eq!(primitive_type("int32_t"), Ok(("", PrimitiveType::Int32)));
166/// assert_eq!(primitive_type("int64_t"), Ok(("", PrimitiveType::Int64)));
167/// assert_eq!(primitive_type("float"), Ok(("", PrimitiveType::Float)));
168/// assert_eq!(primitive_type("double"), Ok(("", PrimitiveType::Double)));
169/// assert_eq!(primitive_type("string"), Ok(("", PrimitiveType::String)));
170/// assert_eq!(primitive_type("boolean"), Ok(("", PrimitiveType::Boolean)));
171/// assert_eq!(primitive_type("byte"), Ok(("", PrimitiveType::Byte)));
172///
173/// assert_eq!(primitive_type(""), Err(Err::Error(("", ErrorKind::Tag))));
174/// assert_eq!(primitive_type("foo"), Err(Err::Error(("foo", ErrorKind::Tag))));
175/// ```
176pub fn primitive_type(input: &str) -> IResult<&str, PrimitiveType> {
177    alt((
178        map(tag("int8_t"), |_| PrimitiveType::Int8),
179        map(tag("int16_t"), |_| PrimitiveType::Int16),
180        map(tag("int32_t"), |_| PrimitiveType::Int32),
181        map(tag("int64_t"), |_| PrimitiveType::Int64),
182        map(tag("float"), |_| PrimitiveType::Float),
183        map(tag("double"), |_| PrimitiveType::Double),
184        map(tag("string"), |_| PrimitiveType::String),
185        map(tag("boolean"), |_| PrimitiveType::Boolean),
186        map(tag("byte"), |_| PrimitiveType::Byte),
187    ))(input)
188}
189
190/// Parse the type part of a field decl.
191/// ```
192/// # use nom::{Err, error::ErrorKind, Needed};
193/// use rust_lcm_codegen::parser::{field_type, Type, PrimitiveType, StructType};
194///
195/// assert_eq!(field_type("int8_t"), Ok(("", Type::Primitive(PrimitiveType::Int8))));
196///
197/// assert_eq!(field_type("foo.bar"),
198///            Ok(("", Type::Struct(StructType { namespace: Some("foo".to_string()), name: "bar".to_string() }))));
199///
200/// assert_eq!(field_type("foo"),
201///            Ok(("", Type::Struct(StructType { namespace: None, name: "foo".to_string() }))));
202///
203/// ```
204pub fn field_type(input: &str) -> IResult<&str, Type> {
205    alt((
206        map(primitive_type, Type::Primitive),
207        map(separated_pair(ident, tag("."), ident), |(ns, n)| {
208            Type::Struct(StructType {
209                namespace: Some(ns.to_string()),
210                name: n.to_string(),
211            })
212        }),
213        map(ident, |n| {
214            Type::Struct(StructType {
215                namespace: None,
216                name: n.to_string(),
217            })
218        }),
219    ))(input)
220}
221
222fn array_dimension(input: &str) -> IResult<&str, ArrayDimension> {
223    let (input, _) = tag("[")(input)?;
224    let (input, _) = opt(ws)(input)?;
225
226    let (input, dim) = alt((
227        map(map_res(digit1, |s: &str| s.parse::<u32>()), |size| {
228            ArrayDimension::Static { size }
229        }),
230        map(ident, |s| ArrayDimension::Dynamic {
231            field_name: s.to_string(),
232        }),
233    ))(input)?;
234
235    let (input, _) = opt(ws)(input)?;
236    let (input, _) = tag("]")(input)?;
237    let (input, _) = opt(ws)(input)?;
238
239    Ok((input, dim))
240}
241
242/// Parse a field declaration, inside a struct, including array dimensions.
243/// (doesn't handle the semicolon or preceding whitespace)
244///
245/// ```
246/// # use nom::{Err, error::ErrorKind, Needed};
247/// use rust_lcm_codegen::parser::{field_decl, Field, PrimitiveType, Type, StructType, ArrayDimension, ArrayType};
248///
249/// assert_eq!(
250///     field_decl("int8_t foo"),
251///     Ok((
252///         "",
253///         Field {
254///             name: "foo".to_owned(),
255///             ty: Type::Primitive(PrimitiveType::Int8)
256///         }
257///     ))
258/// );
259///
260/// assert_eq!(
261///     field_decl("ns.name foo[dim][2]"),
262///     Ok((
263///         "",
264///         Field {
265///           name: "foo".to_owned(),
266///           ty: Type::Array(ArrayType {
267///             item_type: Box::new(
268///               Type::Struct(StructType { namespace: Some("ns".to_string()),
269///                                         name: "name".to_string() })
270///             ),
271///             dimensions: vec![
272///               ArrayDimension::Dynamic { field_name: "dim".to_string() },
273///               ArrayDimension::Static { size: 2 },
274///             ]
275///           })
276///         }
277///     ))
278/// );
279///
280/// assert_eq!(field_decl(""), Err(Err::Error(("", ErrorKind::TakeWhile1))));
281/// assert_eq!(field_decl("int8_t *!@"), Err(Err::Error(("*!@", ErrorKind::TakeWhile1))));
282///
283/// ```
284pub fn field_decl(input: &str) -> IResult<&str, Field> {
285    let (input, mut ty) = field_type(input)?;
286    let (input, _) = ws(input)?;
287    let (input, name) = ident(input)?;
288    let (input, dims) = many0(array_dimension)(input)?;
289
290    if !dims.is_empty() {
291        ty = Type::Array(ArrayType {
292            item_type: Box::new(ty),
293            dimensions: dims,
294        });
295    }
296
297    Ok((
298        input,
299        Field {
300            ty,
301            name: name.to_owned(),
302        },
303    ))
304}
305
306/// Recognize something that looks like an integer, positive or negative. Return a tuple of the
307/// content part of the string and the radix.
308fn recognize_int(input: &str) -> IResult<&str, (String, u32)> {
309    let (input, minus) = opt(tag("-"))(input)?;
310    let minus = minus.unwrap_or("");
311
312    let (input, radix) = alt((
313        value(16, tag_no_case("0x")),
314        value(8, tag("0")),
315        value(10, tag("")),
316    ))(input)?;
317
318    let (input, body) = match radix {
319        16 => hex_digit1(input)?,
320        10 => digit1(input)?,
321        8 => oct_digit1(input)?,
322        _ => unreachable!(),
323    };
324
325    Ok((input, (format!("{}{}", minus, body), radix)))
326}
327
328/// Recognize something that looks like an float, positive or negative. Keep it a string.
329fn recognize_float(input: &str) -> IResult<&str, &str> {
330    recognize(tuple((
331        opt(tag("-")),
332        digit1,
333        opt(tuple((tag("."), digit1, opt(tuple((tag("e"), digit1)))))),
334    )))(input)
335}
336
337/// Parse a const value of the given type.
338///
339/// ```
340/// # use nom::{Err, error::ErrorKind, Needed};
341/// use rust_lcm_codegen::parser::{const_value, ConstValue, PrimitiveType};
342///
343/// assert_eq!(const_value(PrimitiveType::Int8,    "42"), Ok(("", ConstValue::Int8(   42))));
344/// assert_eq!(const_value(PrimitiveType::Int8,   "-42"), Ok(("", ConstValue::Int8(  -42))));
345/// assert_eq!(const_value(PrimitiveType::Int8,  "0x2f"), Ok(("", ConstValue::Int8( 0x2f))));
346/// assert_eq!(const_value(PrimitiveType::Int8, "-0x2f"), Ok(("", ConstValue::Int8(-0x2f))));
347/// assert_eq!(const_value(PrimitiveType::Int8,   "022"), Ok(("", ConstValue::Int8( 0o22))));
348/// assert_eq!(const_value(PrimitiveType::Int8,  "-022"), Ok(("", ConstValue::Int8(-0o22))));
349/// assert_eq!(const_value(PrimitiveType::Int8,  "1024"),
350///            Err(Err::Error(("1024", ErrorKind::MapRes))));
351///
352/// assert_eq!(const_value(PrimitiveType::Int16,  "1024"), Ok(("", ConstValue::Int16( 1024))));
353/// assert_eq!(const_value(PrimitiveType::Int16, "-1024"), Ok(("", ConstValue::Int16(-1024))));
354/// assert_eq!(const_value(PrimitiveType::Int16, "32768"),
355///            Err(Err::Error(("32768", ErrorKind::MapRes))));
356///
357/// assert_eq!(const_value(PrimitiveType::Int32,  "32768"), Ok(("", ConstValue::Int32( 32768))));
358/// assert_eq!(const_value(PrimitiveType::Int32, "-32768"), Ok(("", ConstValue::Int32(-32768))));
359/// assert_eq!(const_value(PrimitiveType::Int32, "2147483648"),
360///            Err(Err::Error(("2147483648", ErrorKind::MapRes))));
361///
362/// assert_eq!(const_value(PrimitiveType::Int64,  "2147483648"), Ok(("", ConstValue::Int64( 2147483648))));
363/// assert_eq!(const_value(PrimitiveType::Int64, "-2147483648"), Ok(("", ConstValue::Int64(-2147483648))));
364/// assert_eq!(const_value(PrimitiveType::Int64, "92233720368547758073"),
365///            Err(Err::Error(("92233720368547758073", ErrorKind::MapRes))));
366///
367/// assert_eq!(const_value(PrimitiveType::Float,           "10"), Ok(("", ConstValue::Float(         "10".to_owned()))));
368/// assert_eq!(const_value(PrimitiveType::Float,        "10.35"), Ok(("", ConstValue::Float(      "10.35".to_owned()))));
369/// assert_eq!(const_value(PrimitiveType::Float,       "-10.35"), Ok(("", ConstValue::Float(     "-10.35".to_owned()))));
370/// assert_eq!(const_value(PrimitiveType::Float,     "10.35e12"), Ok(("", ConstValue::Float(   "10.35e12".to_owned()))));
371/// assert_eq!(const_value(PrimitiveType::Float,    "-10.35e12"), Ok(("", ConstValue::Float(  "-10.35e12".to_owned()))));
372/// assert_eq!(const_value(PrimitiveType::Float,  "10.35e12000"), Ok(("", ConstValue::Float("10.35e12000".to_owned()))));
373/// assert_eq!(const_value(PrimitiveType::Float, "asdf"),
374///            Err(Err::Error(("asdf", ErrorKind::Digit))));
375///
376/// assert_eq!(const_value(PrimitiveType::Double,           "10"), Ok(("", ConstValue::Double(         "10".to_owned()))));
377/// assert_eq!(const_value(PrimitiveType::Double,        "10.35"), Ok(("", ConstValue::Double(      "10.35".to_owned()))));
378/// assert_eq!(const_value(PrimitiveType::Double,       "-10.35"), Ok(("", ConstValue::Double(     "-10.35".to_owned()))));
379/// assert_eq!(const_value(PrimitiveType::Double,     "10.35e12"), Ok(("", ConstValue::Double(   "10.35e12".to_owned()))));
380/// assert_eq!(const_value(PrimitiveType::Double,    "-10.35e12"), Ok(("", ConstValue::Double(  "-10.35e12".to_owned()))));
381/// assert_eq!(const_value(PrimitiveType::Double,  "10.35e12000"), Ok(("", ConstValue::Double("10.35e12000".to_owned()))));
382/// assert_eq!(const_value(PrimitiveType::Double, "asdf"),
383///            Err(Err::Error(("asdf", ErrorKind::Digit))));
384///
385/// assert_eq!(const_value(PrimitiveType::Byte,   "42"), Ok(("", ConstValue::Byte( 42))));
386/// assert_eq!(const_value(PrimitiveType::Byte,  "-42"), Err(Err::Error(("-42", ErrorKind::Digit))));
387/// assert_eq!(const_value(PrimitiveType::Byte, "1024"), Err(Err::Error(("1024", ErrorKind::MapRes))));
388///
389/// ```
390pub fn const_value(ty: PrimitiveType, input: &str) -> IResult<&str, ConstValue> {
391    match ty {
392        PrimitiveType::Int8 => map(
393            map_res(recognize_int, |(s, radix)| i8::from_str_radix(&s, radix)),
394            ConstValue::Int8,
395        )(input),
396        PrimitiveType::Int16 => map(
397            map_res(recognize_int, |(s, radix)| i16::from_str_radix(&s, radix)),
398            ConstValue::Int16,
399        )(input),
400        PrimitiveType::Int32 => map(
401            map_res(recognize_int, |(s, radix)| i32::from_str_radix(&s, radix)),
402            ConstValue::Int32,
403        )(input),
404        PrimitiveType::Int64 => map(
405            map_res(recognize_int, |(s, radix)| i64::from_str_radix(&s, radix)),
406            ConstValue::Int64,
407        )(input),
408        PrimitiveType::Float => map(
409            verify(recognize_float, |s: &str| s.parse::<f32>().is_ok()),
410            |s: &str| ConstValue::Float(s.to_owned()),
411        )(input),
412        PrimitiveType::Double => map(
413            verify(recognize_float, |s: &str| s.parse::<f64>().is_ok()),
414            |s: &str| ConstValue::Double(s.to_owned()),
415        )(input),
416        PrimitiveType::String => panic!("String constants are not supported"),
417        PrimitiveType::Boolean => panic!("Boolean constants are not supported"),
418        PrimitiveType::Byte => {
419            map(map_res(digit1, |s: &str| s.parse::<u8>()), ConstValue::Byte)(input)
420        }
421    }
422}
423
424fn const_name_val(ty: PrimitiveType) -> impl Fn(&str) -> IResult<&str, (&str, ConstValue)> {
425    move |input: &str| {
426        let (input, name) = ident(input)?;
427        let (input, _) = tuple((opt(ws), tag("="), opt(ws)))(input)?;
428        let (input, value) = const_value(ty, input)?;
429
430        Ok((input, (name, value)))
431    }
432}
433
434/// Parse a const declaration, inside a struct. (doesn't handle the semicolon or
435/// preceding whitespace)
436///
437/// ```
438/// # use nom::{Err, error::ErrorKind, Needed};
439/// use rust_lcm_codegen::parser::{const_decl, Const, ConstValue, PrimitiveType};
440///
441/// assert_eq!(
442///     const_decl("const int32_t YELLOW=1, GOLDENROD=2, CANARY=3"),
443///     Ok((
444///         "",
445///         vec![
446///           Const {
447///             name: "YELLOW".to_owned(),
448///             ty: PrimitiveType::Int32,
449///             value: ConstValue::Int32(1)
450///           },
451///           Const {
452///             name: "GOLDENROD".to_owned(),
453///             ty: PrimitiveType::Int32,
454///             value: ConstValue::Int32(2)
455///           },
456///           Const {
457///             name: "CANARY".to_owned(),
458///             ty: PrimitiveType::Int32,
459///             value: ConstValue::Int32(3)
460///           },
461///         ]
462///     ))
463/// );
464///
465/// assert_eq!(
466///     const_decl("const double PI=3.14159"),
467///     Ok((
468///         "",
469///         vec![Const {
470///             name: "PI".to_owned(),
471///             ty: PrimitiveType::Double,
472///             value: ConstValue::Double("3.14159".to_owned())
473///         }]
474///     ))
475/// );
476///
477///
478/// ```
479pub fn const_decl(input: &str) -> IResult<&str, Vec<Const>> {
480    let (input, _) = tuple((tag("const"), ws))(input)?;
481    let (input, ty) = primitive_type(input)?;
482    let (input, _) = ws(input)?;
483    let (input, name_vals) = separated_nonempty_list(spaced_comma, const_name_val(ty))(input)?;
484
485    Ok((
486        input,
487        name_vals
488            .into_iter()
489            .map(|(name, value)| Const {
490                name: name.to_string(),
491                value,
492                ty,
493            })
494            .collect(),
495    ))
496}
497
498pub fn struct_member(input: &str) -> IResult<&str, Vec<StructMember>> {
499    alt((
500        map(const_decl, |cds| {
501            cds.into_iter().map(StructMember::Const).collect()
502        }),
503        map(field_decl, |fd| vec![StructMember::Field(fd)]),
504    ))(input)
505}
506
507/// Parse a whole struct declaration.
508///
509/// ```
510/// # use nom::{Err, error::ErrorKind, Needed};
511/// use rust_lcm_codegen::parser::{struct_decl, Struct, Const, ConstValue, PrimitiveType, StructMember, Field, Type};
512///
513/// assert_eq!(
514///     struct_decl("struct empty_struct { }"),
515///     Ok((
516///         "",
517///         Struct {
518///           name: "empty_struct".to_string(),
519///           members: vec![],
520///         }
521///     ))
522/// );
523///
524/// assert_eq!(
525///     struct_decl("struct my_struct {\n  const int32_t YELLOW=1;\n  int32_t color;\n}"),
526///     Ok((
527///         "",
528///         Struct {
529///           name: "my_struct".to_string(),
530///           members: vec![
531///             StructMember::Const(
532///               Const {
533///                 name: "YELLOW".to_owned(),
534///                 ty: PrimitiveType::Int32,
535///                 value: ConstValue::Int32(1)
536///               }
537///             ),
538///             StructMember::Field(
539///               Field {
540///                 name: "color".to_owned(),
541///                 ty: Type::Primitive(PrimitiveType::Int32),
542///               }
543///             ),
544///           ]
545///         }
546///     ))
547/// );
548/// ```
549pub fn struct_decl(input: &str) -> IResult<&str, Struct> {
550    let (input, _) = tuple((tag("struct"), ws))(input)?;
551    let (input, name) = ident(input)?;
552    let (input, _) = tuple((ws, tag("{"), ws))(input)?;
553
554    let (input, member_vecs) = many0(terminated(
555        struct_member,
556        tuple((opt(ws), tag(";"), opt(ws))),
557    ))(input)?;
558
559    let (input, _) = tag("}")(input)?;
560
561    let mut members = vec![];
562    for mv in member_vecs.into_iter() {
563        members.extend(mv);
564    }
565
566    Ok((
567        input,
568        Struct {
569            name: name.to_owned(),
570            members,
571        },
572    ))
573}
574
575/// Parse a package line, not including the semicolon.
576///
577/// ```
578/// # use nom::{Err, error::ErrorKind, Needed};
579/// use rust_lcm_codegen::parser::package_decl;
580///
581/// assert_eq!(
582///     package_decl("package my_package"),
583///     Ok((
584///         "",
585///         "my_package".to_string(),
586///     ))
587/// );
588/// ```
589pub fn package_decl(input: &str) -> IResult<&str, String> {
590    map(
591        preceded(tuple((tag("package"), ws)), ident),
592        |name: &str| name.to_string(),
593    )(input)
594}
595
596/// Parse an entire schema file
597/// ```
598/// # use nom::{Err, error::ErrorKind, Needed};
599/// use rust_lcm_codegen::parser::{schema, Schema, Struct, Const, ConstValue, PrimitiveType, StructMember, Field};
600///
601/// assert_eq!(
602///     schema("package test;\n\nstruct empty { }\nstruct empty2 { }"),
603///     Ok((
604///         "",
605///         Schema {
606///           package: Some("test".to_string()),
607///           structs: vec![
608///             Struct {
609///               name: "empty".to_string(),
610///               members: vec![],
611///             },
612///             Struct {
613///               name: "empty2".to_string(),
614///               members: vec![],
615///             }
616///           ]
617///         },
618///     ))
619/// );
620///
621///  assert_eq!(
622///     schema("struct empty { }"),
623///     Ok((
624///         "",
625///         Schema {
626///           package: None,
627///           structs: vec![
628///             Struct {
629///               name: "empty".to_string(),
630///               members: vec![],
631///             }
632///           ]
633///         },
634///     ))
635/// );
636/// ```
637pub fn schema(input: &str) -> IResult<&str, Schema> {
638    let (input, _) = opt(ws)(input)?;
639    let (input, package) = opt(terminated(
640        package_decl,
641        tuple((opt(ws), tag(";"), opt(ws))),
642    ))(input)?;
643
644    let (input, structs) = many0(terminated(struct_decl, opt(ws)))(input)?;
645
646    Ok((input, Schema { package, structs }))
647}
648
649#[cfg(test)]
650mod test {
651    use super::*;
652
653    #[test]
654    fn comments_test() {
655        let schema_text = "
656/* before */
657// before
658package comment_stress_test;
659
660struct s {
661  /* a */
662  int32_t a; // a
663
664  /* b */
665  int32_t b; // b
666
667  /* c */
668  int32_t c[ /* two */ 2]; // c
669
670  /* D */
671  const int32_t D = /* three */ 3; //  D
672}
673
674/* after */
675// after
676";
677
678        let (unparsed, s) = schema(schema_text).unwrap();
679        assert_eq!(unparsed, "");
680    }
681}