use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum NodeType {
ObjectType,
InterfaceType,
InputType,
EnumType,
UnionType,
ScalarType,
Directive,
}
impl std::fmt::Display for NodeType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
NodeType::ObjectType => write!(f, "type"),
NodeType::InterfaceType => write!(f, "interface"),
NodeType::InputType => write!(f, "input"),
NodeType::EnumType => write!(f, "enum"),
NodeType::UnionType => write!(f, "union"),
NodeType::ScalarType => write!(f, "scalar"),
NodeType::Directive => write!(f, "directive"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum EdgeType {
Field,
Implements,
UnionMember,
Argument,
DirectiveUse,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
pub struct Position {
pub x: f64,
pub y: f64,
}
impl Position {
pub fn new(x: f64, y: f64) -> Self {
Self { x, y }
}
pub fn distance_to(&self, other: &Position) -> f64 {
let dx = self.x - other.x;
let dy = self.y - other.y;
(dx * dx + dy * dy).sqrt()
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct Size {
pub width: f64,
pub height: f64,
}
impl Default for Size {
fn default() -> Self {
Self {
width: 200.0,
height: 100.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemaNode {
pub id: String,
pub name: String,
pub node_type: NodeType,
pub description: Option<String>,
pub fields: Vec<FieldDefinition>,
pub enum_values: Vec<EnumValue>,
pub union_members: Vec<String>,
pub implements: Vec<String>,
pub directives: Vec<DirectiveUse>,
pub position: Position,
pub size: Size,
pub collapsed: bool,
pub selected: bool,
pub metadata: HashMap<String, String>,
}
impl SchemaNode {
pub fn new(id: &str, name: &str, node_type: NodeType) -> Self {
Self {
id: id.to_string(),
name: name.to_string(),
node_type,
description: None,
fields: Vec::new(),
enum_values: Vec::new(),
union_members: Vec::new(),
implements: Vec::new(),
directives: Vec::new(),
position: Position::default(),
size: Size::default(),
collapsed: false,
selected: false,
metadata: HashMap::new(),
}
}
pub fn with_description(mut self, description: &str) -> Self {
self.description = Some(description.to_string());
self
}
pub fn with_field(mut self, field: FieldDefinition) -> Self {
self.fields.push(field);
self
}
pub fn with_position(mut self, x: f64, y: f64) -> Self {
self.position = Position::new(x, y);
self
}
pub fn calculate_height(&self) -> f64 {
let base_height = 40.0; let field_height = 24.0;
let padding = 16.0;
if self.collapsed {
return base_height;
}
let content_height = match self.node_type {
NodeType::ObjectType | NodeType::InterfaceType | NodeType::InputType => {
self.fields.len() as f64 * field_height
}
NodeType::EnumType => self.enum_values.len() as f64 * field_height,
NodeType::UnionType => self.union_members.len() as f64 * field_height,
_ => 0.0,
};
base_height + content_height + padding
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldDefinition {
pub name: String,
pub field_type: String,
pub nullable: bool,
pub is_list: bool,
pub description: Option<String>,
pub arguments: Vec<ArgumentDefinition>,
pub directives: Vec<DirectiveUse>,
pub default_value: Option<String>,
pub deprecated: bool,
pub deprecation_reason: Option<String>,
}
impl FieldDefinition {
pub fn new(name: &str, field_type: &str) -> Self {
Self {
name: name.to_string(),
field_type: field_type.to_string(),
nullable: true,
is_list: false,
description: None,
arguments: Vec::new(),
directives: Vec::new(),
default_value: None,
deprecated: false,
deprecation_reason: None,
}
}
pub fn non_null(mut self) -> Self {
self.nullable = false;
self
}
pub fn list(mut self) -> Self {
self.is_list = true;
self
}
pub fn with_description(mut self, description: &str) -> Self {
self.description = Some(description.to_string());
self
}
pub fn type_string(&self) -> String {
let mut s = self.field_type.clone();
if self.is_list {
s = format!("[{}]", s);
}
if !self.nullable {
s = format!("{}!", s);
}
s
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArgumentDefinition {
pub name: String,
pub arg_type: String,
pub nullable: bool,
pub default_value: Option<String>,
pub description: Option<String>,
}
impl ArgumentDefinition {
pub fn new(name: &str, arg_type: &str) -> Self {
Self {
name: name.to_string(),
arg_type: arg_type.to_string(),
nullable: true,
default_value: None,
description: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnumValue {
pub name: String,
pub description: Option<String>,
pub deprecated: bool,
pub deprecation_reason: Option<String>,
}
impl EnumValue {
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
description: None,
deprecated: false,
deprecation_reason: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DirectiveUse {
pub name: String,
pub arguments: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemaEdge {
pub id: String,
pub source: String,
pub target: String,
pub source_field: Option<String>,
pub edge_type: EdgeType,
pub label: Option<String>,
pub highlighted: bool,
}
impl SchemaEdge {
pub fn new(id: &str, source: &str, target: &str, edge_type: EdgeType) -> Self {
Self {
id: id.to_string(),
source: source.to_string(),
target: target.to_string(),
source_field: None,
edge_type,
label: None,
highlighted: false,
}
}
pub fn with_source_field(mut self, field: &str) -> Self {
self.source_field = Some(field.to_string());
self
}
pub fn with_label(mut self, label: &str) -> Self {
self.label = Some(label.to_string());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemaGraph {
pub nodes: Vec<SchemaNode>,
pub edges: Vec<SchemaEdge>,
pub width: f64,
pub height: f64,
pub zoom: f64,
pub pan: Position,
}
impl Default for SchemaGraph {
fn default() -> Self {
Self {
nodes: Vec::new(),
edges: Vec::new(),
width: 1000.0,
height: 800.0,
zoom: 1.0,
pan: Position::default(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum LayoutAlgorithm {
#[default]
ForceDirected,
Hierarchical,
Circular,
Grid,
Manual,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LayoutOptions {
pub algorithm: LayoutAlgorithm,
pub node_spacing: f64,
pub layer_spacing: f64,
pub iterations: u32,
pub animation_duration: u64,
pub group_by_type: bool,
}
impl Default for LayoutOptions {
fn default() -> Self {
Self {
algorithm: LayoutAlgorithm::default(),
node_spacing: 50.0,
layer_spacing: 100.0,
iterations: 100,
animation_duration: 300,
group_by_type: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum EditOperation {
AddNode { node: SchemaNode },
RemoveNode { node_id: String },
UpdateNode { node: SchemaNode },
AddField {
node_id: String,
field: FieldDefinition,
},
RemoveField { node_id: String, field_name: String },
UpdateField {
node_id: String,
field: FieldDefinition,
},
AddEdge { edge: SchemaEdge },
RemoveEdge { edge_id: String },
MoveNode { node_id: String, position: Position },
Batch { operations: Vec<EditOperation> },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationResult {
pub valid: bool,
pub errors: Vec<ValidationError>,
pub warnings: Vec<ValidationWarning>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationError {
pub code: String,
pub message: String,
pub node_id: Option<String>,
pub field_name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationWarning {
pub code: String,
pub message: String,
pub node_id: Option<String>,
}
struct DesignerState {
graph: SchemaGraph,
node_index: HashMap<String, usize>,
edge_index: HashMap<String, usize>,
history: Vec<EditOperation>,
history_position: usize,
layout_options: LayoutOptions,
}
impl DesignerState {
fn new() -> Self {
Self {
graph: SchemaGraph::default(),
node_index: HashMap::new(),
edge_index: HashMap::new(),
history: Vec::new(),
history_position: 0,
layout_options: LayoutOptions::default(),
}
}
fn rebuild_indices(&mut self) {
self.node_index.clear();
for (i, node) in self.graph.nodes.iter().enumerate() {
self.node_index.insert(node.id.clone(), i);
}
self.edge_index.clear();
for (i, edge) in self.graph.edges.iter().enumerate() {
self.edge_index.insert(edge.id.clone(), i);
}
}
}
pub struct SchemaDesigner {
state: Arc<RwLock<DesignerState>>,
}
impl SchemaDesigner {
pub fn new() -> Self {
Self {
state: Arc::new(RwLock::new(DesignerState::new())),
}
}
pub async fn get_visualization(&self) -> SchemaGraph {
let state = self.state.read().await;
state.graph.clone()
}
pub async fn add_node(&self, mut node: SchemaNode) -> Result<String> {
let node_id = if node.id.is_empty() {
uuid::Uuid::new_v4().to_string()
} else {
node.id.clone()
};
node.id = node_id.clone();
let mut state = self.state.write().await;
if state.graph.nodes.iter().any(|n| n.name == node.name) {
return Err(anyhow!("Type '{}' already exists", node.name));
}
node.size.height = node.calculate_height();
state.graph.nodes.push(node.clone());
let node_index_value = state.graph.nodes.len() - 1;
state.node_index.insert(node_id.clone(), node_index_value);
let history_pos = state.history_position;
state.history.truncate(history_pos);
state.history.push(EditOperation::AddNode { node });
state.history_position = state.history.len();
Ok(node_id)
}
pub async fn remove_node(&self, node_id: &str) -> Result<()> {
let mut state = self.state.write().await;
let Some(&index) = state.node_index.get(node_id) else {
return Err(anyhow!("Node '{}' not found", node_id));
};
let _node = state.graph.nodes.remove(index);
state
.graph
.edges
.retain(|e| e.source != node_id && e.target != node_id);
state.rebuild_indices();
let history_pos = state.history_position;
state.history.truncate(history_pos);
state.history.push(EditOperation::RemoveNode {
node_id: node_id.to_string(),
});
state.history_position = state.history.len();
Ok(())
}
pub async fn update_node(&self, node: SchemaNode) -> Result<()> {
let mut state = self.state.write().await;
let Some(&index) = state.node_index.get(&node.id) else {
return Err(anyhow!("Node '{}' not found", node.id));
};
let mut updated_node = node.clone();
updated_node.size.height = updated_node.calculate_height();
state.graph.nodes[index] = updated_node;
let history_pos = state.history_position;
state.history.truncate(history_pos);
state.history.push(EditOperation::UpdateNode { node });
state.history_position = state.history.len();
Ok(())
}
pub async fn add_field(&self, node_id: &str, field: FieldDefinition) -> Result<()> {
let mut state = self.state.write().await;
let Some(&index) = state.node_index.get(node_id) else {
return Err(anyhow!("Node '{}' not found", node_id));
};
if state.graph.nodes[index]
.fields
.iter()
.any(|f| f.name == field.name)
{
return Err(anyhow!(
"Field '{}' already exists in '{}'",
field.name,
node_id
));
}
state.graph.nodes[index].fields.push(field.clone());
state.graph.nodes[index].size.height = state.graph.nodes[index].calculate_height();
let target_type = field
.field_type
.trim_matches(|c| c == '[' || c == ']' || c == '!');
if let Some(target_node) = state.graph.nodes.iter().find(|n| n.name == target_type) {
let edge = SchemaEdge::new(
&uuid::Uuid::new_v4().to_string(),
node_id,
&target_node.id,
EdgeType::Field,
)
.with_source_field(&field.name);
state.graph.edges.push(edge);
state.rebuild_indices();
}
let history_pos = state.history_position;
state.history.truncate(history_pos);
state.history.push(EditOperation::AddField {
node_id: node_id.to_string(),
field,
});
state.history_position = state.history.len();
Ok(())
}
pub async fn remove_field(&self, node_id: &str, field_name: &str) -> Result<()> {
let mut state = self.state.write().await;
let Some(&index) = state.node_index.get(node_id) else {
return Err(anyhow!("Node '{}' not found", node_id));
};
state.graph.nodes[index]
.fields
.retain(|f| f.name != field_name);
state.graph.nodes[index].size.height = state.graph.nodes[index].calculate_height();
state
.graph
.edges
.retain(|e| e.source != node_id || e.source_field.as_deref() != Some(field_name));
state.rebuild_indices();
let history_pos = state.history_position;
state.history.truncate(history_pos);
state.history.push(EditOperation::RemoveField {
node_id: node_id.to_string(),
field_name: field_name.to_string(),
});
state.history_position = state.history.len();
Ok(())
}
pub async fn move_node(&self, node_id: &str, position: Position) -> Result<()> {
let mut state = self.state.write().await;
let Some(&index) = state.node_index.get(node_id) else {
return Err(anyhow!("Node '{}' not found", node_id));
};
state.graph.nodes[index].position = position;
Ok(())
}
pub async fn apply_layout(&self) -> Result<()> {
let mut state = self.state.write().await;
match state.layout_options.algorithm {
LayoutAlgorithm::ForceDirected => {
self.apply_force_directed_layout(&mut state)?;
}
LayoutAlgorithm::Hierarchical => {
self.apply_hierarchical_layout(&mut state)?;
}
LayoutAlgorithm::Circular => {
self.apply_circular_layout(&mut state)?;
}
LayoutAlgorithm::Grid => {
self.apply_grid_layout(&mut state)?;
}
LayoutAlgorithm::Manual => {
}
}
Ok(())
}
fn apply_force_directed_layout(&self, state: &mut DesignerState) -> Result<()> {
let node_count = state.graph.nodes.len();
if node_count == 0 {
return Ok(());
}
for node in &mut state.graph.nodes {
if node.position.x == 0.0 && node.position.y == 0.0 {
node.position.x = fastrand::f64() * 800.0;
node.position.y = fastrand::f64() * 600.0;
}
}
let spacing = state.layout_options.node_spacing;
let iterations = state.layout_options.iterations;
for _ in 0..iterations {
let mut forces: Vec<Position> = vec![Position::default(); node_count];
for i in 0..node_count {
for j in (i + 1)..node_count {
let dx = state.graph.nodes[j].position.x - state.graph.nodes[i].position.x;
let dy = state.graph.nodes[j].position.y - state.graph.nodes[i].position.y;
let dist = (dx * dx + dy * dy).sqrt().max(1.0);
let repulsion = spacing * spacing / dist;
let fx = repulsion * dx / dist;
let fy = repulsion * dy / dist;
forces[i].x -= fx;
forces[i].y -= fy;
forces[j].x += fx;
forces[j].y += fy;
}
}
for edge in &state.graph.edges {
let Some(&source_idx) = state.node_index.get(&edge.source) else {
continue;
};
let Some(&target_idx) = state.node_index.get(&edge.target) else {
continue;
};
let dx = state.graph.nodes[target_idx].position.x
- state.graph.nodes[source_idx].position.x;
let dy = state.graph.nodes[target_idx].position.y
- state.graph.nodes[source_idx].position.y;
let dist = (dx * dx + dy * dy).sqrt();
let attraction = dist / spacing;
let fx = attraction * dx / dist.max(1.0);
let fy = attraction * dy / dist.max(1.0);
forces[source_idx].x += fx * 0.1;
forces[source_idx].y += fy * 0.1;
forces[target_idx].x -= fx * 0.1;
forces[target_idx].y -= fy * 0.1;
}
let damping = 0.85;
for (i, node) in state.graph.nodes.iter_mut().enumerate() {
node.position.x += forces[i].x * damping;
node.position.y += forces[i].y * damping;
node.position.x = node.position.x.max(50.0).min(state.graph.width - 50.0);
node.position.y = node.position.y.max(50.0).min(state.graph.height - 50.0);
}
}
Ok(())
}
fn apply_hierarchical_layout(&self, state: &mut DesignerState) -> Result<()> {
if state.graph.nodes.is_empty() {
return Ok(());
}
let spacing = state.layout_options.node_spacing;
let layer_spacing = state.layout_options.layer_spacing;
let groups: Vec<Vec<usize>> = if state.layout_options.group_by_type {
let mut type_groups: HashMap<NodeType, Vec<usize>> = HashMap::new();
for (i, node) in state.graph.nodes.iter().enumerate() {
type_groups.entry(node.node_type).or_default().push(i);
}
type_groups.into_values().collect()
} else {
vec![(0..state.graph.nodes.len()).collect()]
};
let mut y = 50.0;
for group in groups {
let mut x = 50.0;
for idx in group {
state.graph.nodes[idx].position.x = x;
state.graph.nodes[idx].position.y = y;
x += state.graph.nodes[idx].size.width + spacing;
}
y += layer_spacing;
}
Ok(())
}
fn apply_circular_layout(&self, state: &mut DesignerState) -> Result<()> {
let node_count = state.graph.nodes.len();
if node_count == 0 {
return Ok(());
}
let center_x = state.graph.width / 2.0;
let center_y = state.graph.height / 2.0;
let radius = (state.graph.width.min(state.graph.height) / 2.0) - 150.0;
for (i, node) in state.graph.nodes.iter_mut().enumerate() {
let angle = 2.0 * std::f64::consts::PI * (i as f64) / (node_count as f64);
node.position.x = center_x + radius * angle.cos();
node.position.y = center_y + radius * angle.sin();
}
Ok(())
}
fn apply_grid_layout(&self, state: &mut DesignerState) -> Result<()> {
let node_count = state.graph.nodes.len();
if node_count == 0 {
return Ok(());
}
let cols = (node_count as f64).sqrt().ceil() as usize;
let spacing = state.layout_options.node_spacing;
for (i, node) in state.graph.nodes.iter_mut().enumerate() {
let row = i / cols;
let col = i % cols;
node.position.x = 50.0 + (col as f64) * (node.size.width + spacing);
node.position.y = 50.0 + (row as f64) * (node.size.height + spacing);
}
Ok(())
}
pub async fn set_layout_options(&self, options: LayoutOptions) {
let mut state = self.state.write().await;
state.layout_options = options;
}
pub async fn validate(&self) -> ValidationResult {
let state = self.state.read().await;
let mut errors = Vec::new();
let mut warnings = Vec::new();
if !state.graph.nodes.iter().any(|n| n.name == "Query") {
errors.push(ValidationError {
code: "MISSING_QUERY".to_string(),
message: "Schema must have a Query type".to_string(),
node_id: None,
field_name: None,
});
}
let type_names: HashSet<_> = state.graph.nodes.iter().map(|n| n.name.as_str()).collect();
let builtin_types: HashSet<&str> = ["String", "Int", "Float", "Boolean", "ID"]
.iter()
.copied()
.collect();
for node in &state.graph.nodes {
for field in &node.fields {
let field_type = field
.field_type
.trim_matches(|c| c == '[' || c == ']' || c == '!');
if !type_names.contains(field_type) && !builtin_types.contains(field_type) {
errors.push(ValidationError {
code: "UNRESOLVED_TYPE".to_string(),
message: format!("Unknown type '{}' in field '{}'", field_type, field.name),
node_id: Some(node.id.clone()),
field_name: Some(field.name.clone()),
});
}
}
if matches!(
node.node_type,
NodeType::ObjectType | NodeType::InterfaceType | NodeType::InputType
) && node.fields.is_empty()
{
warnings.push(ValidationWarning {
code: "EMPTY_TYPE".to_string(),
message: format!("Type '{}' has no fields", node.name),
node_id: Some(node.id.clone()),
});
}
if node.node_type == NodeType::EnumType && node.enum_values.is_empty() {
errors.push(ValidationError {
code: "EMPTY_ENUM".to_string(),
message: format!("Enum '{}' has no values", node.name),
node_id: Some(node.id.clone()),
field_name: None,
});
}
}
ValidationResult {
valid: errors.is_empty(),
errors,
warnings,
}
}
pub async fn export_sdl(&self) -> String {
let state = self.state.read().await;
let mut sdl = String::new();
for node in &state.graph.nodes {
if let Some(desc) = &node.description {
sdl.push_str(&format!("\"\"\"{}\"\"\"\n", desc));
}
match node.node_type {
NodeType::ObjectType => {
let implements = if !node.implements.is_empty() {
format!(" implements {}", node.implements.join(" & "))
} else {
String::new()
};
sdl.push_str(&format!("type {}{} {{\n", node.name, implements));
for field in &node.fields {
if let Some(desc) = &field.description {
sdl.push_str(&format!(" \"\"\"{}\"\"\"\n", desc));
}
sdl.push_str(&format!(" {}: {}\n", field.name, field.type_string()));
}
sdl.push_str("}\n\n");
}
NodeType::InterfaceType => {
sdl.push_str(&format!("interface {} {{\n", node.name));
for field in &node.fields {
sdl.push_str(&format!(" {}: {}\n", field.name, field.type_string()));
}
sdl.push_str("}\n\n");
}
NodeType::InputType => {
sdl.push_str(&format!("input {} {{\n", node.name));
for field in &node.fields {
sdl.push_str(&format!(" {}: {}\n", field.name, field.type_string()));
}
sdl.push_str("}\n\n");
}
NodeType::EnumType => {
sdl.push_str(&format!("enum {} {{\n", node.name));
for value in &node.enum_values {
sdl.push_str(&format!(" {}\n", value.name));
}
sdl.push_str("}\n\n");
}
NodeType::UnionType => {
sdl.push_str(&format!(
"union {} = {}\n\n",
node.name,
node.union_members.join(" | ")
));
}
NodeType::ScalarType => {
sdl.push_str(&format!("scalar {}\n\n", node.name));
}
NodeType::Directive => {
}
}
}
sdl
}
pub async fn import_sdl(&self, sdl: &str) -> Result<()> {
let mut state = self.state.write().await;
state.graph.nodes.clear();
state.graph.edges.clear();
let lines: Vec<&str> = sdl.lines().collect();
let mut current_node: Option<SchemaNode> = None;
let mut in_type = false;
for line in lines {
let line = line.trim();
if line.starts_with("type ")
|| line.starts_with("interface ")
|| line.starts_with("input ")
|| line.starts_with("enum ")
{
if let Some(node) = current_node.take() {
state.graph.nodes.push(node);
}
let (kind, rest) = if let Some(rest) = line.strip_prefix("type ") {
(NodeType::ObjectType, rest)
} else if let Some(rest) = line.strip_prefix("interface ") {
(NodeType::InterfaceType, rest)
} else if let Some(rest) = line.strip_prefix("input ") {
(NodeType::InputType, rest)
} else if let Some(rest) = line.strip_prefix("enum ") {
(NodeType::EnumType, rest)
} else {
continue;
};
let name = rest
.split(|c: char| c.is_whitespace() || c == '{')
.next()
.unwrap_or("");
current_node = Some(SchemaNode::new(
&uuid::Uuid::new_v4().to_string(),
name,
kind,
));
in_type = true;
} else if line == "}" {
if let Some(node) = current_node.take() {
state.graph.nodes.push(node);
}
in_type = false;
} else if in_type && !line.is_empty() && !line.starts_with("\"\"\"") {
if let Some(ref mut node) = current_node {
if let Some((name, type_part)) = line.split_once(':') {
let name = name.trim();
let type_str = type_part.trim().trim_end_matches(',');
let field = FieldDefinition::new(name, type_str);
node.fields.push(field);
} else if node.node_type == NodeType::EnumType {
node.enum_values.push(EnumValue::new(line));
}
}
} else if let Some(rest) = line.strip_prefix("scalar ") {
let name = rest.trim();
let node = SchemaNode::new(
&uuid::Uuid::new_v4().to_string(),
name,
NodeType::ScalarType,
);
state.graph.nodes.push(node);
} else if let Some(rest) = line.strip_prefix("union ") {
if let Some((name, members)) = rest.split_once('=') {
let name = name.trim();
let members: Vec<String> =
members.split('|').map(|s| s.trim().to_string()).collect();
let mut node = SchemaNode::new(
&uuid::Uuid::new_v4().to_string(),
name,
NodeType::UnionType,
);
node.union_members = members;
state.graph.nodes.push(node);
}
}
}
if let Some(node) = current_node {
state.graph.nodes.push(node);
}
state.rebuild_indices();
self.create_edges_from_fields(&mut state);
drop(state);
self.apply_layout().await?;
Ok(())
}
fn create_edges_from_fields(&self, state: &mut DesignerState) {
let type_map: HashMap<String, String> = state
.graph
.nodes
.iter()
.map(|n| (n.name.clone(), n.id.clone()))
.collect();
let builtin_types: HashSet<&str> = ["String", "Int", "Float", "Boolean", "ID"]
.iter()
.copied()
.collect();
let mut edges = Vec::new();
for node in &state.graph.nodes {
for field in &node.fields {
let field_type = field
.field_type
.trim_matches(|c| c == '[' || c == ']' || c == '!');
if builtin_types.contains(field_type) {
continue;
}
if let Some(target_id) = type_map.get(field_type) {
let edge = SchemaEdge::new(
&uuid::Uuid::new_v4().to_string(),
&node.id,
target_id,
EdgeType::Field,
)
.with_source_field(&field.name);
edges.push(edge);
}
}
for interface in &node.implements {
if let Some(target_id) = type_map.get(interface) {
let edge = SchemaEdge::new(
&uuid::Uuid::new_v4().to_string(),
&node.id,
target_id,
EdgeType::Implements,
);
edges.push(edge);
}
}
for member in &node.union_members {
if let Some(target_id) = type_map.get(member) {
let edge = SchemaEdge::new(
&uuid::Uuid::new_v4().to_string(),
&node.id,
target_id,
EdgeType::UnionMember,
);
edges.push(edge);
}
}
}
state.graph.edges = edges;
state.rebuild_indices();
}
pub async fn can_undo(&self) -> bool {
let state = self.state.read().await;
state.history_position > 0
}
pub async fn undo(&self) -> Result<()> {
let mut state = self.state.write().await;
if state.history_position == 0 {
return Err(anyhow!("Nothing to undo"));
}
state.history_position -= 1;
Ok(())
}
pub async fn can_redo(&self) -> bool {
let state = self.state.read().await;
state.history_position < state.history.len()
}
pub async fn redo(&self) -> Result<()> {
let mut state = self.state.write().await;
if state.history_position >= state.history.len() {
return Err(anyhow!("Nothing to redo"));
}
state.history_position += 1;
Ok(())
}
}
impl Default for SchemaDesigner {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_designer_creation() {
let designer = SchemaDesigner::new();
let graph = designer.get_visualization().await;
assert!(graph.nodes.is_empty());
}
#[tokio::test]
async fn test_add_node() {
let designer = SchemaDesigner::new();
let node = SchemaNode::new("", "User", NodeType::ObjectType);
let node_id = designer.add_node(node).await.expect("should succeed");
let graph = designer.get_visualization().await;
assert_eq!(graph.nodes.len(), 1);
assert_eq!(graph.nodes[0].id, node_id);
}
#[tokio::test]
async fn test_add_field() {
let designer = SchemaDesigner::new();
let node = SchemaNode::new("", "User", NodeType::ObjectType);
let node_id = designer.add_node(node).await.expect("should succeed");
let field = FieldDefinition::new("name", "String").non_null();
designer
.add_field(&node_id, field)
.await
.expect("should succeed");
let graph = designer.get_visualization().await;
assert_eq!(graph.nodes[0].fields.len(), 1);
assert_eq!(graph.nodes[0].fields[0].name, "name");
}
#[tokio::test]
async fn test_validation() {
let designer = SchemaDesigner::new();
let result = designer.validate().await;
assert!(!result.valid);
assert!(result.errors.iter().any(|e| e.code == "MISSING_QUERY"));
let query = SchemaNode::new("", "Query", NodeType::ObjectType)
.with_field(FieldDefinition::new("hello", "String"));
designer.add_node(query).await.expect("should succeed");
let result = designer.validate().await;
assert!(result.valid);
}
#[tokio::test]
async fn test_export_sdl() {
let designer = SchemaDesigner::new();
let user = SchemaNode::new("", "User", NodeType::ObjectType)
.with_field(FieldDefinition::new("id", "ID").non_null())
.with_field(FieldDefinition::new("name", "String"));
designer.add_node(user).await.expect("should succeed");
let sdl = designer.export_sdl().await;
assert!(sdl.contains("type User"));
assert!(sdl.contains("id: ID!"));
assert!(sdl.contains("name: String"));
}
#[tokio::test]
async fn test_import_sdl() {
let designer = SchemaDesigner::new();
let sdl = r#"
type Query {
users: [User]
}
type User {
id: ID!
name: String
}
"#;
designer.import_sdl(sdl).await.expect("should succeed");
let graph = designer.get_visualization().await;
assert_eq!(graph.nodes.len(), 2);
}
#[tokio::test]
async fn test_layout_algorithms() {
let designer = SchemaDesigner::new();
for i in 0..5 {
let node = SchemaNode::new("", &format!("Type{}", i), NodeType::ObjectType);
designer.add_node(node).await.expect("should succeed");
}
designer
.set_layout_options(LayoutOptions {
algorithm: LayoutAlgorithm::ForceDirected,
..Default::default()
})
.await;
designer.apply_layout().await.expect("should succeed");
designer
.set_layout_options(LayoutOptions {
algorithm: LayoutAlgorithm::Circular,
..Default::default()
})
.await;
designer.apply_layout().await.expect("should succeed");
designer
.set_layout_options(LayoutOptions {
algorithm: LayoutAlgorithm::Grid,
..Default::default()
})
.await;
designer.apply_layout().await.expect("should succeed");
let graph = designer.get_visualization().await;
for node in &graph.nodes {
assert!(node.position.x > 0.0 || node.position.y > 0.0);
}
}
#[tokio::test]
async fn test_remove_node() {
let designer = SchemaDesigner::new();
let node = SchemaNode::new("", "User", NodeType::ObjectType);
let node_id = designer.add_node(node).await.expect("should succeed");
designer
.remove_node(&node_id)
.await
.expect("should succeed");
let graph = designer.get_visualization().await;
assert!(graph.nodes.is_empty());
}
#[tokio::test]
async fn test_field_type_string() {
let field = FieldDefinition::new("items", "Item").list().non_null();
assert_eq!(field.type_string(), "[Item]!");
}
#[tokio::test]
async fn test_position_distance() {
let p1 = Position::new(0.0, 0.0);
let p2 = Position::new(3.0, 4.0);
assert!((p1.distance_to(&p2) - 5.0).abs() < 0.0001);
}
}