#![allow(
dead_code,
unused_imports,
clippy::default_constructed_unit_structs,
clippy::needless_borrow,
clippy::unnecessary_lazy_evaluations
)]
use armature::prelude::*;
use armature_graphql::{
EmptyMutation, EmptySubscription, GraphQLConfig, Object, Schema, SimpleObject, async_graphql,
generate_schema_docs_html, graphiql_html, graphql_playground_html,
};
#[derive(SimpleObject, Clone)]
struct User {
id: i32,
name: String,
email: String,
}
#[derive(SimpleObject, Clone)]
struct Post {
id: i32,
title: String,
content: String,
author_id: i32,
}
struct Query;
#[Object]
impl Query {
async fn user(&self, id: i32) -> Option<User> {
Some(User {
id,
name: format!("User {}", id),
email: format!("user{}@example.com", id),
})
}
async fn users(&self) -> Vec<User> {
vec![
User {
id: 1,
name: "Alice".to_string(),
email: "alice@example.com".to_string(),
},
User {
id: 2,
name: "Bob".to_string(),
email: "bob@example.com".to_string(),
},
]
}
async fn post(&self, id: i32) -> Option<Post> {
Some(Post {
id,
title: format!("Post {}", id),
content: format!("Content for post {}", id),
author_id: 1,
})
}
async fn posts(&self) -> Vec<Post> {
vec![
Post {
id: 1,
title: "First Post".to_string(),
content: "This is the first post".to_string(),
author_id: 1,
},
Post {
id: 2,
title: "Second Post".to_string(),
content: "This is the second post".to_string(),
author_id: 2,
},
]
}
}
#[injectable]
#[derive(Clone)]
struct GraphQLService {
schema: Schema<Query, EmptyMutation, EmptySubscription>,
config: GraphQLConfig,
}
impl Default for GraphQLService {
fn default() -> Self {
let schema = Schema::build(Query, EmptyMutation, EmptySubscription).finish();
let config = GraphQLConfig::development("/api/graphql")
.with_playground(true)
.with_graphiql(true)
.with_schema_docs(true);
Self { schema, config }
}
}
impl GraphQLService {
async fn execute_query(&self, query: &str) -> String {
let request = async_graphql::Request::new(query);
let response = self.schema.execute(request).await;
serde_json::to_string(&response).unwrap_or_else(|_| "{}".to_string())
}
}
#[controller("/api/graphql")]
#[derive(Default, Clone)]
struct GraphQLController;
#[routes]
impl GraphQLController {
#[post("")]
async fn graphql_endpoint(req: HttpRequest) -> Result<HttpResponse, Error> {
let service = GraphQLService::default();
let query_data: serde_json::Value =
serde_json::from_slice(&req.body).map_err(|e| Error::BadRequest(e.to_string()))?;
let query = query_data["query"]
.as_str()
.ok_or_else(|| Error::BadRequest("Missing query field".to_string()))?;
let result = service.execute_query(query).await;
Ok(HttpResponse::ok()
.with_header("Content-Type".to_string(), "application/json".to_string())
.with_body(result.into_bytes()))
}
#[get("/playground")]
async fn playground() -> Result<HttpResponse, Error> {
let service = GraphQLService::default();
if !service.config.enable_playground {
return Err(Error::NotFound("Playground disabled".to_string()));
}
let html = graphql_playground_html(&service.config.endpoint);
Ok(HttpResponse::ok()
.with_header("Content-Type".to_string(), "text/html".to_string())
.with_body(html.into_bytes()))
}
#[get("/graphiql")]
async fn graphiql() -> Result<HttpResponse, Error> {
let service = GraphQLService::default();
if !service.config.enable_graphiql {
return Err(Error::NotFound("GraphiQL disabled".to_string()));
}
let html = graphiql_html(&service.config.endpoint);
Ok(HttpResponse::ok()
.with_header("Content-Type".to_string(), "text/html".to_string())
.with_body(html.into_bytes()))
}
#[get("/schema")]
async fn schema_docs() -> Result<HttpResponse, Error> {
let service = GraphQLService::default();
if !service.config.enable_schema_docs {
return Err(Error::NotFound("Schema docs disabled".to_string()));
}
let html = generate_schema_docs_html(&service.schema, &service.config.endpoint, "Blog API");
Ok(HttpResponse::ok()
.with_header("Content-Type".to_string(), "text/html".to_string())
.with_body(html.into_bytes()))
}
#[get("/schema.graphql")]
async fn schema_sdl() -> Result<HttpResponse, Error> {
let service = GraphQLService::default();
let sdl = service.schema.sdl();
Ok(HttpResponse::ok()
.with_header("Content-Type".to_string(), "text/plain".to_string())
.with_header(
"Content-Disposition".to_string(),
"attachment; filename=\"schema.graphql\"".to_string(),
)
.with_body(sdl.into_bytes()))
}
}
#[module(
providers: [GraphQLService],
controllers: [GraphQLController]
)]
#[derive(Default, Clone)]
struct AppModule;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("🚀 Starting GraphQL server with documentation...\n");
println!("📍 Endpoints:");
println!(" - GraphQL API: http://localhost:4000/api/graphql");
println!(" - Playground: http://localhost:4000/api/graphql/playground");
println!(" - GraphiQL: http://localhost:4000/api/graphql/graphiql");
println!(" - Schema Docs: http://localhost:4000/api/graphql/schema");
println!(" - Schema SDL: http://localhost:4000/api/graphql/schema.graphql");
println!("\n💡 Try these queries in the playground:");
println!(" query {{ users {{ id name email }} }}");
println!(" query {{ posts {{ id title content authorId }} }}");
println!(" query {{ user(id: 1) {{ name email }} }}\n");
let app = Application::create::<AppModule>().await;
app.listen(4000).await?;
Ok(())
}