protograph_core/
parser.rs

1use crate::ast::*;
2use crate::directives::*;
3use async_graphql_parser::types::{
4    BaseType, FieldDefinition, InputValueDefinition, ObjectType, Type, TypeDefinition,
5    TypeKind, TypeSystemDefinition,
6};
7use async_graphql_parser::{parse_schema, Positioned};
8use async_graphql_value::ConstValue;
9use thiserror::Error;
10
11#[derive(Error, Debug)]
12pub enum ParseError {
13    #[error("Failed to parse GraphQL schema: {0}")]
14    GraphQL(String),
15    #[error("Invalid directive argument: {0}")]
16    InvalidDirective(String),
17}
18
19pub fn parse_schema_file(content: &str) -> Result<ProtographSchema, ParseError> {
20    let document =
21        parse_schema(content).map_err(|e| ParseError::GraphQL(e.to_string()))?;
22
23    let mut schema = ProtographSchema::default();
24
25    for definition in document.definitions {
26        match definition {
27            TypeSystemDefinition::Type(type_def) => {
28                let type_name = type_def.node.name.node.to_string();
29                let directives = &type_def.node.directives;
30
31                match &type_def.node.kind {
32                    TypeKind::Object(obj) => {
33                        if type_name == "Query" {
34                            schema.query_fields = parse_query_fields(obj);
35                        } else if type_name == "Mutation" {
36                            schema.mutation_fields = parse_mutation_fields(obj);
37                        } else {
38                            let entity = parse_object_type(&type_name, directives, obj)?;
39                            schema.types.insert(entity.name.clone(), entity);
40                        }
41                    }
42                    TypeKind::InputObject(input) => {
43                        let input_type = parse_input_type(&type_name, input);
44                        schema.input_types.insert(input_type.name.clone(), input_type);
45                    }
46                    TypeKind::Enum(en) => {
47                        let enum_type = EnumType {
48                            name: type_name.clone(),
49                            values: en
50                                .values
51                                .iter()
52                                .map(|v| v.node.value.node.to_string())
53                                .collect(),
54                        };
55                        schema.enums.insert(enum_type.name.clone(), enum_type);
56                    }
57                    _ => {}
58                }
59            }
60            _ => {}
61        }
62    }
63
64    Ok(schema)
65}
66
67fn parse_object_type(
68    name: &str,
69    directives: &[Positioned<async_graphql_parser::types::ConstDirective>],
70    obj: &ObjectType,
71) -> Result<EntityType, ParseError> {
72    let is_entity = has_directive(directives, DIRECTIVE_ENTITY);
73    let is_private = has_directive(directives, DIRECTIVE_PRIVATE);
74
75    let mut fields = Vec::new();
76    for field_def in &obj.fields {
77        let field = parse_field(&field_def.node)?;
78        fields.push(field);
79    }
80
81    Ok(EntityType {
82        name: name.to_string(),
83        is_entity,
84        is_private,
85        fields,
86    })
87}
88
89fn parse_field(field: &FieldDefinition) -> Result<Field, ParseError> {
90    let name = field.name.node.to_string();
91    let field_type = convert_type(&field.ty.node);
92    let is_private = has_directive(&field.directives, DIRECTIVE_PRIVATE);
93    let relationship = parse_relationship_directive(&field.directives)?;
94
95    Ok(Field {
96        name,
97        field_type,
98        is_private,
99        relationship,
100    })
101}
102
103fn parse_input_type(
104    name: &str,
105    input: &async_graphql_parser::types::InputObjectType,
106) -> InputType {
107    let fields = input
108        .fields
109        .iter()
110        .map(|f| InputField {
111            name: f.node.name.node.to_string(),
112            field_type: convert_type(&f.node.ty.node),
113        })
114        .collect();
115
116    InputType { name: name.to_string(), fields }
117}
118
119fn parse_query_fields(obj: &ObjectType) -> Vec<QueryField> {
120    obj.fields
121        .iter()
122        .map(|f| QueryField {
123            name: f.node.name.node.to_string(),
124            arguments: parse_arguments(&f.node.arguments),
125            return_type: convert_type(&f.node.ty.node),
126        })
127        .collect()
128}
129
130fn parse_mutation_fields(obj: &ObjectType) -> Vec<MutationField> {
131    obj.fields
132        .iter()
133        .map(|f| MutationField {
134            name: f.node.name.node.to_string(),
135            arguments: parse_arguments(&f.node.arguments),
136            return_type: convert_type(&f.node.ty.node),
137        })
138        .collect()
139}
140
141fn parse_arguments(args: &[Positioned<InputValueDefinition>]) -> Vec<InputField> {
142    args.iter()
143        .map(|a| InputField {
144            name: a.node.name.node.to_string(),
145            field_type: convert_type(&a.node.ty.node),
146        })
147        .collect()
148}
149
150fn convert_type(ty: &Type) -> FieldType {
151    match &ty.base {
152        BaseType::Named(name) => {
153            let base = FieldType::Named(name.to_string());
154            if ty.nullable {
155                base
156            } else {
157                FieldType::NonNull(Box::new(base))
158            }
159        }
160        BaseType::List(inner) => {
161            let inner_type = convert_type(inner);
162            let list = FieldType::List(Box::new(inner_type));
163            if ty.nullable {
164                list
165            } else {
166                FieldType::NonNull(Box::new(list))
167            }
168        }
169    }
170}
171
172fn has_directive(
173    directives: &[Positioned<async_graphql_parser::types::ConstDirective>],
174    name: &str,
175) -> bool {
176    directives.iter().any(|d| d.node.name.node == name)
177}
178
179fn parse_relationship_directive(
180    directives: &[Positioned<async_graphql_parser::types::ConstDirective>],
181) -> Result<Option<Relationship>, ParseError> {
182    for directive in directives {
183        let name = directive.node.name.node.as_str();
184        match name {
185            DIRECTIVE_BELONGS_TO => {
186                let field = get_directive_arg(&directive.node, ARG_FIELD)?;
187                return Ok(Some(Relationship::BelongsTo { foreign_key: field }));
188            }
189            DIRECTIVE_HAS_MANY => {
190                let field = get_directive_arg(&directive.node, ARG_FIELD)?;
191                return Ok(Some(Relationship::HasMany { foreign_key: field }));
192            }
193            DIRECTIVE_MANY_TO_MANY => {
194                let through = get_directive_arg(&directive.node, ARG_THROUGH)?;
195                let field = get_directive_arg(&directive.node, ARG_FIELD)?;
196                return Ok(Some(Relationship::ManyToMany {
197                    junction_table: through,
198                    foreign_key: field,
199                }));
200            }
201            _ => {}
202        }
203    }
204    Ok(None)
205}
206
207fn get_directive_arg(
208    directive: &async_graphql_parser::types::ConstDirective,
209    arg_name: &str,
210) -> Result<String, ParseError> {
211    for (name, value) in &directive.arguments {
212        if name.node == arg_name {
213            if let ConstValue::String(s) = &value.node {
214                return Ok(s.clone());
215            }
216        }
217    }
218    Err(ParseError::InvalidDirective(format!(
219        "Missing required argument '{}' on @{}",
220        arg_name, directive.name.node
221    )))
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn test_parse_simple_schema() {
230        let schema = r#"
231            type User @entity {
232                id: ID!
233                name: String!
234                email: String! @private
235            }
236
237            type Post @entity {
238                id: ID!
239                title: String!
240                author: User! @belongsTo(field: "authorId")
241                authorId: ID! @private
242            }
243
244            type Query {
245                user(id: ID!): User
246                users: [User!]!
247            }
248        "#;
249
250        let result = parse_schema_file(schema).unwrap();
251
252        assert!(result.types.contains_key("User"));
253        assert!(result.types.contains_key("Post"));
254        assert!(result.types.get("User").unwrap().is_entity);
255        assert_eq!(result.query_fields.len(), 2);
256
257        let post = result.types.get("Post").unwrap();
258        let author_field = post.fields.iter().find(|f| f.name == "author").unwrap();
259        assert!(matches!(
260            &author_field.relationship,
261            Some(Relationship::BelongsTo { foreign_key }) if foreign_key == "authorId"
262        ));
263    }
264}