Skip to main content

apollo_smith/
name.rs

1use crate::DocumentBuilder;
2use arbitrary::Result as ArbitraryResult;
3use std::fmt::Write as _;
4
5// First char in a GraphQL name can't be a digit and we don't want it to be
6// `_` either. Body chars can be letters, `_`, or digits.
7const CHARSET_NAME_HEAD: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
8const CHARSET_NAME_BODY: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0123456789";
9const RESERVED_KEYWORDS: &[&str] = &[
10    "on",
11    "Int",
12    "Float",
13    "String",
14    "Boolean",
15    "ID",
16    "type",
17    "enum",
18    "union",
19    "extend",
20    "scalar",
21    "directive",
22    "query",
23    "mutation",
24    "subscription",
25    "schema",
26    "interface",
27];
28
29/// Name is useful to name different elements.
30///
31/// GraphQL Documents are full of named things: operations, fields, arguments, types, directives, fragments, and variables.
32/// All names must follow the same grammatical form.
33/// Names in GraphQL are case-sensitive. That is to say name, Name, and NAME all refer to different names.
34/// Underscores are significant, which means other_name and othername are two different names
35///
36/// Detailed documentation can be found in [GraphQL spec](https://spec.graphql.org/October2021/#Name).
37#[derive(Debug, Clone, PartialEq, Eq, Hash)]
38pub struct Name {
39    pub(crate) name: String,
40}
41
42impl From<Name> for String {
43    fn from(val: Name) -> Self {
44        val.name
45    }
46}
47
48impl From<Name> for apollo_compiler::Name {
49    fn from(value: Name) -> Self {
50        (&value).into()
51    }
52}
53
54impl From<&'_ Name> for apollo_compiler::Name {
55    fn from(value: &'_ Name) -> Self {
56        // FIXME: falliable instead of unwrap?
57        // Names from `DocumentBuilder` do have valid syntax,
58        // but the `new` constructor accepts any string
59        apollo_compiler::Name::new(&value.name).unwrap()
60    }
61}
62
63impl From<apollo_parser::cst::Name> for Name {
64    fn from(name: apollo_parser::cst::Name) -> Self {
65        Self {
66            name: name.ident_token().unwrap().to_string(),
67        }
68    }
69}
70
71impl Name {
72    pub const fn new(name: String) -> Self {
73        Self { name }
74    }
75}
76
77impl DocumentBuilder<'_> {
78    /// Create an arbitrary `Name`
79    pub fn name(&mut self) -> ArbitraryResult<Name> {
80        Ok(Name::new(self.limited_string(30)?))
81    }
82
83    /// Create an arbitrary type `Name` that does not yet exist in the document.
84    pub fn type_name(&mut self) -> ArbitraryResult<Name> {
85        let base = self.limited_string(30)?;
86        let mut suffix = 0usize;
87        let mut new_name = base.clone();
88        while self.used_type_names.contains(new_name.as_str()) {
89            new_name.clear();
90            let _ = write!(new_name, "{base}{suffix}");
91            suffix += 1;
92        }
93        self.used_type_names.insert(new_name.clone());
94        Ok(Name::new(new_name))
95    }
96
97    /// Create an arbitrary `Name` with an index included in the name (to avoid name conflict)
98    pub fn name_with_index(&mut self, index: usize) -> ArbitraryResult<Name> {
99        let mut name = self.limited_string(30)?;
100        let _ = write!(name, "{index}");
101
102        Ok(Name::new(name))
103    }
104
105    // Mirror what happens in `Arbitrary for String`, but do so with a clamped size.
106    pub(crate) fn limited_string(&mut self, max_size: usize) -> ArbitraryResult<String> {
107        loop {
108            let size = self.u.int_in_range(1..=max_size)?;
109
110            let gen_str = String::from_utf8(
111                (0..size)
112                    .map(|curr_idx| {
113                        // GraphQL names can't start with a digit or `_`.
114                        let charset = if curr_idx == 0 {
115                            CHARSET_NAME_HEAD
116                        } else {
117                            CHARSET_NAME_BODY
118                        };
119                        Ok(*self.u.choose(charset)?)
120                    })
121                    .collect::<ArbitraryResult<Vec<u8>>>()?,
122            )
123            .unwrap();
124            let new_gen = gen_str.trim_end_matches('_');
125            if !new_gen.is_empty() && !RESERVED_KEYWORDS.contains(&new_gen) {
126                break Ok(new_gen.to_string());
127            }
128        }
129    }
130}