protograph_core/
parser.rs1use 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}