1#![forbid(unsafe_code)]
15#![deny(missing_docs)]
16
17use std::collections::BTreeSet;
18
19use wesley_core::{OperationType, SchemaOperation, TypeKind, TypeReference, WesleyIR};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum ScalarKind {
24 Bool,
26 Int,
28 Float,
30 String,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum CodecOp {
38 Scalar(ScalarKind),
40 Named(String),
42 Option(Box<CodecOp>),
44 List(Box<CodecOp>),
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct FieldPlan {
51 pub name: String,
53 pub op: CodecOp,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum StructKind {
61 Input,
63 Object,
65 Interface,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
71pub enum CodecDef {
72 Enum {
74 name: String,
76 variants: Vec<String>,
78 },
79 Struct {
81 name: String,
83 kind: StructKind,
85 fields: Vec<FieldPlan>,
87 },
88 Operation {
92 operation_type: OperationType,
94 field_name: String,
96 fields: Vec<FieldPlan>,
98 },
99}
100
101#[must_use]
107pub fn plan(ir: &WesleyIR, operations: &[SchemaOperation]) -> Vec<CodecDef> {
108 let root_type_names: BTreeSet<&str> = operations
109 .iter()
110 .map(|operation| operation.root_type_name.as_str())
111 .collect();
112
113 let mut defs = Vec::new();
114 for type_def in &ir.types {
115 match type_def.kind {
116 TypeKind::Enum => defs.push(CodecDef::Enum {
117 name: type_def.name.clone(),
118 variants: type_def.enum_values.clone(),
119 }),
120 TypeKind::InputObject | TypeKind::Object | TypeKind::Interface
121 if !root_type_names.contains(type_def.name.as_str()) =>
122 {
123 defs.push(CodecDef::Struct {
124 name: type_def.name.clone(),
125 kind: match type_def.kind {
126 TypeKind::InputObject => StructKind::Input,
127 TypeKind::Interface => StructKind::Interface,
128 _ => StructKind::Object,
129 },
130 fields: type_def
131 .fields
132 .iter()
133 .map(|field| FieldPlan {
134 name: field.name.clone(),
135 op: op_for(&field.r#type),
136 })
137 .collect(),
138 });
139 }
140 _ => {}
141 }
142 }
143
144 for operation in operations {
145 defs.push(CodecDef::Operation {
146 operation_type: operation.operation_type,
147 field_name: operation.field_name.clone(),
148 fields: operation
149 .arguments
150 .iter()
151 .map(|argument| FieldPlan {
152 name: argument.name.clone(),
153 op: op_for(&argument.r#type),
154 })
155 .collect(),
156 });
157 }
158
159 defs
160}
161
162fn op_for(ty: &TypeReference) -> CodecOp {
165 if ty.nullable {
166 let mut inner = ty.clone();
167 inner.nullable = false;
168 return CodecOp::Option(Box::new(op_for(&inner)));
169 }
170 if let Some(item) = list_item_type_reference(ty) {
171 return CodecOp::List(Box::new(op_for(&item)));
172 }
173 match ty.base.as_str() {
174 "Boolean" => CodecOp::Scalar(ScalarKind::Bool),
175 "Int" => CodecOp::Scalar(ScalarKind::Int),
176 "Float" => CodecOp::Scalar(ScalarKind::Float),
177 "String" | "ID" => CodecOp::Scalar(ScalarKind::String),
178 other => CodecOp::Named(other.to_string()),
179 }
180}
181
182fn list_item_type_reference(ty: &TypeReference) -> Option<TypeReference> {
186 if !ty.is_list {
187 return None;
188 }
189
190 if ty.list_wrappers.is_empty() {
191 return Some(TypeReference {
192 base: ty.base.clone(),
193 nullable: ty.list_item_nullable.unwrap_or(true),
194 is_list: false,
195 list_item_nullable: None,
196 list_wrappers: Vec::new(),
197 leaf_nullable: None,
198 });
199 }
200
201 let remaining = ty.list_wrappers[1..].to_vec();
202 if remaining.is_empty() {
203 return Some(TypeReference {
204 base: ty.base.clone(),
205 nullable: ty.leaf_nullable.unwrap_or(true),
206 is_list: false,
207 list_item_nullable: None,
208 list_wrappers: Vec::new(),
209 leaf_nullable: None,
210 });
211 }
212
213 let next_nullable = remaining
214 .get(1)
215 .map(|wrapper| wrapper.nullable)
216 .unwrap_or_else(|| ty.leaf_nullable.unwrap_or(true));
217 Some(TypeReference {
218 base: ty.base.clone(),
219 nullable: remaining[0].nullable,
220 is_list: true,
221 list_item_nullable: Some(next_nullable),
222 list_wrappers: remaining,
223 leaf_nullable: ty.leaf_nullable,
224 })
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230 use wesley_core::{list_schema_operations_sdl, lower_schema_sdl};
231
232 fn named(defs: &[CodecDef], name: &str) -> CodecDef {
233 defs.iter()
234 .find(|def| match def {
235 CodecDef::Enum { name: n, .. } | CodecDef::Struct { name: n, .. } => n == name,
236 CodecDef::Operation { field_name, .. } => field_name == name,
237 })
238 .cloned()
239 .unwrap_or_else(|| panic!("no codec def named {name}"))
240 }
241
242 #[test]
243 fn lowers_scalars_enums_options_and_nested_lists() {
244 let sdl = include_str!("../../../test/fixtures/typescript-emitter/le-binary-codec.graphql");
245 let ir = lower_schema_sdl(sdl).expect("schema lowers");
246 let ops = list_schema_operations_sdl(sdl).expect("operations enumerable");
247 let defs = plan(&ir, &ops);
248
249 assert_eq!(
251 named(&defs, "Color"),
252 CodecDef::Enum {
253 name: "Color".to_string(),
254 variants: vec!["RED".into(), "GREEN".into(), "BLUE".into()],
255 }
256 );
257
258 let CodecDef::Struct { fields, .. } = named(&defs, "MakeWidgetInput") else {
260 panic!("MakeWidgetInput is a struct");
261 };
262 let op_of = |name: &str| fields.iter().find(|f| f.name == name).unwrap().op.clone();
263 assert_eq!(op_of("label"), CodecOp::Scalar(ScalarKind::String));
264 assert_eq!(op_of("count"), CodecOp::Scalar(ScalarKind::Int));
265 assert_eq!(
266 op_of("color"),
267 CodecOp::Option(Box::new(CodecOp::Named("Color".to_string())))
268 );
269 assert_eq!(
270 op_of("tags"),
271 CodecOp::List(Box::new(CodecOp::Scalar(ScalarKind::String)))
272 );
273 assert_eq!(
275 op_of("matrix"),
276 CodecOp::List(Box::new(CodecOp::List(Box::new(CodecOp::Scalar(
277 ScalarKind::Int
278 )))))
279 );
280
281 assert!(matches!(named(&defs, "Widget"), CodecDef::Struct { .. }));
283 assert!(!defs.iter().any(|def| matches!(
284 def,
285 CodecDef::Struct { name, .. } if name == "Mutation" || name == "Query"
286 )));
287
288 assert!(matches!(
290 named(&defs, "makeWidget"),
291 CodecDef::Operation { .. }
292 ));
293 }
294}