mirror of
https://github.com/actix/examples
synced 2024-11-23 22:41:07 +01:00
Add async-graphql example (#278)
This commit is contained in:
parent
d836b046c9
commit
e5f64562b1
@ -4,6 +4,7 @@ members = [
|
|||||||
"async_db",
|
"async_db",
|
||||||
"async_ex1",
|
"async_ex1",
|
||||||
"async_ex2",
|
"async_ex2",
|
||||||
|
"async-graphql-demo",
|
||||||
"async_pg",
|
"async_pg",
|
||||||
"awc_https",
|
"awc_https",
|
||||||
"basics",
|
"basics",
|
||||||
@ -51,5 +52,5 @@ members = [
|
|||||||
"websocket-autobahn",
|
"websocket-autobahn",
|
||||||
"websocket-chat",
|
"websocket-chat",
|
||||||
"websocket-chat-broker",
|
"websocket-chat-broker",
|
||||||
"websocket-tcp-chat",
|
"websocket-tcp-chat"
|
||||||
]
|
]
|
||||||
|
11
async-graphql-demo/Cargo.toml
Normal file
11
async-graphql-demo/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "async-graphql-demo"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["sunli <scott_s829@163.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
actix-web = "3.0.0"
|
||||||
|
async-graphql = "2.0.0-alpha.7"
|
||||||
|
async-graphql-actix-web = "2.0.0-alpha.7"
|
||||||
|
slab = "0.4.2"
|
33
async-graphql-demo/src/README.md
Normal file
33
async-graphql-demo/src/README.md
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
Getting started using [Async-graphql](https://github.com/async-graphql/async-graphql) with Actix web.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run --bin async-graphql-demo
|
||||||
|
```
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
GET http://127.0.0.1:8000/ GraphQL Playground UI
|
||||||
|
POST http://127.0.0.1:8000/ For GraphQL query
|
||||||
|
|
||||||
|
## Query Examples
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
{
|
||||||
|
humans {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
friends {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
appearsIn
|
||||||
|
homePlanet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
38
async-graphql-demo/src/main.rs
Normal file
38
async-graphql-demo/src/main.rs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
mod starwars;
|
||||||
|
|
||||||
|
use actix_web::{guard, web, App, HttpResponse, HttpServer, Result};
|
||||||
|
use async_graphql::http::{playground_source, GraphQLPlaygroundConfig};
|
||||||
|
use async_graphql::{EmptyMutation, EmptySubscription, Schema};
|
||||||
|
use async_graphql_actix_web::{GQLRequest, GQLResponse};
|
||||||
|
use starwars::{QueryRoot, StarWars, StarWarsSchema};
|
||||||
|
|
||||||
|
async fn index(schema: web::Data<StarWarsSchema>, req: GQLRequest) -> GQLResponse {
|
||||||
|
schema.execute(req.into_inner()).await.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn index_playground() -> Result<HttpResponse> {
|
||||||
|
Ok(HttpResponse::Ok()
|
||||||
|
.content_type("text/html; charset=utf-8")
|
||||||
|
.body(playground_source(
|
||||||
|
GraphQLPlaygroundConfig::new("/").subscription_endpoint("/"),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription)
|
||||||
|
.data(StarWars::new())
|
||||||
|
.finish();
|
||||||
|
|
||||||
|
println!("Playground: http://localhost:8000");
|
||||||
|
|
||||||
|
HttpServer::new(move || {
|
||||||
|
App::new()
|
||||||
|
.data(schema.clone())
|
||||||
|
.service(web::resource("/").guard(guard::Post()).to(index))
|
||||||
|
.service(web::resource("/").guard(guard::Get()).to(index_playground))
|
||||||
|
})
|
||||||
|
.bind("127.0.0.1:8000")?
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
139
async-graphql-demo/src/starwars/mod.rs
Normal file
139
async-graphql-demo/src/starwars/mod.rs
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
mod model;
|
||||||
|
|
||||||
|
use async_graphql::{EmptyMutation, EmptySubscription, Schema};
|
||||||
|
use model::Episode;
|
||||||
|
use slab::Slab;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
pub use model::QueryRoot;
|
||||||
|
pub type StarWarsSchema = Schema<QueryRoot, EmptyMutation, EmptySubscription>;
|
||||||
|
|
||||||
|
pub struct StarWarsChar {
|
||||||
|
id: &'static str,
|
||||||
|
name: &'static str,
|
||||||
|
friends: Vec<usize>,
|
||||||
|
appears_in: Vec<Episode>,
|
||||||
|
home_planet: Option<&'static str>,
|
||||||
|
primary_function: Option<&'static str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct StarWars {
|
||||||
|
luke: usize,
|
||||||
|
artoo: usize,
|
||||||
|
chars: Slab<StarWarsChar>,
|
||||||
|
human_data: HashMap<&'static str, usize>,
|
||||||
|
droid_data: HashMap<&'static str, usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StarWars {
|
||||||
|
#[allow(clippy::new_without_default)]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let mut chars = Slab::new();
|
||||||
|
|
||||||
|
let luke = chars.insert(StarWarsChar {
|
||||||
|
id: "1000",
|
||||||
|
name: "Luke Skywalker",
|
||||||
|
friends: vec![],
|
||||||
|
appears_in: vec![],
|
||||||
|
home_planet: Some("Tatooine"),
|
||||||
|
primary_function: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
let vader = chars.insert(StarWarsChar {
|
||||||
|
id: "1001",
|
||||||
|
name: "Luke Skywalker",
|
||||||
|
friends: vec![],
|
||||||
|
appears_in: vec![],
|
||||||
|
home_planet: Some("Tatooine"),
|
||||||
|
primary_function: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
let han = chars.insert(StarWarsChar {
|
||||||
|
id: "1002",
|
||||||
|
name: "Han Solo",
|
||||||
|
friends: vec![],
|
||||||
|
appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi],
|
||||||
|
home_planet: None,
|
||||||
|
primary_function: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
let leia = chars.insert(StarWarsChar {
|
||||||
|
id: "1003",
|
||||||
|
name: "Leia Organa",
|
||||||
|
friends: vec![],
|
||||||
|
appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi],
|
||||||
|
home_planet: Some("Alderaa"),
|
||||||
|
primary_function: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
let tarkin = chars.insert(StarWarsChar {
|
||||||
|
id: "1004",
|
||||||
|
name: "Wilhuff Tarkin",
|
||||||
|
friends: vec![],
|
||||||
|
appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi],
|
||||||
|
home_planet: None,
|
||||||
|
primary_function: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
let threepio = chars.insert(StarWarsChar {
|
||||||
|
id: "2000",
|
||||||
|
name: "C-3PO",
|
||||||
|
friends: vec![],
|
||||||
|
appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi],
|
||||||
|
home_planet: None,
|
||||||
|
primary_function: Some("Protocol"),
|
||||||
|
});
|
||||||
|
|
||||||
|
let artoo = chars.insert(StarWarsChar {
|
||||||
|
id: "2001",
|
||||||
|
name: "R2-D2",
|
||||||
|
friends: vec![],
|
||||||
|
appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi],
|
||||||
|
home_planet: None,
|
||||||
|
primary_function: Some("Astromech"),
|
||||||
|
});
|
||||||
|
|
||||||
|
chars[luke].friends = vec![han, leia, threepio, artoo];
|
||||||
|
chars[vader].friends = vec![tarkin];
|
||||||
|
chars[han].friends = vec![luke, leia, artoo];
|
||||||
|
chars[leia].friends = vec![luke, han, threepio, artoo];
|
||||||
|
chars[tarkin].friends = vec![vader];
|
||||||
|
chars[threepio].friends = vec![luke, han, leia, artoo];
|
||||||
|
chars[artoo].friends = vec![luke, han, leia];
|
||||||
|
|
||||||
|
let mut human_data = HashMap::new();
|
||||||
|
human_data.insert("1000", luke);
|
||||||
|
human_data.insert("1001", vader);
|
||||||
|
human_data.insert("1002", han);
|
||||||
|
human_data.insert("1003", leia);
|
||||||
|
human_data.insert("1004", tarkin);
|
||||||
|
|
||||||
|
let mut droid_data = HashMap::new();
|
||||||
|
droid_data.insert("2000", threepio);
|
||||||
|
droid_data.insert("2001", artoo);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
luke,
|
||||||
|
artoo,
|
||||||
|
chars,
|
||||||
|
human_data,
|
||||||
|
droid_data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn human(&self, id: &str) -> Option<usize> {
|
||||||
|
self.human_data.get(id).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn droid(&self, id: &str) -> Option<usize> {
|
||||||
|
self.droid_data.get(id).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn humans(&self) -> Vec<usize> {
|
||||||
|
self.human_data.values().cloned().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn droids(&self) -> Vec<usize> {
|
||||||
|
self.droid_data.values().cloned().collect()
|
||||||
|
}
|
||||||
|
}
|
225
async-graphql-demo/src/starwars/model.rs
Normal file
225
async-graphql-demo/src/starwars/model.rs
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
use super::StarWars;
|
||||||
|
use async_graphql::connection::{query, Connection, Edge, EmptyFields};
|
||||||
|
use async_graphql::{Context, FieldResult, GQLEnum, GQLInterface, GQLObject};
|
||||||
|
|
||||||
|
/// One of the films in the Star Wars Trilogy
|
||||||
|
#[derive(GQLEnum, Copy, Clone, Eq, PartialEq)]
|
||||||
|
pub enum Episode {
|
||||||
|
/// Released in 1977.
|
||||||
|
NewHope,
|
||||||
|
|
||||||
|
/// Released in 1980.
|
||||||
|
Empire,
|
||||||
|
|
||||||
|
/// Released in 1983.
|
||||||
|
Jedi,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Human(usize);
|
||||||
|
|
||||||
|
/// A humanoid creature in the Star Wars universe.
|
||||||
|
#[GQLObject]
|
||||||
|
impl Human {
|
||||||
|
/// The id of the human.
|
||||||
|
async fn id(&self, ctx: &Context<'_>) -> &str {
|
||||||
|
ctx.data_unchecked::<StarWars>().chars[self.0].id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The name of the human.
|
||||||
|
async fn name(&self, ctx: &Context<'_>) -> &str {
|
||||||
|
ctx.data_unchecked::<StarWars>().chars[self.0].name
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The friends of the human, or an empty list if they have none.
|
||||||
|
async fn friends(&self, ctx: &Context<'_>) -> Vec<Character> {
|
||||||
|
ctx.data_unchecked::<StarWars>().chars[self.0]
|
||||||
|
.friends
|
||||||
|
.iter()
|
||||||
|
.map(|id| Human(*id).into())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Which movies they appear in.
|
||||||
|
async fn appears_in<'a>(&self, ctx: &'a Context<'_>) -> &'a [Episode] {
|
||||||
|
&ctx.data_unchecked::<StarWars>().chars[self.0].appears_in
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The home planet of the human, or null if unknown.
|
||||||
|
async fn home_planet<'a>(&self, ctx: &'a Context<'_>) -> &'a Option<&'a str> {
|
||||||
|
&ctx.data_unchecked::<StarWars>().chars[self.0].home_planet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Droid(usize);
|
||||||
|
|
||||||
|
/// A mechanical creature in the Star Wars universe.
|
||||||
|
#[GQLObject]
|
||||||
|
impl Droid {
|
||||||
|
/// The id of the droid.
|
||||||
|
async fn id(&self, ctx: &Context<'_>) -> &str {
|
||||||
|
ctx.data_unchecked::<StarWars>().chars[self.0].id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The name of the droid.
|
||||||
|
async fn name(&self, ctx: &Context<'_>) -> &str {
|
||||||
|
ctx.data_unchecked::<StarWars>().chars[self.0].name
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The friends of the droid, or an empty list if they have none.
|
||||||
|
async fn friends(&self, ctx: &Context<'_>) -> Vec<Character> {
|
||||||
|
ctx.data_unchecked::<StarWars>().chars[self.0]
|
||||||
|
.friends
|
||||||
|
.iter()
|
||||||
|
.map(|id| Droid(*id).into())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Which movies they appear in.
|
||||||
|
async fn appears_in<'a>(&self, ctx: &'a Context<'_>) -> &'a [Episode] {
|
||||||
|
&ctx.data_unchecked::<StarWars>().chars[self.0].appears_in
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The primary function of the droid.
|
||||||
|
async fn primary_function<'a>(&self, ctx: &'a Context<'_>) -> &'a Option<&'a str> {
|
||||||
|
&ctx.data_unchecked::<StarWars>().chars[self.0].primary_function
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct QueryRoot;
|
||||||
|
|
||||||
|
#[GQLObject]
|
||||||
|
impl QueryRoot {
|
||||||
|
async fn hero(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
#[arg(
|
||||||
|
desc = "If omitted, returns the hero of the whole saga. If provided, returns the hero of that particular episode."
|
||||||
|
)]
|
||||||
|
episode: Episode,
|
||||||
|
) -> Character {
|
||||||
|
if episode == Episode::Empire {
|
||||||
|
Human(ctx.data_unchecked::<StarWars>().luke).into()
|
||||||
|
} else {
|
||||||
|
Droid(ctx.data_unchecked::<StarWars>().artoo).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn human(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
#[arg(desc = "id of the human")] id: String,
|
||||||
|
) -> Option<Human> {
|
||||||
|
ctx.data_unchecked::<StarWars>().human(&id).map(Human)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn humans(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
after: Option<String>,
|
||||||
|
before: Option<String>,
|
||||||
|
first: Option<i32>,
|
||||||
|
last: Option<i32>,
|
||||||
|
) -> FieldResult<Connection<usize, Human, EmptyFields, EmptyFields>> {
|
||||||
|
let humans = ctx
|
||||||
|
.data_unchecked::<StarWars>()
|
||||||
|
.humans()
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
query_characters(after, before, first, last, &humans)
|
||||||
|
.await
|
||||||
|
.map(|conn| conn.map_node(Human))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn droid(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
#[arg(desc = "id of the droid")] id: String,
|
||||||
|
) -> Option<Droid> {
|
||||||
|
ctx.data_unchecked::<StarWars>().droid(&id).map(Droid)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn droids(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
after: Option<String>,
|
||||||
|
before: Option<String>,
|
||||||
|
first: Option<i32>,
|
||||||
|
last: Option<i32>,
|
||||||
|
) -> FieldResult<Connection<usize, Droid, EmptyFields, EmptyFields>> {
|
||||||
|
let droids = ctx
|
||||||
|
.data_unchecked::<StarWars>()
|
||||||
|
.droids()
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
query_characters(after, before, first, last, &droids)
|
||||||
|
.await
|
||||||
|
.map(|conn| conn.map_node(Droid))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(GQLInterface)]
|
||||||
|
#[graphql(
|
||||||
|
field(name = "id", type = "&str", context),
|
||||||
|
field(name = "name", type = "&str", context),
|
||||||
|
field(name = "friends", type = "Vec<Character>", context),
|
||||||
|
field(name = "appears_in", type = "&'ctx [Episode]", context)
|
||||||
|
)]
|
||||||
|
pub enum Character {
|
||||||
|
Human(Human),
|
||||||
|
Droid(Droid),
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn query_characters(
|
||||||
|
after: Option<String>,
|
||||||
|
before: Option<String>,
|
||||||
|
first: Option<i32>,
|
||||||
|
last: Option<i32>,
|
||||||
|
characters: &[usize],
|
||||||
|
) -> FieldResult<Connection<usize, usize, EmptyFields, EmptyFields>> {
|
||||||
|
query(
|
||||||
|
after,
|
||||||
|
before,
|
||||||
|
first,
|
||||||
|
last,
|
||||||
|
|after, before, first, last| async move {
|
||||||
|
let mut start = 0usize;
|
||||||
|
let mut end = characters.len();
|
||||||
|
|
||||||
|
if let Some(after) = after {
|
||||||
|
if after >= characters.len() {
|
||||||
|
return Ok(Connection::new(false, false));
|
||||||
|
}
|
||||||
|
start = after + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(before) = before {
|
||||||
|
if before == 0 {
|
||||||
|
return Ok(Connection::new(false, false));
|
||||||
|
}
|
||||||
|
end = before;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut slice = &characters[start..end];
|
||||||
|
|
||||||
|
if let Some(first) = first {
|
||||||
|
slice = &slice[..first.min(slice.len())];
|
||||||
|
end -= first.min(slice.len());
|
||||||
|
} else if let Some(last) = last {
|
||||||
|
slice = &slice[slice.len() - last.min(slice.len())..];
|
||||||
|
start = end - last.min(slice.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut connection = Connection::new(start > 0, end < characters.len());
|
||||||
|
connection.append(
|
||||||
|
slice
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(idx, item)| Edge::new(start + idx, *item)),
|
||||||
|
);
|
||||||
|
Ok(connection)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user