rust语言

Rust graphql 入门之星球大战示例1

2021-07-24  本文已影响0人  子十一刻

星球大战 GraphQL 项目示例,基于 async-graphql + actix-web。项目结构略有修改

项目地址

原 github 示例仓库链接

1.创建项目

cargo new startwars

2.目录结构

项目结构

3. 添加依赖包

依赖包

4.main.rs 添加测试代码

bin/main.rs

use actix_web::{guard, web, App, HttpResponse, HttpServer, Result};
use async_graphql::http::{playground_source, GraphQLPlaygroundConfig};

async fn index() -> HttpResponse {
    HttpResponse::Ok().body("index test")
}

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<()> {
    println!("Playgroud: http://localhost:8000");

    HttpServer::new(move || {
        App::new()
            .service(
                web::resource("/").guard(guard::Post()).to(index)
            )
            .service(
                web::resource("/")
                    .guard(guard::Get()).to(index_playground)
            )
    })
        .bind("0.0.0.0:8000")?
        .run()
        .await
}

运行项目 cargo run 访问 http://localhost:8000

Graphql 调试界面

lib.rs

///
/// 星球大战
///

// 声名模块
pub mod common;
pub mod human;
pub mod droid;
pub mod query_root;
pub mod star_wars;

// 模块重新导出
pub use common::*;
pub use human::*;
pub use droid::*;
pub use query_root::*;
pub use star_wars::*;

common.rs

use crate::{Human, Droid, QueryRoot};
use async_graphql::{Interface, Enum, Schema, EmptyMutation, EmptySubscription};

pub type StarWarsSchema = Schema<QueryRoot, EmptyMutation, EmptySubscription>;

///
/// 星战系列
/// 正传三部曲
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
pub enum Episode {
    // 新希望
    NewHope,
    // 帝国反击战
    Empire,
    // 绝地归来
    Jedi
}

/// 角色类型
#[derive(Interface)]
#[graphql(
    field(name = "id", type = "&str"),
    field(name = "name", type = "&str"),
    field(name = "friends", type = "Vec<Character>"),
    field(name = "appears_in", type = "&'ctx [Episode]"),
)]
pub enum Character {
    // 人类
    Human(Human),
    // 机器人
    Droid(Droid),
}

/// 角色
pub struct StarWarsChar {
    pub id: &'static str,
    // 姓名
    pub name: &'static str,
    // 英文姓名
    pub en_name: &'static str,
    // 朋友
    pub friends: Vec<usize>,
    // 参与的电影系列
    pub appears_in: Vec<Episode>,
    // 籍贯行星
    pub home_planet: Option<&'static str>,
    // 主要功能
    pub primary_function: Option<&'static str>,
}

droid.rs

use super::{Character, Episode, StarWars};
use async_graphql::{Context, Object};

/// 机器人
pub struct Droid(pub usize);

#[Object]
impl Droid {
    pub async fn id(&self, ctx: &Context<'_>) -> &str {
        ctx.data_unchecked::<StarWars>().chars[self.0].id
    }

    pub async fn name(&self, ctx: &Context<'_>) -> &str {
        ctx.data_unchecked::<StarWars>().chars[self.0].name
    }

    pub async fn friends(&self, ctx: &Context<'_>) -> Vec<Character> {
        ctx.data_unchecked::<StarWars>().chars[self.0]
            .friends
            .iter()
            .map(|id| Droid(*id).into())
            .collect()
    }

    pub async fn appears_in<'a>(&self, ctx: &'a Context<'_>) -> &'a [Episode] {
        &ctx.data_unchecked::<StarWars>().chars[self.0].appears_in
    }

    pub async fn primary_function<'a>(&self, ctx: &'a Context<'_>) -> &'a Option<&'a str> {
        &ctx.data_unchecked::<StarWars>().chars[self.0].primary_function
    }
}

human.rs

use super::{Character, Episode, StarWars};
use async_graphql::{Context, Object};

/// 人类
pub struct Human(pub usize);

#[Object]
impl Human {
    pub async fn id(&self, ctx: &Context<'_>) -> &str {
        ctx.data_unchecked::<StarWars>().chars[self.0].id
    }

    pub async fn name(&self, ctx: &Context<'_>) -> &str {
        ctx.data_unchecked::<StarWars>().chars[self.0].name
    }

