#![forbid(unsafe_code)]
#![deny(missing_docs)]
use std::collections::BTreeSet;
use wesley_core::{OperationType, SchemaOperation, TypeKind, TypeReference, WesleyIR};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScalarKind {
Bool,
Int,
Float,
String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CodecOp {
Scalar(ScalarKind),
Named(String),
Option(Box<CodecOp>),
List(Box<CodecOp>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FieldPlan {
pub name: String,
pub op: CodecOp,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StructKind {
Input,
Object,
Interface,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CodecDef {
Enum {
name: String,
variants: Vec<String>,
},
Struct {
name: String,
kind: StructKind,
fields: Vec<FieldPlan>,
},
Operation {
operation_type: OperationType,
field_name: String,
fields: Vec<FieldPlan>,
},
}
#[must_use]
pub fn plan(ir: &WesleyIR, operations: &[SchemaOperation]) -> Vec<CodecDef> {
let root_type_names: BTreeSet<&str> = operations
.iter()
.map(|operation| operation.root_type_name.as_str())
.collect();
let mut defs = Vec::new();
for type_def in &ir.types {
match type_def.kind {
TypeKind::Enum => defs.push(CodecDef::Enum {
name: type_def.name.clone(),
variants: type_def.enum_values.clone(),
}),
TypeKind::InputObject | TypeKind::Object | TypeKind::Interface
if !root_type_names.contains(type_def.name.as_str()) =>
{
defs.push(CodecDef::Struct {
name: type_def.name.clone(),
kind: match type_def.kind {
TypeKind::InputObject => StructKind::Input,
TypeKind::Interface => StructKind::Interface,
_ => StructKind::Object,
},
fields: type_def
.fields
.iter()
.map(|field| FieldPlan {
name: field.name.clone(),
op: op_for(&field.r#type),
})
.collect(),
});
}
_ => {}
}
}
for operation in operations {
defs.push(CodecDef::Operation {
operation_type: operation.operation_type,
field_name: operation.field_name.clone(),
fields: operation
.arguments
.iter()
.map(|argument| FieldPlan {
name: argument.name.clone(),
op: op_for(&argument.r#type),
})
.collect(),
});
}
defs
}
fn op_for(ty: &TypeReference) -> CodecOp {
if ty.nullable {
let mut inner = ty.clone();
inner.nullable = false;
return CodecOp::Option(Box::new(op_for(&inner)));
}
if let Some(item) = list_item_type_reference(ty) {
return CodecOp::List(Box::new(op_for(&item)));
}
match ty.base.as_str() {
"Boolean" => CodecOp::Scalar(ScalarKind::Bool),
"Int" => CodecOp::Scalar(ScalarKind::Int),
"Float" => CodecOp::Scalar(ScalarKind::Float),
"String" | "ID" => CodecOp::Scalar(ScalarKind::String),
other => CodecOp::Named(other.to_string()),
}
}
fn list_item_type_reference(ty: &TypeReference) -> Option<TypeReference> {
if !ty.is_list {
return None;
}
if ty.list_wrappers.is_empty() {
return Some(TypeReference {
base: ty.base.clone(),
nullable: ty.list_item_nullable.unwrap_or(true),
is_list: false,
list_item_nullable: None,
list_wrappers: Vec::new(),
leaf_nullable: None,
});
}
let remaining = ty.list_wrappers[1..].to_vec();
if remaining.is_empty() {
return Some(TypeReference {
base: ty.base.clone(),
nullable: ty.leaf_nullable.unwrap_or(true),
is_list: false,
list_item_nullable: None,
list_wrappers: Vec::new(),
leaf_nullable: None,
});
}
let next_nullable = remaining
.get(1)
.map(|wrapper| wrapper.nullable)
.unwrap_or_else(|| ty.leaf_nullable.unwrap_or(true));
Some(TypeReference {
base: ty.base.clone(),
nullable: remaining[0].nullable,
is_list: true,
list_item_nullable: Some(next_nullable),
list_wrappers: remaining,
leaf_nullable: ty.leaf_nullable,
})
}
#[cfg(test)]
mod tests {
use super::*;
use wesley_core::{list_schema_operations_sdl, lower_schema_sdl};
fn named(defs: &[CodecDef], name: &str) -> CodecDef {
defs.iter()
.find(|def| match def {
CodecDef::Enum { name: n, .. } | CodecDef::Struct { name: n, .. } => n == name,
CodecDef::Operation { field_name, .. } => field_name == name,
})
.cloned()
.unwrap_or_else(|| panic!("no codec def named {name}"))
}
#[test]
fn lowers_scalars_enums_options_and_nested_lists() {
let sdl = include_str!("../../../test/fixtures/typescript-emitter/le-binary-codec.graphql");
let ir = lower_schema_sdl(sdl).expect("schema lowers");
let ops = list_schema_operations_sdl(sdl).expect("operations enumerable");
let defs = plan(&ir, &ops);
assert_eq!(
named(&defs, "Color"),
CodecDef::Enum {
name: "Color".to_string(),
variants: vec!["RED".into(), "GREEN".into(), "BLUE".into()],
}
);
let CodecDef::Struct { fields, .. } = named(&defs, "MakeWidgetInput") else {
panic!("MakeWidgetInput is a struct");
};
let op_of = |name: &str| fields.iter().find(|f| f.name == name).unwrap().op.clone();
assert_eq!(op_of("label"), CodecOp::Scalar(ScalarKind::String));
assert_eq!(op_of("count"), CodecOp::Scalar(ScalarKind::Int));
assert_eq!(
op_of("color"),
CodecOp::Option(Box::new(CodecOp::Named("Color".to_string())))
);
assert_eq!(
op_of("tags"),
CodecOp::List(Box::new(CodecOp::Scalar(ScalarKind::String)))
);
assert_eq!(
op_of("matrix"),
CodecOp::List(Box::new(CodecOp::List(Box::new(CodecOp::Scalar(
ScalarKind::Int
)))))
);
assert!(matches!(named(&defs, "Widget"), CodecDef::Struct { .. }));
assert!(!defs.iter().any(|def| matches!(
def,
CodecDef::Struct { name, .. } if name == "Mutation" || name == "Query"
)));
assert!(matches!(
named(&defs, "makeWidget"),
CodecDef::Operation { .. }
));
}
}