gurkle_parser/
common.rs

1use std::collections::BTreeMap;
2
3use combine::combinator::{choice, many, many1, optional, position};
4use combine::easy::Error;
5use combine::error::StreamError;
6use combine::{parser, ParseResult, Parser};
7
8use crate::helpers::{ident, kind, name, punct};
9use crate::position::Pos;
10use crate::tokenizer::{Kind as T, Token, TokenStream};
11
12/// An alias for string, used where graphql expects a name
13pub type Name = String;
14
15#[derive(Debug, Clone, PartialEq)]
16pub struct Directive {
17    pub position: Pos,
18    pub name: Name,
19    pub arguments: Vec<(Name, Value)>,
20}
21
22/// This represents integer number
23///
24/// But since there is no definition on limit of number in spec
25/// (only in implemetation), we do a trick similar to the one
26/// in `serde_json`: encapsulate value in new-type, allowing type
27/// to be extended later.
28#[derive(Debug, Clone, PartialEq)]
29// we use i64 as a reference implementation: graphql-js thinks even 32bit
30// integers is enough. We might consider lift this limit later though
31pub struct Number(pub(crate) i64);
32
33#[derive(Debug, Clone, PartialEq)]
34pub enum Value {
35    Variable(Name),
36    Int(Number),
37    Float(f64),
38    String(String),
39    Boolean(bool),
40    Null,
41    Enum(Name),
42    List(Vec<Value>),
43    Object(BTreeMap<Name, Value>),
44}
45
46#[derive(Debug, Clone, PartialEq)]
47pub enum Type {
48    NamedType(Name),
49    ListType(Box<Type>),
50    NonNullType(Box<Type>),
51}
52
53impl Number {
54    /// Returns a number as i64 if it fits the type
55    pub fn as_i64(&self) -> Option<i64> {
56        Some(self.0)
57    }
58}
59
60impl From<i32> for Number {
61    fn from(i: i32) -> Self {
62        Number(i as i64)
63    }
64}
65
66pub fn directives<'a>(input: &mut TokenStream<'a>) -> ParseResult<Vec<Directive>, TokenStream<'a>> {
67    many(
68        position()
69            .skip(punct("@"))
70            .and(name())
71            .and(parser(arguments))
72            .map(|((position, name), arguments)| Directive {
73                position,
74                name,
75                arguments,
76            }),
77    )
78    .parse_stream(input)
79}
80
81pub fn arguments<'a>(
82    input: &mut TokenStream<'a>,
83) -> ParseResult<Vec<(String, Value)>, TokenStream<'a>> {
84    optional(
85        punct("(")
86            .with(many1(name().skip(punct(":")).and(parser(value))))
87            .skip(punct(")")),
88    )
89    .map(|opt| opt.unwrap_or_else(Vec::new))
90    .parse_stream(input)
91}
92
93pub fn int_value<'a>(input: &mut TokenStream<'a>) -> ParseResult<Value, TokenStream<'a>> {
94    kind(T::IntValue)
95        .and_then(|tok| tok.value.parse())
96        .map(Number)
97        .map(Value::Int)
98        .parse_stream(input)
99}
100
101pub fn float_value<'a>(input: &mut TokenStream<'a>) -> ParseResult<Value, TokenStream<'a>> {
102    kind(T::FloatValue)
103        .and_then(|tok| tok.value.parse())
104        .map(Value::Float)
105        .parse_stream(input)
106}
107
108fn unquote_block_string(src: &str) -> Result<String, Error<Token, Token>> {
109    debug_assert!(src.starts_with("\"\"\"") && src.ends_with("\"\"\""));
110    let indent = src[3..src.len() - 3]
111        .lines()
112        .skip(1)
113        .filter_map(|line| {
114            let trimmed = line.trim_start().len();
115            if trimmed > 0 {
116                Some(line.len() - trimmed)
117            } else {
118                None // skip whitespace-only lines
119            }
120        })
121        .min()
122        .unwrap_or(0);
123    let mut result = String::with_capacity(src.len() - 6);
124    let mut lines = src[3..src.len() - 3].lines();
125    if let Some(first) = lines.next() {
126        let stripped = first.trim();
127        if !stripped.is_empty() {
128            result.push_str(stripped);
129            result.push('\n');
130        }
131    }
132    let mut last_line = 0;
133    for line in lines {
134        last_line = result.len();
135        if line.len() > indent {
136            result.push_str(&line[indent..].replace(r#"\""""#, r#"""""#));
137        }
138        result.push('\n');
139    }
140    if result[last_line..].trim().is_empty() {
141        result.truncate(last_line);
142    }
143
144    Ok(result)
145}
146
147fn unquote_string(s: &str) -> Result<String, Error<Token, Token>> {
148    let mut res = String::with_capacity(s.len());
149    debug_assert!(s.starts_with('"') && s.ends_with('"'));
150    let mut chars = s[1..s.len() - 1].chars();
151    let mut temp_code_point = String::with_capacity(4);
152    while let Some(c) = chars.next() {
153        match c {
154            '\\' => {
155                match chars.next().expect("slash cant be at the end") {
156                    c @ '"' | c @ '\\' | c @ '/' => res.push(c),
157                    'b' => res.push('\u{0010}'),
158                    'f' => res.push('\u{000C}'),
159                    'n' => res.push('\n'),
160                    'r' => res.push('\r'),
161                    't' => res.push('\t'),
162                    'u' => {
163                        temp_code_point.clear();
164                        for _ in 0..4 {
165                            match chars.next() {
166                                Some(inner_c) => temp_code_point.push(inner_c),
167                                None => {
168                                    return Err(Error::unexpected_message(format_args!(
169                                        "\\u must have 4 characters after it, only found '{}'",
170                                        temp_code_point
171                                    )))
172                                }
173                            }
174                        }
175
176                        // convert our hex string into a u32, then convert that into a char
177                        match u32::from_str_radix(&temp_code_point, 16).map(std::char::from_u32) {
178                            Ok(Some(unicode_char)) => res.push(unicode_char),
179                            _ => {
180                                return Err(Error::unexpected_message(format_args!(
181                                    "{} is not a valid unicode code point",
182                                    temp_code_point
183                                )))
184                            }
185                        }
186                    }
187                    c => {
188                        return Err(Error::unexpected_message(format_args!(
189                            "bad escaped char {:?}",
190                            c
191                        )));
192                    }
193                }
194            }
195            c => res.push(c),
196        }
197    }
198
199    Ok(res)
200}
201
202pub fn string<'a>(input: &mut TokenStream<'a>) -> ParseResult<String, TokenStream<'a>> {
203    choice((
204        kind(T::StringValue).and_then(|tok| unquote_string(tok.value)),
205        kind(T::BlockString).and_then(|tok| unquote_block_string(tok.value)),
206    ))
207    .parse_stream(input)
208}
209
210pub fn string_value<'a>(input: &mut TokenStream<'a>) -> ParseResult<Value, TokenStream<'a>> {
211    kind(T::StringValue)
212        .and_then(|tok| unquote_string(tok.value))
213        .map(Value::String)
214        .parse_stream(input)
215}
216
217pub fn block_string_value<'a>(input: &mut TokenStream<'a>) -> ParseResult<Value, TokenStream<'a>> {
218    kind(T::BlockString)
219        .and_then(|tok| unquote_block_string(tok.value))
220        .map(Value::String)
221        .parse_stream(input)
222}
223
224pub fn plain_value<'a>(input: &mut TokenStream<'a>) -> ParseResult<Value, TokenStream<'a>> {
225    ident("true")
226        .map(|_| Value::Boolean(true))
227        .or(ident("false").map(|_| Value::Boolean(false)))
228        .or(ident("null").map(|_| Value::Null))
229        .or(name().map(Value::Enum))
230        .or(parser(int_value))
231        .or(parser(float_value))
232        .or(parser(string_value))
233        .or(parser(block_string_value))
234        .parse_stream(input)
235}
236
237pub fn value<'a>(input: &mut TokenStream<'a>) -> ParseResult<Value, TokenStream<'a>> {
238    parser(plain_value)
239        .or(punct("$").with(name()).map(Value::Variable))
240        .or(punct("[")
241            .with(many(parser(value)))
242            .skip(punct("]"))
243            .map(Value::List))
244        .or(punct("{")
245            .with(many(name().skip(punct(":")).and(parser(value))))
246            .skip(punct("}"))
247            .map(Value::Object))
248        .parse_stream(input)
249}
250
251pub fn default_value<'a>(input: &mut TokenStream<'a>) -> ParseResult<Value, TokenStream<'a>> {
252    parser(plain_value)
253        .or(punct("[")
254            .with(many(parser(default_value)))
255            .skip(punct("]"))
256            .map(Value::List))
257        .or(punct("{")
258            .with(many(name().skip(punct(":")).and(parser(default_value))))
259            .skip(punct("}"))
260            .map(Value::Object))
261        .parse_stream(input)
262}
263
264pub fn parse_type<'a>(input: &mut TokenStream<'a>) -> ParseResult<Type, TokenStream<'a>> {
265    name()
266        .map(Type::NamedType)
267        .or(punct("[")
268            .with(parser(parse_type))
269            .skip(punct("]"))
270            .map(Box::new)
271            .map(Type::ListType))
272        .and(optional(punct("!")).map(|v| v.is_some()))
273        .map(|(typ, strict)| {
274            if strict {
275                Type::NonNullType(Box::new(typ))
276            } else {
277                typ
278            }
279        })
280        .parse_stream(input)
281}
282
283#[cfg(test)]
284mod tests {
285    use super::unquote_string;
286    use super::Number;
287
288    #[test]
289    fn number_from_i32_and_to_i64_conversion() {
290        assert_eq!(Number::from(1).as_i64(), Some(1));
291        assert_eq!(Number::from(584).as_i64(), Some(584));
292        assert_eq!(
293            Number::from(i32::min_value()).as_i64(),
294            Some(i32::min_value() as i64)
295        );
296        assert_eq!(
297            Number::from(i32::max_value()).as_i64(),
298            Some(i32::max_value() as i64)
299        );
300    }
301
302    #[test]
303    fn unquote_unicode_string() {
304        // basic tests
305        assert_eq!(unquote_string(r#""\u0009""#).expect(""), "\u{0009}");
306        assert_eq!(unquote_string(r#""\u000A""#).expect(""), "\u{000A}");
307        assert_eq!(unquote_string(r#""\u000D""#).expect(""), "\u{000D}");
308        assert_eq!(unquote_string(r#""\u0020""#).expect(""), "\u{0020}");
309        assert_eq!(unquote_string(r#""\uFFFF""#).expect(""), "\u{FFFF}");
310
311        // a more complex string
312        assert_eq!(
313            unquote_string(r#""\u0009 hello \u000A there""#).expect(""),
314            "\u{0009} hello \u{000A} there"
315        );
316    }
317}