typewriter_graphql/
mapper.rs1use typewriter_core::ir::*;
4use typewriter_core::mapper::TypeMapper;
5use typewriter_core::naming::{FileStyle, to_file_style};
6
7use crate::emitter;
8
9pub struct GraphQLMapper {
32 pub file_style: FileStyle,
34}
35
36impl GraphQLMapper {
37 pub fn new() -> Self {
38 Self {
39 file_style: FileStyle::SnakeCase,
40 }
41 }
42
43 pub fn with_file_style(mut self, style: FileStyle) -> Self {
44 self.file_style = style;
45 self
46 }
47}
48
49impl Default for GraphQLMapper {
50 fn default() -> Self {
51 Self::new()
52 }
53}
54
55impl TypeMapper for GraphQLMapper {
56 fn map_primitive(&self, ty: &PrimitiveType) -> String {
57 match ty {
58 PrimitiveType::String => "String".to_string(),
59 PrimitiveType::Bool => "Boolean".to_string(),
60 PrimitiveType::U8 | PrimitiveType::U16 | PrimitiveType::U32 => "Int".to_string(),
61 PrimitiveType::I8 | PrimitiveType::I16 | PrimitiveType::I32 => "Int".to_string(),
62 PrimitiveType::F32 | PrimitiveType::F64 => "Float".to_string(),
63 PrimitiveType::U64 | PrimitiveType::U128 => "String".to_string(),
65 PrimitiveType::I64 | PrimitiveType::I128 => "String".to_string(),
66 PrimitiveType::Uuid => "ID".to_string(),
67 PrimitiveType::DateTime | PrimitiveType::NaiveDate => "DateTime".to_string(),
68 PrimitiveType::JsonValue => "JSON".to_string(),
69 }
70 }
71
72 fn map_option(&self, inner: &TypeKind) -> String {
73 self.map_type(inner)
76 }
77
78 fn map_vec(&self, inner: &TypeKind) -> String {
79 format!("[{}!]", self.map_type(inner))
80 }
81
82 fn map_hashmap(&self, _key: &TypeKind, _value: &TypeKind) -> String {
83 "JSON".to_string()
85 }
86
87 fn map_tuple(&self, _elements: &[TypeKind]) -> String {
88 "JSON".to_string()
90 }
91
92 fn map_named(&self, name: &str) -> String {
93 name.to_string()
94 }
95
96 fn emit_struct(&self, def: &StructDef) -> String {
97 emitter::render_type(self, def)
98 }
99
100 fn emit_enum(&self, def: &EnumDef) -> String {
101 emitter::render_enum(self, def)
102 }
103
104 fn file_header(&self, type_name: &str) -> String {
105 format!(
106 "# Auto-generated by typewriter v0.4.1. DO NOT EDIT.\n\
107 # Source: {}\n\n",
108 type_name
109 )
110 }
111
112 fn file_extension(&self) -> &str {
113 "graphql"
114 }
115
116 fn emit_imports(&self, def: &TypeDef) -> String {
117 let mut needs_datetime = false;
119 let mut needs_json = false;
120
121 fn scan_type(ty: &TypeKind, needs_datetime: &mut bool, needs_json: &mut bool) {
122 match ty {
123 TypeKind::Primitive(p) => match p {
124 PrimitiveType::DateTime | PrimitiveType::NaiveDate => {
125 *needs_datetime = true;
126 }
127 PrimitiveType::JsonValue => {
128 *needs_json = true;
129 }
130 _ => {}
131 },
132 TypeKind::Option(inner) | TypeKind::Vec(inner) => {
133 scan_type(inner, needs_datetime, needs_json);
134 }
135 TypeKind::HashMap(_, _) => {
136 *needs_json = true;
137 }
138 TypeKind::Tuple(_) => {
139 *needs_json = true;
140 }
141 TypeKind::Generic(_, params) => {
142 for p in params {
143 scan_type(p, needs_datetime, needs_json);
144 }
145 }
146 _ => {}
147 }
148 }
149
150 match def {
151 TypeDef::Struct(s) => {
152 for field in &s.fields {
153 if !field.skip {
154 scan_type(&field.ty, &mut needs_datetime, &mut needs_json);
155 }
156 }
157 }
158 TypeDef::Enum(e) => {
159 for variant in &e.variants {
160 match &variant.kind {
161 VariantKind::Struct(fields) => {
162 for field in fields {
163 if !field.skip {
164 scan_type(&field.ty, &mut needs_datetime, &mut needs_json);
165 }
166 }
167 }
168 VariantKind::Tuple(types) => {
169 for ty in types {
170 scan_type(ty, &mut needs_datetime, &mut needs_json);
171 }
172 }
173 VariantKind::Unit => {}
174 }
175 }
176 }
177 }
178
179 let mut output = String::new();
180 if needs_datetime {
181 output.push_str("scalar DateTime\n");
182 }
183 if needs_json {
184 output.push_str("scalar JSON\n");
185 }
186 if !output.is_empty() {
187 output.push('\n');
188 }
189 output
190 }
191
192 fn file_naming(&self, type_name: &str) -> String {
193 to_file_style(type_name, self.file_style)
194 }
195
196 fn map_generic(&self, name: &str, _params: &[TypeKind]) -> String {
197 name.to_string()
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 fn mapper() -> GraphQLMapper {
207 GraphQLMapper::new()
208 }
209
210 #[test]
211 fn test_primitive_mappings() {
212 let m = mapper();
213 assert_eq!(m.map_primitive(&PrimitiveType::String), "String");
214 assert_eq!(m.map_primitive(&PrimitiveType::Bool), "Boolean");
215 assert_eq!(m.map_primitive(&PrimitiveType::U32), "Int");
216 assert_eq!(m.map_primitive(&PrimitiveType::I32), "Int");
217 assert_eq!(m.map_primitive(&PrimitiveType::F64), "Float");
218 assert_eq!(m.map_primitive(&PrimitiveType::U64), "String");
219 assert_eq!(m.map_primitive(&PrimitiveType::I64), "String");
220 assert_eq!(m.map_primitive(&PrimitiveType::Uuid), "ID");
221 assert_eq!(m.map_primitive(&PrimitiveType::DateTime), "DateTime");
222 assert_eq!(m.map_primitive(&PrimitiveType::JsonValue), "JSON");
223 }
224
225 #[test]
226 fn test_option_mapping() {
227 let m = mapper();
228 assert_eq!(
229 m.map_option(&TypeKind::Primitive(PrimitiveType::U32)),
230 "Int"
231 );
232 }
233
234 #[test]
235 fn test_vec_mapping() {
236 let m = mapper();
237 assert_eq!(
238 m.map_vec(&TypeKind::Primitive(PrimitiveType::String)),
239 "[String!]"
240 );
241 }
242
243 #[test]
244 fn test_hashmap_mapping() {
245 let m = mapper();
246 assert_eq!(
247 m.map_hashmap(
248 &TypeKind::Primitive(PrimitiveType::String),
249 &TypeKind::Primitive(PrimitiveType::U32)
250 ),
251 "JSON"
252 );
253 }
254
255 #[test]
256 fn test_tuple_mapping() {
257 let m = mapper();
258 assert_eq!(
259 m.map_tuple(&[
260 TypeKind::Primitive(PrimitiveType::String),
261 TypeKind::Primitive(PrimitiveType::U32)
262 ]),
263 "JSON"
264 );
265 }
266
267 #[test]
268 fn test_file_naming() {
269 let m = mapper();
270 assert_eq!(m.file_naming("UserProfile"), "user_profile");
271 assert_eq!(m.file_naming("User"), "user");
272 }
273
274 #[test]
275 fn test_output_filename() {
276 let m = mapper();
277 assert_eq!(m.output_filename("UserProfile"), "user_profile.graphql");
278 }
279
280 #[test]
281 fn test_file_naming_kebab() {
282 let m = GraphQLMapper::new().with_file_style(FileStyle::KebabCase);
283 assert_eq!(m.file_naming("UserProfile"), "user-profile");
284 assert_eq!(m.output_filename("UserProfile"), "user-profile.graphql");
285 }
286
287 #[test]
288 fn test_generic_mapped_as_base_name() {
289 let m = mapper();
290 assert_eq!(
291 m.map_generic("Pagination", &[TypeKind::Named("User".to_string())]),
292 "Pagination"
293 );
294 }
295}