    pub async fn friends(&self, ctx: &Context<'_>) -> Vec<Character> {
        ctx.data_unchecked::<StarWars>().chars[self.0]
            .friends
            .iter()
            .map(|id| Human(*id).into())
            .collect()
    }

    pub async fn appears_in<'a>(&self, ctx: &'a Context<'_>) -> &'a [Episode] {
        &ctx.data_unchecked::<StarWars>().chars[self.0].appears_in
    }

    pub async fn home_planet<'a>(&self, ctx: &'a Context<'_>) -> &'a Option<&'a str> {
        &ctx.data_unchecked::<StarWars>().chars[self.0].home_planet
    }
}

star_wars.rs

use std::collections::HashMap;
use slab::Slab;
use crate::{StarWarsChar, Episode};

///
/// 星球大战所有数据初始化
///
pub struct StarWars {
    // 卢克·天行者
    pub luke: usize,
    // R2-D2
    pub artoo: usize,
    // 所有角色
    pub chars: Slab<StarWarsChar>,
    // 人类数据
    pub human_data: HashMap<&'static str, usize>,
    // 机器人数据
    pub 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: "卢克·天行者",
            en_name: "Luke Skywalker",
            friends: vec![],
            appears_in: vec![],
            home_planet: Some("塔图因星(Tatooine)"),
            primary_function: None,
        });

        // 卢克
        let vader = chars.insert(StarWarsChar {
            id: "1001",
            name: "卢克·天行者",
            en_name: "Luke Skywalker",
            friends: vec![],
            appears_in: vec![],
            home_planet: Some("塔图因星(Tatooine)"),
            primary_function: None,
        });

        // 汉
        let han = chars.insert(StarWarsChar {
            id: "1002",
            name: "汉·索罗",
            en_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: "莉亚·欧嘉纳",
            en_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: "威尔赫夫·塔金",
            en_name: "Wilhuff Tarkin",
            friends: vec![],
            appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi],
            home_planet: None,
            primary_function: None,
        });

        // 3po
        let threepio = chars.insert(StarWarsChar {
            id: "2000",
            name: "C-3PO",
            en_name: "C-3PO",
            friends: vec![],
            appears_in: vec![Episode::Empire, Episode::NewHope, Episode::Jedi],
            home_planet: None,
            primary_function: Some("礼仪机器人(Protocol)"),
        });

        // r2
        let artoo = chars.insert(StarWarsChar {
            id: "2001",
            name: "R2-D2",
            en_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()
    }

}

query_root.rs

use async_graphql::{Object, Context, FieldResult};
use async_graphql::connection::{query, Connection, Edge, EmptyFields};
use super::{Episode, Human, Droid, StarWars, Character};

pub struct QueryRoot;

#[Object]
impl QueryRoot {
    async fn hero(
        &self,
        ctx: &Context<'_>,
        #[graphql(
            desc = "无值返回所有系列的角色,有值返回指定的系列角色"
        )]
        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<'_>,
        #[graphql(desc = "人类ID")]
        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 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();

            // 如果有 after 参数 更新 start
            if let Some(after) = after {
                if after >= characters.len() {
                    return Ok(Connection::new(false, false));
                }
                start = after + 1;
            }

            // 如果有 before 参数 更新 end
            if let Some(before) = before {
                if before == 0 {
                    return Ok(Connection::new(false, false));
                }
                end = before;
            }

            let mut slice = &characters[start..end];

            // 如果有 first 参数
            if let Some(first) = first {
                // 更新切片为 first 参数指定大小的切片 最大是整个切片 从前向后取
                slice = &slice[..first.min(slice.len())];
                end -= first.min(slice.len());
            } else if let Some(last) = last {
                // 如果指定了 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
}

5. 测试运行并更新入口文件 main.rs

先测试无报错再修改 main.rs

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::{Request, Response};
use startwars1::{QueryRoot, StarWars, StarWarsSchema};

async fn index(schema: web::Data<StarWarsSchema>, req: Request) -> Response {
    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!("Playgroud: 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("0.0.0.0:8000")?
        .run()
        .await
}

请求示例:

按电影系列获取一个角色信息 获取指定ID的人类信息 获取带分页信息的数据列表
上一篇 下一篇

猜你喜欢

热点阅读