pub mod derivation;
pub mod naming;
pub mod validation;
#[cfg(test)]
mod tests;
use std::{collections::HashMap, fmt};
use derivation::derive_resource;
use fraiseql_core::schema::{CompiledSchema, MutationDefinition, QueryDefinition};
use tracing::debug;
use validation::{detect_conflicts, is_filtered_out, should_skip_query};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum HttpMethod {
Get,
Post,
Put,
Patch,
Delete,
}
impl fmt::Display for HttpMethod {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Get => write!(f, "GET"),
Self::Post => write!(f, "POST"),
Self::Put => write!(f, "PUT"),
Self::Patch => write!(f, "PATCH"),
Self::Delete => write!(f, "DELETE"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum UpdateCoverage {
Full,
Partial,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum RouteSource {
Query {
name: String,
},
Mutation {
name: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RestRoute {
pub method: HttpMethod,
pub path: String,
pub source: RouteSource,
pub update_coverage: Option<UpdateCoverage>,
pub success_status: u16,
}
#[derive(Debug, Clone)]
pub struct RestResource {
pub name: String,
pub type_name: String,
pub id_arg: Option<String>,
pub routes: Vec<RestRoute>,
}
#[derive(Debug, Clone)]
pub struct RestRouteTable {
pub base_path: String,
pub resources: Vec<RestResource>,
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Diagnostic {
pub level: DiagnosticLevel,
pub message: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum DiagnosticLevel {
Info,
Warning,
Error,
}
impl RestRouteTable {
pub fn from_compiled_schema(schema: &CompiledSchema) -> Result<Self, String> {
let config = schema.rest_config.clone().unwrap_or_default();
let base_path = config.path.clone();
let mut query_groups: HashMap<&str, Vec<&QueryDefinition>> = HashMap::new();
let mut mutation_groups: HashMap<&str, Vec<&MutationDefinition>> = HashMap::new();
for q in &schema.queries {
if should_skip_query(q) {
debug!(query = %q.name, "skipping query (aggregate/window/scalar)");
continue;
}
if is_filtered_out(&q.name, &config) {
debug!(query = %q.name, "skipping query (include/exclude filter)");
continue;
}
if schema.find_type(&q.return_type).is_none() {
debug!(query = %q.name, return_type = %q.return_type, "skipping query (no TypeDefinition)");
continue;
}
query_groups.entry(q.return_type.as_str()).or_default().push(q);
}
for m in &schema.mutations {
if is_filtered_out(&m.name, &config) {
debug!(mutation = %m.name, "skipping mutation (include/exclude filter)");
continue;
}
if schema.find_type(&m.return_type).is_none() {
debug!(mutation = %m.name, return_type = %m.return_type, "skipping mutation (no TypeDefinition)");
continue;
}
mutation_groups.entry(m.return_type.as_str()).or_default().push(m);
}
let mut all_types: Vec<&str> = query_groups.keys().copied().collect();
for t in mutation_groups.keys() {
if !all_types.contains(t) {
all_types.push(t);
}
}
all_types.sort_unstable();
let mut resources = Vec::new();
let mut diagnostics = Vec::new();
for type_name in all_types {
let Some(type_def) = schema.find_type(type_name) else {
continue;
};
let queries = query_groups.get(type_name).map_or(&[][..], |v| v.as_slice());
let mutations = mutation_groups.get(type_name).map_or(&[][..], |v| v.as_slice());
let resource =
derive_resource(type_name, type_def, queries, mutations, &config, &mut diagnostics);
if let Some(r) = resource {
resources.push(r);
}
}
detect_conflicts(&resources, &mut diagnostics)?;
let table = Self {
base_path,
resources,
diagnostics,
};
Ok(table)
}
}
impl fmt::Display for RestRouteTable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "REST Route Table (base: {})", self.base_path)?;
for resource in &self.resources {
writeln!(f, " Resource: {} (type: {})", resource.name, resource.type_name)?;
for route in &resource.routes {
writeln!(f, " {} {}{}", route.method, self.base_path, route.path)?;
}
}
Ok(())
}
}