├── docs ├── .gitignore ├── src │ ├── types │ │ ├── index.md │ │ ├── enum.md │ │ ├── input_object.md │ │ ├── union.md │ │ ├── scalar.md │ │ ├── interface.md │ │ ├── object.md │ │ └── directive.md │ ├── schema │ │ ├── index.md │ │ ├── mutation.md │ │ └── query.md │ ├── roadmap.md │ ├── introduction.md │ ├── SUMMARY.md │ ├── directory_structure.md │ ├── error_handling.md │ └── getting_started.md └── book.toml ├── .gitignore ├── cli ├── .gitignore ├── src │ ├── code_generate │ │ ├── project │ │ │ ├── axum │ │ │ │ ├── mod.rs │ │ │ │ ├── cargo_toml_file.rs │ │ │ │ └── main_file.rs │ │ │ ├── gitignore_file.rs │ │ │ ├── example_schema_file.rs │ │ │ └── mod.rs │ │ ├── root_mod_file.rs │ │ ├── mod_file.rs │ │ ├── operation │ │ │ ├── field_file.rs │ │ │ ├── mod.rs │ │ │ └── operation_mod_file.rs │ │ ├── type_definition │ │ │ └── scalar_file.rs │ │ ├── util.rs │ │ ├── directive │ │ │ └── mod.rs │ │ └── mod.rs │ ├── exit_codes.rs │ ├── app.rs │ └── main.rs └── Cargo.toml ├── .DS_Store ├── examples └── axum │ ├── src │ ├── graphql │ │ ├── scalar │ │ │ ├── mod.rs │ │ │ └── date_time.rs │ │ ├── input │ │ │ ├── mod.rs │ │ │ └── review_input.rs │ │ ├── directive │ │ │ ├── mod.rs │ │ │ └── possible_types.rs │ │ ├── resolver │ │ │ ├── length_unit.rs │ │ │ ├── search_result.rs │ │ │ ├── episode.rs │ │ │ ├── friends_edge.rs │ │ │ ├── mod.rs │ │ │ ├── review.rs │ │ │ ├── friends_connection.rs │ │ │ ├── page_info.rs │ │ │ ├── character.rs │ │ │ ├── droid.rs │ │ │ └── human.rs │ │ ├── query │ │ │ ├── reviews.rs │ │ │ ├── droid.rs │ │ │ ├── human.rs │ │ │ ├── hero.rs │ │ │ ├── character.rs │ │ │ ├── search.rs │ │ │ └── mod.rs │ │ ├── mod.rs │ │ └── mutation │ │ │ ├── create_review.rs │ │ │ └── mod.rs │ ├── main.rs │ └── starwars.rs │ ├── Cargo.toml │ └── schemas │ └── schema.graphql ├── frameworks ├── axum │ ├── src │ │ ├── lib.rs │ │ ├── response.rs │ │ └── request.rs │ └── Cargo.toml └── actix_web │ ├── src │ ├── lib.rs │ ├── request.rs │ └── response.rs │ └── Cargo.toml ├── tests ├── schemas │ ├── enum.graphql │ ├── extend.graphql │ ├── input_object.graphql │ ├── custom_scalar.graphql │ ├── interface.graphql │ ├── custom_directive.graphql │ ├── union.graphql │ ├── test_schema.graphql │ └── starwars.graphql ├── with_lifetime.rs ├── enum.rs ├── input_object.rs ├── mutation.rs ├── variables.rs ├── custom_scalar.rs ├── interface.rs ├── custom_directive.rs ├── fragment.rs └── union.rs ├── src ├── types │ ├── introspection │ │ ├── mod.rs │ │ ├── enum_value.rs │ │ ├── schema.rs │ │ ├── introspection_sdl.rs │ │ └── input_value.rs │ ├── union_type.rs │ ├── mod.rs │ ├── object.rs │ ├── interface.rs │ ├── id.rs │ ├── input_object.rs │ ├── argument.rs │ ├── field.rs │ ├── enum_type.rs │ ├── scalar.rs │ └── value_type.rs ├── custom_directive.rs ├── variables.rs ├── input │ ├── optional.rs │ ├── boolean.rs │ ├── string.rs │ ├── id.rs │ ├── mod.rs │ └── object.rs ├── resolver │ ├── boolean.rs │ ├── id.rs │ ├── optional.rs │ ├── string.rs │ └── object.rs ├── test_utils.rs ├── validation │ ├── rules │ │ ├── possible_fragment_spreads.rs │ │ ├── mod.rs │ │ ├── unique_variable_names.rs │ │ ├── known_fragment_names.rs │ │ ├── variables_are_input_types.rs │ │ ├── arguments_of_correct_type.rs │ │ ├── known_type_names.rs │ │ ├── unique_argument_names.rs │ │ └── fields_on_correct_type.rs │ ├── mod.rs │ └── test_utils.rs ├── container.rs ├── response.rs ├── query_root.rs ├── playground_html.rs ├── executor.rs └── request.rs ├── README.md ├── .github └── workflows │ ├── book.yml │ ├── release.yml │ └── ci.yml ├── macro ├── Cargo.toml └── src │ ├── scalar.rs │ ├── interface.rs │ ├── lib.rs │ ├── union.rs │ ├── utils.rs │ └── input_object.rs ├── Cargo.toml └── LICENSE /docs/.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /macro/target 3 | Cargo.lock 4 | .vscode/ 5 | -------------------------------------------------------------------------------- /cli/.gitignore: -------------------------------------------------------------------------------- 1 | graphql/** 2 | sample/** 3 | src/graphql 4 | /schema 5 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tak-Iwamoto/rusty-gql/HEAD/.DS_Store -------------------------------------------------------------------------------- /examples/axum/src/graphql/scalar/mod.rs: -------------------------------------------------------------------------------- 1 | mod date_time; 2 | 3 | pub use date_time::DateTime; 4 | -------------------------------------------------------------------------------- /docs/src/types/index.md: -------------------------------------------------------------------------------- 1 | # Types 2 | 3 | rusty-gql generates GraphQL types as Rust codes from schemas. 4 | -------------------------------------------------------------------------------- /examples/axum/src/graphql/input/mod.rs: -------------------------------------------------------------------------------- 1 | mod review_input; 2 | 3 | pub use review_input::ReviewInput; 4 | -------------------------------------------------------------------------------- /examples/axum/src/graphql/directive/mod.rs: -------------------------------------------------------------------------------- 1 | mod possible_types; 2 | 3 | pub use possible_types::possibleTypes; 4 | -------------------------------------------------------------------------------- /frameworks/axum/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod request; 2 | mod response; 3 | 4 | pub use request::GqlRequest; 5 | pub use response::GqlResponse; 6 | -------------------------------------------------------------------------------- /tests/schemas/enum.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | enum_value: SampleEnum 3 | } 4 | 5 | enum SampleEnum { 6 | Value0 7 | Value1 8 | } 9 | -------------------------------------------------------------------------------- /docs/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Tak-Iwamoto"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "rusty-gql Book" 7 | -------------------------------------------------------------------------------- /tests/schemas/extend.graphql: -------------------------------------------------------------------------------- 1 | extend type Query { 2 | authors: [Author] 3 | } 4 | 5 | type Author { 6 | name: String! 7 | description: String 8 | } 9 | -------------------------------------------------------------------------------- /cli/src/code_generate/project/axum/mod.rs: -------------------------------------------------------------------------------- 1 | mod cargo_toml_file; 2 | mod main_file; 3 | 4 | pub use cargo_toml_file::AxumCargoTomlFile; 5 | pub use main_file::AxumMainFile; 6 | -------------------------------------------------------------------------------- /tests/schemas/input_object.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | input_test(input: InputObj): String! 3 | } 4 | 5 | input InputObj { 6 | str_value: String! 7 | int_value: Int 8 | } 9 | -------------------------------------------------------------------------------- /tests/schemas/custom_scalar.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | test_custom_scalar: SampleResponse! 3 | } 4 | 5 | type SampleResponse { 6 | test: CustomScalar! 7 | } 8 | 9 | scalar CustomScalar 10 | -------------------------------------------------------------------------------- /docs/src/schema/index.md: -------------------------------------------------------------------------------- 1 | # Schema 2 | 3 | rusty-gql supports Query and Mutation. (Subscription is work in progress.) 4 | 5 | These will be generated automatically when we create a rusty-gql project. 6 | -------------------------------------------------------------------------------- /examples/axum/src/graphql/resolver/length_unit.rs: -------------------------------------------------------------------------------- 1 | #![allow(warnings, unused)] 2 | use crate::graphql::*; 3 | use rusty_gql::*; 4 | 5 | #[derive(GqlEnum)] 6 | pub enum LengthUnit { 7 | METER, 8 | FOOT, 9 | } 10 | -------------------------------------------------------------------------------- /examples/axum/src/graphql/resolver/search_result.rs: -------------------------------------------------------------------------------- 1 | #![allow(warnings, unused)] 2 | use crate::graphql::*; 3 | use rusty_gql::*; 4 | 5 | #[derive(GqlUnion)] 6 | pub enum SearchResult { 7 | Human(Human), 8 | Droid(Droid), 9 | } 10 | -------------------------------------------------------------------------------- /docs/src/roadmap.md: -------------------------------------------------------------------------------- 1 | # Roadmap 2 | 3 | The following features will be implemented. 4 | 5 | - Subscription 6 | - Dataloader 7 | - Calculate Query complexity 8 | - Apollo tracing 9 | - Apollo Federation 10 | - Automatic Persisted Query 11 | - etc. 12 | -------------------------------------------------------------------------------- /examples/axum/src/graphql/query/reviews.rs: -------------------------------------------------------------------------------- 1 | #![allow(warnings, unused)] 2 | use crate::{graphql::*, starwars::all_reviews}; 3 | use rusty_gql::*; 4 | 5 | pub async fn reviews(ctx: &Context<'_>, episode: Episode) -> Vec { 6 | all_reviews() 7 | } 8 | -------------------------------------------------------------------------------- /tests/with_lifetime.rs: -------------------------------------------------------------------------------- 1 | use rusty_gql::*; 2 | #[allow(dead_code)] 3 | 4 | struct Test<'a> { 5 | test: &'a str, 6 | } 7 | 8 | #[GqlType] 9 | impl<'a> Test<'a> { 10 | async fn value(&self) -> &'a str { 11 | self.test 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/axum/src/graphql/input/review_input.rs: -------------------------------------------------------------------------------- 1 | #![allow(warnings, unused)] 2 | use crate::graphql::*; 3 | use rusty_gql::*; 4 | 5 | #[derive(GqlInputObject)] 6 | pub struct ReviewInput { 7 | pub stars: i32, 8 | pub commentary: Option, 9 | } 10 | -------------------------------------------------------------------------------- /examples/axum/src/graphql/resolver/episode.rs: -------------------------------------------------------------------------------- 1 | #![allow(warnings, unused)] 2 | use crate::graphql::*; 3 | use rusty_gql::*; 4 | 5 | #[derive(GqlEnum, Debug, Copy, Clone, Eq, PartialEq)] 6 | pub enum Episode { 7 | NEWHOPE, 8 | EMPIRE, 9 | JEDI, 10 | } 11 | -------------------------------------------------------------------------------- /examples/axum/src/graphql/mod.rs: -------------------------------------------------------------------------------- 1 | mod directive; 2 | mod input; 3 | mod mutation; 4 | mod query; 5 | mod resolver; 6 | mod scalar; 7 | 8 | pub use directive::*; 9 | pub use input::*; 10 | pub use mutation::*; 11 | pub use query::*; 12 | pub use resolver::*; 13 | pub use scalar::*; 14 | -------------------------------------------------------------------------------- /cli/src/exit_codes.rs: -------------------------------------------------------------------------------- 1 | pub enum ExitCode { 2 | Success, 3 | Failure, 4 | } 5 | 6 | impl From for i32 { 7 | fn from(code: ExitCode) -> Self { 8 | match code { 9 | ExitCode::Success => 0, 10 | ExitCode::Failure => 1, 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /frameworks/actix_web/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod request; 2 | mod response; 3 | 4 | pub use request::GqlRequest; 5 | pub use response::GqlResponse; 6 | 7 | #[cfg(test)] 8 | mod tests { 9 | #[test] 10 | fn it_works() { 11 | let result = 2 + 2; 12 | assert_eq!(result, 4); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/schemas/interface.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | search_animal(query: String!): Animal 3 | } 4 | 5 | interface Animal { 6 | name: String 7 | } 8 | 9 | type Cat implements Pet { 10 | name: String 11 | meows: Boolean 12 | } 13 | 14 | type Dog implements Pet { 15 | name: String 16 | woofs: Boolean 17 | } 18 | -------------------------------------------------------------------------------- /src/types/introspection/mod.rs: -------------------------------------------------------------------------------- 1 | mod directive; 2 | mod enum_value; 3 | mod field; 4 | mod input_value; 5 | mod introspection_sdl; 6 | mod introspection_type; 7 | mod schema; 8 | pub use introspection_sdl::introspection_sdl; 9 | pub use introspection_type::__Type; 10 | pub use schema::{__Schema, build_schema_introspection}; 11 | -------------------------------------------------------------------------------- /examples/axum/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "rusty-gql-axum-example" 4 | version = "0.1.2" 5 | 6 | [dependencies] 7 | axum = {version = "0.4.2", features = ["headers"]} 8 | hyper = "0.14.16" 9 | rusty-gql = {path = "../.."} 10 | rusty-gql-axum = {path = "../../frameworks/axum"} 11 | tokio = {version = "1.12.0", features = ["full"]} 12 | -------------------------------------------------------------------------------- /examples/axum/src/graphql/mutation/create_review.rs: -------------------------------------------------------------------------------- 1 | #![allow(warnings, unused)] 2 | use crate::graphql::*; 3 | use rusty_gql::*; 4 | 5 | pub async fn createReview(episode: Option, review: ReviewInput) -> Option { 6 | Some(Review { 7 | stars: review.stars, 8 | commentary: review.commentary, 9 | episode, 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /tests/schemas/custom_directive.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | persons: [Person!]! 3 | person(id: ID): Person 4 | } 5 | 6 | type Person { 7 | name: String! @auth(requires: ADMIN) 8 | description: String 9 | age: Int! @auth(requires: USER) 10 | } 11 | 12 | directive @auth(requires: Role!) on FIELD_DEFINITION | OBJECT 13 | 14 | enum Role { 15 | ADMIN 16 | USER 17 | } 18 | -------------------------------------------------------------------------------- /cli/src/app.rs: -------------------------------------------------------------------------------- 1 | use clap::{crate_version, App, AppSettings, Arg}; 2 | 3 | pub fn build_app() -> App<'static> { 4 | App::new("rusty-gql") 5 | .version(crate_version!()) 6 | .setting(AppSettings::DeriveDisplayOrder) 7 | .subcommand(App::new("new").arg(Arg::new("name").required(true).index(1))) 8 | .subcommand(App::new("generate").alias("g")) 9 | } 10 | -------------------------------------------------------------------------------- /examples/axum/src/graphql/scalar/date_time.rs: -------------------------------------------------------------------------------- 1 | #![allow(warnings, unused)] 2 | use crate::graphql::*; 3 | use rusty_gql::*; 4 | 5 | #[derive(GqlScalar)] 6 | pub struct DateTime; 7 | 8 | impl GqlInputType for DateTime { 9 | fn from_gql_value(value: Option) -> Result { 10 | todo!() 11 | } 12 | 13 | fn to_gql_value(&self) -> GqlValue { 14 | todo!() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /frameworks/actix_web/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rusty-gql-actix-web" 3 | version = "0.1.2" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | actix-web = { version = "4.0.0-beta.14", default-features = false } 10 | futures-util = "0.3.18" 11 | rusty-gql = { path = "../.." } 12 | serde_json = "1.0.72" 13 | serde_urlencoded = "0.7.0" 14 | -------------------------------------------------------------------------------- /src/custom_directive.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use crate::{Context, GqlValue, ResolveFut, ResolverResult}; 4 | 5 | #[async_trait::async_trait] 6 | pub trait CustomDirective: Send + Sync { 7 | async fn resolve_field( 8 | &self, 9 | ctx: &Context<'_>, 10 | directive_args: &BTreeMap, 11 | resolve_fut: ResolveFut<'_>, 12 | ) -> ResolverResult>; 13 | } 14 | -------------------------------------------------------------------------------- /tests/schemas/union.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | search_animal(query: String!): SearchAnimal 3 | } 4 | 5 | type SearchAnimal { 6 | test: CustomScalar! 7 | } 8 | 9 | type Cat { 10 | name: String 11 | meows: Boolean 12 | } 13 | 14 | type Dog { 15 | name: String 16 | woofs: Boolean 17 | } 18 | 19 | type Person { 20 | name: String! 21 | description: String 22 | age: Int! 23 | } 24 | 25 | union SearchAnimal = Dog | Cat | Person 26 | -------------------------------------------------------------------------------- /frameworks/axum/src/response.rs: -------------------------------------------------------------------------------- 1 | use axum::response::IntoResponse; 2 | 3 | pub struct GqlResponse(pub rusty_gql::Response); 4 | 5 | impl From for GqlResponse { 6 | fn from(response: rusty_gql::Response) -> Self { 7 | GqlResponse(response) 8 | } 9 | } 10 | 11 | impl IntoResponse for GqlResponse { 12 | fn into_response(self) -> axum::response::Response { 13 | axum::Json(&self.0).into_response() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/axum/src/graphql/mutation/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(warnings, unused)] 2 | mod create_review; 3 | 4 | use crate::graphql::*; 5 | use rusty_gql::*; 6 | 7 | #[derive(Clone)] 8 | pub struct Mutation; 9 | 10 | #[GqlType] 11 | impl Mutation { 12 | pub async fn createReview( 13 | &self, 14 | episode: Option, 15 | review: ReviewInput, 16 | ) -> Option { 17 | create_review::createReview(episode, review).await 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /docs/src/introduction.md: -------------------------------------------------------------------------------- 1 | # rusty-gql 2 | 3 | rusty-gql is a Schema First GraphQL library for Rust. 4 | 5 | It is designed to make it easier to create a GraphQL server. 6 | 7 | ## Features 8 | 9 | - Schema First approach 10 | - Code Generate from GraphQL schema 11 | - Convention Over Configuration 12 | 13 | ## Status 14 | 15 | rusty-gql is still an experimental project. APIs and the architecture are subject to change. 16 | 17 | It is not yet recommended for use in production. 18 | -------------------------------------------------------------------------------- /examples/axum/src/graphql/query/droid.rs: -------------------------------------------------------------------------------- 1 | #![allow(warnings, unused)] 2 | use crate::{ 3 | graphql::*, 4 | starwars::{c3po, r2d2}, 5 | }; 6 | use rusty_gql::*; 7 | 8 | pub async fn droid(ctx: &Context<'_>, id: ID) -> Option { 9 | if id.0 == "5" { 10 | Some(r2d2()) 11 | } else if id.0 == "6" { 12 | Some(c3po()) 13 | } else { 14 | ctx.add_error(&GqlError::new("Droid Not found", Some(ctx.item.position))); 15 | None 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /cli/src/code_generate/project/gitignore_file.rs: -------------------------------------------------------------------------------- 1 | use crate::code_generate::FileDefinition; 2 | 3 | pub struct GitignoreFile<'a> { 4 | pub app_name: &'a str, 5 | } 6 | 7 | impl<'a> FileDefinition for GitignoreFile<'a> { 8 | fn name(&self) -> String { 9 | ".gitignore".to_string() 10 | } 11 | 12 | fn path(&self) -> String { 13 | format!("{}/.gitignore", self.app_name) 14 | } 15 | 16 | fn content(&self) -> String { 17 | "/target".to_string() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /docs/src/types/enum.md: -------------------------------------------------------------------------------- 1 | # Enum 2 | 3 | rusty-gql defines GraphQL Enum as Rust enum with `#[derive(GqlEnum)]`. 4 | 5 | src/graphql/resolver/episode.rs 6 | 7 | ```rust 8 | #![allow(warnings, unused)] 9 | use crate::graphql::*; 10 | use rusty_gql::*; 11 | 12 | #[derive(GqlEnum, Debug, Copy, Clone, Eq, PartialEq)] 13 | pub enum Episode { 14 | NEWHOPE, 15 | EMPIRE, 16 | JEDI, 17 | } 18 | ``` 19 | 20 | schema.graphql 21 | 22 | ```graphql 23 | enum Episode { 24 | NEWHOPE 25 | EMPIRE 26 | JEDI 27 | } 28 | ``` 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rusty-gql 2 | 3 | rusty-gql is a Schema First GraphQL library for Rust. 4 | 5 | It is designed to make it easier to create a GraphQL server. 6 | 7 | [Book](https://tak-iwamoto.github.io/rusty-gql/) 8 | 9 | ## Features 10 | 11 | - Schema First approach 12 | - Code Generate from GraphQL schema 13 | - Convention Over Configuration 14 | 15 | ## Status 16 | 17 | rusty-gql is still an experimental project. APIs and the architecture are subject to change. 18 | 19 | It is not yet recommended for use in production. 20 | -------------------------------------------------------------------------------- /examples/axum/src/graphql/query/human.rs: -------------------------------------------------------------------------------- 1 | #![allow(warnings, unused)] 2 | use crate::{ 3 | graphql::*, 4 | starwars::{han, leia, luke, vader}, 5 | }; 6 | use rusty_gql::*; 7 | 8 | pub async fn human(ctx: &Context<'_>, id: ID) -> Option { 9 | if id.0 == "1" { 10 | Some(luke()) 11 | } else if id.0 == "2" { 12 | Some(vader()) 13 | } else if id.0 == "3" { 14 | Some(han()) 15 | } else if id.0 == "4" { 16 | Some(leia()) 17 | } else { 18 | None 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/axum/src/graphql/resolver/friends_edge.rs: -------------------------------------------------------------------------------- 1 | #![allow(warnings, unused)] 2 | use crate::graphql::*; 3 | use rusty_gql::*; 4 | 5 | #[derive(Clone)] 6 | pub struct FriendsEdge { 7 | pub cursor: ID, 8 | pub node: Option, 9 | } 10 | 11 | #[GqlType] 12 | impl FriendsEdge { 13 | pub async fn cursor(&self, ctx: &Context<'_>) -> ID { 14 | self.cursor.clone() 15 | } 16 | 17 | pub async fn node(&self, ctx: &Context<'_>) -> Option { 18 | self.node.clone() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/axum/src/graphql/directive/possible_types.rs: -------------------------------------------------------------------------------- 1 | #![allow(warnings, unused)] 2 | use crate::graphql::*; 3 | use rusty_gql::*; 4 | use std::collections::BTreeMap; 5 | 6 | pub struct possibleTypes; 7 | 8 | #[async_trait::async_trait] 9 | impl CustomDirective for possibleTypes { 10 | async fn resolve_field( 11 | &self, 12 | ctx: &Context<'_>, 13 | directive_args: &BTreeMap, 14 | resolve_fut: ResolveFut<'_>, 15 | ) -> ResolverResult> { 16 | todo!() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/variables.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::GqlValue; 6 | 7 | #[derive(Serialize, Clone, Default, Debug)] 8 | pub struct Variables(pub BTreeMap); 9 | 10 | impl<'de> Deserialize<'de> for Variables { 11 | fn deserialize(deserializer: D) -> Result 12 | where 13 | D: serde::Deserializer<'de>, 14 | { 15 | Ok(Self( 16 | >>::deserialize(deserializer)?.unwrap_or_default(), 17 | )) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/axum/src/graphql/resolver/mod.rs: -------------------------------------------------------------------------------- 1 | mod character; 2 | mod droid; 3 | mod episode; 4 | mod friends_connection; 5 | mod friends_edge; 6 | mod human; 7 | mod length_unit; 8 | mod page_info; 9 | mod review; 10 | mod search_result; 11 | 12 | pub use character::Character; 13 | pub use droid::Droid; 14 | pub use episode::Episode; 15 | pub use friends_connection::FriendsConnection; 16 | pub use friends_edge::FriendsEdge; 17 | pub use human::Human; 18 | pub use length_unit::LengthUnit; 19 | pub use page_info::PageInfo; 20 | pub use review::Review; 21 | pub use search_result::SearchResult; 22 | -------------------------------------------------------------------------------- /examples/axum/src/graphql/query/hero.rs: -------------------------------------------------------------------------------- 1 | #![allow(warnings, unused)] 2 | use crate::{ 3 | graphql::*, 4 | starwars::{han, luke, vader}, 5 | }; 6 | use rusty_gql::*; 7 | 8 | pub async fn hero(ctx: &Context<'_>, episode: Option) -> Option { 9 | match episode { 10 | Some(episode) => match episode { 11 | Episode::NEWHOPE => Some(Character::Human(luke())), 12 | Episode::EMPIRE => Some(Character::Human(han())), 13 | Episode::JEDI => Some(Character::Human(vader())), 14 | }, 15 | None => None, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/input/optional.rs: -------------------------------------------------------------------------------- 1 | use crate::GqlValue; 2 | 3 | use super::GqlInputType; 4 | 5 | impl GqlInputType for Option { 6 | fn from_gql_value(value: Option) -> Result { 7 | match value.unwrap_or_default() { 8 | GqlValue::Null => Ok(None), 9 | value => Ok(Some(T::from_gql_value(Some(value))?)), 10 | } 11 | } 12 | 13 | fn to_gql_value(&self) -> GqlValue { 14 | match self { 15 | Some(value) => value.to_gql_value(), 16 | None => GqlValue::Null, 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/axum/src/graphql/resolver/review.rs: -------------------------------------------------------------------------------- 1 | #![allow(warnings, unused)] 2 | use crate::graphql::*; 3 | use rusty_gql::*; 4 | 5 | pub struct Review { 6 | pub stars: i32, 7 | pub commentary: Option, 8 | pub episode: Option, 9 | } 10 | 11 | #[GqlType] 12 | impl Review { 13 | pub async fn episode(&self, ctx: &Context<'_>) -> Option { 14 | self.episode 15 | } 16 | 17 | pub async fn stars(&self, ctx: &Context<'_>) -> i32 { 18 | self.stars 19 | } 20 | 21 | pub async fn commentary(&self, ctx: &Context<'_>) -> Option { 22 | self.commentary.clone() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /docs/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Introduction](./introduction.md) 4 | - [Getting Started](./getting_started.md) 5 | - [Directory Structure](./directory_structure.md) 6 | - [Types](./types/index.md) 7 | - [Object](./types/object.md) 8 | - [Interface](./types/interface.md) 9 | - [Union](./types/union.md) 10 | - [Enum](./types/enum.md) 11 | - [InputObject](./types/input_object.md) 12 | - [Scalar](./types/scalar.md) 13 | - [Directive](./types/directive.md) 14 | - [Schema](./schema/index.md) 15 | - [Query](./schema/query.md) 16 | - [Mutation](./schema/mutation.md) 17 | - [Error Handling](./error_handling.md) 18 | - [Roadmap](./roadmap.md) 19 | -------------------------------------------------------------------------------- /examples/axum/src/graphql/resolver/friends_connection.rs: -------------------------------------------------------------------------------- 1 | #![allow(warnings, unused)] 2 | use crate::graphql::*; 3 | use rusty_gql::*; 4 | 5 | pub struct FriendsConnection { 6 | pub totalCount: Option, 7 | pub edges: Vec, 8 | pub pageInfo: PageInfo, 9 | } 10 | 11 | #[GqlType] 12 | impl FriendsConnection { 13 | pub async fn totalCount(&self, ctx: &Context<'_>) -> Option { 14 | self.totalCount 15 | } 16 | 17 | pub async fn edges(&self, ctx: &Context<'_>) -> Vec { 18 | self.edges.clone() 19 | } 20 | 21 | pub async fn pageInfo(&self, ctx: &Context<'_>) -> PageInfo { 22 | self.pageInfo.clone() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/book.yml: -------------------------------------------------------------------------------- 1 | name: Book 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - 'main' 8 | paths: 9 | - 'docs/**' 10 | 11 | jobs: 12 | deploy: 13 | name: Deploy book on GitHub pages 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v2 18 | - name: Setup mdBook 19 | uses: peaceiris/actions-mdbook@v1 20 | with: 21 | mdbook-version: '0.4.10' 22 | - run: mdbook build docs 23 | 24 | - name: Deploy 25 | uses: peaceiris/actions-gh-pages@v3 26 | with: 27 | github_token: ${{ secrets.GITHUB_TOKEN }} 28 | publish_dir: ./docs/book 29 | -------------------------------------------------------------------------------- /frameworks/actix_web/src/request.rs: -------------------------------------------------------------------------------- 1 | use std::pin::Pin; 2 | 3 | use actix_web::{ 4 | http::{Error, Method}, 5 | Error, FromRequest, Result, 6 | }; 7 | use futures::Future; 8 | 9 | pub struct GqlRequest(pub rusty_gql::Request); 10 | 11 | impl FromRequest for GqlRequest { 12 | type Error = Error; 13 | 14 | type Future; 15 | 16 | fn from_request( 17 | req: &actix_web::HttpRequest, 18 | payload: &mut actix_web::dev::Payload, 19 | ) -> Self::Future { 20 | if req.method() == Method::GET { 21 | let body = serde_urlencoded::from_str(req.query_string()); 22 | Box::pin(async move { Ok(Self(rusty_gql::Request(body?))) }) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/input/boolean.rs: -------------------------------------------------------------------------------- 1 | use crate::GqlValue; 2 | 3 | use super::GqlInputType; 4 | 5 | impl GqlInputType for bool { 6 | fn from_gql_value(value: Option) -> Result { 7 | match value { 8 | Some(value) => match value { 9 | GqlValue::Boolean(v) => Ok(v), 10 | invalid_value => Err(format!( 11 | "Expected type: boolean, but found: {}", 12 | invalid_value.to_string() 13 | )), 14 | }, 15 | None => Err("Expected type: boolean, but not found".to_string()), 16 | } 17 | } 18 | 19 | fn to_gql_value(&self) -> GqlValue { 20 | GqlValue::Boolean(*self) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/input/string.rs: -------------------------------------------------------------------------------- 1 | use crate::GqlValue; 2 | 3 | use super::GqlInputType; 4 | 5 | impl GqlInputType for String { 6 | fn from_gql_value(value: Option) -> Result { 7 | match value { 8 | Some(value) => match value { 9 | GqlValue::String(v) => Ok(v), 10 | invalid_value => Err(format!( 11 | "{}: invalid gql value for string", 12 | invalid_value.to_string() 13 | )), 14 | }, 15 | None => Err("Expected type: boolean, but not found".to_string()), 16 | } 17 | } 18 | 19 | fn to_gql_value(&self) -> GqlValue { 20 | GqlValue::String(self.clone()) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /frameworks/actix_web/src/response.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{ 2 | http::{Error, StatusCode}, 3 | HttpResponse, Responder, 4 | }; 5 | use futures_util::future::{ready, Ready}; 6 | 7 | pub struct GqlResponse(pub rusty_gql::Response); 8 | 9 | impl From for GqlResponse { 10 | fn from(response: rusty_gql::Response) -> Self { 11 | GqlResponse(response) 12 | } 13 | } 14 | 15 | impl Responder for GqlResponse { 16 | type Body = HttpResponse; 17 | 18 | fn respond_to(self, _: &actix_web::HttpRequest) -> HttpResponse { 19 | let body = serde_json::to_string(&self.0).unwrap(); 20 | HttpResponse::Ok() 21 | .content_type("application/json") 22 | .body(body) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /docs/src/types/input_object.md: -------------------------------------------------------------------------------- 1 | # InputObject 2 | 3 | rusty-gql defines GraphQL InputObject as Rust struct with `#[derive(GqlInputObject)]`. 4 | 5 | src/graphql/input/review_input.rs 6 | 7 | ```rust 8 | #![allow(warnings, unused)] 9 | use crate::graphql::*; 10 | use rusty_gql::*; 11 | 12 | #[derive(GqlInputObject)] 13 | pub struct ReviewInput { 14 | pub stars: i32, 15 | pub commentary: Option, 16 | } 17 | ``` 18 | 19 | schema.graphql 20 | 21 | ```graphql 22 | type Mutation { 23 | createReview(episode: Episode, review: ReviewInput!): Review 24 | } 25 | 26 | input ReviewInput { 27 | stars: Int! 28 | commentary: String 29 | } 30 | 31 | type Review { 32 | episode: Episode 33 | stars: Int! 34 | commentary: String 35 | } 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/src/types/union.md: -------------------------------------------------------------------------------- 1 | # Union 2 | 3 | rusty-gql defines GraphQL Union as Rust enum with different types and `#[derive(GqlUnion)]`. 4 | 5 | src/graphql/resolver/search_result.rs 6 | 7 | ```rust 8 | #![allow(warnings, unused)] 9 | use crate::graphql::*; 10 | use rusty_gql::*; 11 | 12 | #[derive(GqlUnion)] 13 | pub enum SearchResult { 14 | Human(Human), 15 | Droid(Droid), 16 | } 17 | ``` 18 | 19 | schema.graphql 20 | 21 | ```graphql 22 | type Query { 23 | search(text: String): [SearchResult!]! 24 | } 25 | 26 | union SearchResult = Human | Droid 27 | 28 | type Human { 29 | id: ID! 30 | name: String! 31 | homePlanet: String 32 | } 33 | 34 | type Droid { 35 | id: ID! 36 | name: String! 37 | primaryFunction: String 38 | } 39 | ``` 40 | -------------------------------------------------------------------------------- /cli/src/code_generate/project/example_schema_file.rs: -------------------------------------------------------------------------------- 1 | use crate::code_generate::FileDefinition; 2 | 3 | pub struct TodoSchemaFile<'a> { 4 | pub app_name: &'a str, 5 | } 6 | impl<'a> FileDefinition for TodoSchemaFile<'a> { 7 | fn name(&self) -> String { 8 | "schema.graphql".to_string() 9 | } 10 | 11 | fn path(&self) -> String { 12 | format!("{}/schema/schema.graphql", self.app_name) 13 | } 14 | 15 | fn content(&self) -> String { 16 | todo_schema_content().to_string() 17 | } 18 | } 19 | 20 | fn todo_schema_content() -> &'static str { 21 | r#"type Query { 22 | todos(first: Int): [Todo!]! 23 | } 24 | 25 | type Todo { 26 | title: String! 27 | content: String 28 | done: Boolean! 29 | } 30 | "# 31 | } 32 | -------------------------------------------------------------------------------- /src/types/introspection/enum_value.rs: -------------------------------------------------------------------------------- 1 | use rusty_gql_macro::GqlType; 2 | 3 | use crate::{types::EnumTypeValue, SelectionSetResolver}; 4 | 5 | pub struct __EnumValue { 6 | detail: EnumTypeValue, 7 | } 8 | 9 | pub fn build_enum_value_introspection(value: &EnumTypeValue) -> __EnumValue { 10 | __EnumValue { 11 | detail: value.clone(), 12 | } 13 | } 14 | 15 | #[allow(non_snake_case)] 16 | #[GqlType(internal)] 17 | impl __EnumValue { 18 | async fn name(&self) -> &str { 19 | self.detail.name.as_str() 20 | } 21 | 22 | async fn description(&self) -> Option<&str> { 23 | self.detail.description.as_deref() 24 | } 25 | 26 | async fn isDeprecated(&self) -> bool { 27 | self.detail.is_deprecated() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/axum/src/graphql/query/character.rs: -------------------------------------------------------------------------------- 1 | #![allow(warnings, unused)] 2 | use crate::{ 3 | graphql::*, 4 | starwars::{c3po, han, leia, luke, r2d2, vader}, 5 | }; 6 | use rusty_gql::*; 7 | 8 | pub async fn character(ctx: &Context<'_>, id: ID) -> Option { 9 | if id.0 == "1" { 10 | Some(Character::Human(luke())) 11 | } else if id.0 == "2" { 12 | Some(Character::Human(vader())) 13 | } else if id.0 == "3" { 14 | Some(Character::Human(han())) 15 | } else if id.0 == "4" { 16 | Some(Character::Human(leia())) 17 | } else if id.0 == "5" { 18 | Some(Character::Droid(r2d2())) 19 | } else if id.0 == "6" { 20 | Some(Character::Droid(c3po())) 21 | } else { 22 | None 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/input/id.rs: -------------------------------------------------------------------------------- 1 | use crate::{types::ID, GqlValue}; 2 | 3 | use super::GqlInputType; 4 | 5 | impl GqlInputType for ID { 6 | fn from_gql_value(value: Option) -> Result { 7 | match value { 8 | Some(value) => match value { 9 | GqlValue::String(v) => Ok(ID(v)), 10 | GqlValue::Number(v) => Ok(ID(v.to_string())), 11 | invalid_value => Err(format!( 12 | "{}: invalid gql value for id", 13 | invalid_value.to_string() 14 | )), 15 | }, 16 | None => Err("Expected type: id, but not found".to_string()), 17 | } 18 | } 19 | 20 | fn to_gql_value(&self) -> GqlValue { 21 | GqlValue::String(self.0.clone()) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/resolver/boolean.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | Context, FieldResolver, GqlValue, ResolverResult, SelectionSetContext, SelectionSetResolver, 3 | }; 4 | 5 | use super::CollectFields; 6 | 7 | #[async_trait::async_trait] 8 | impl FieldResolver for bool { 9 | async fn resolve_field(&self, _ctx: &Context<'_>) -> ResolverResult> { 10 | Ok(Some(GqlValue::Boolean(*self))) 11 | } 12 | fn type_name() -> String { 13 | "Boolean".to_string() 14 | } 15 | } 16 | 17 | impl CollectFields for bool {} 18 | 19 | #[async_trait::async_trait] 20 | impl SelectionSetResolver for bool { 21 | async fn resolve_selection_set( 22 | &self, 23 | _ctx: &SelectionSetContext<'_>, 24 | ) -> ResolverResult { 25 | Ok(GqlValue::Boolean(*self)) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/resolver/id.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | CollectFields, Context, FieldResolver, GqlValue, ResolverResult, SelectionSetContext, 3 | SelectionSetResolver, ID, 4 | }; 5 | 6 | #[async_trait::async_trait] 7 | impl FieldResolver for ID { 8 | async fn resolve_field(&self, _ctx: &Context<'_>) -> ResolverResult> { 9 | Ok(Some(GqlValue::String(self.0.to_string()))) 10 | } 11 | fn type_name() -> String { 12 | "ID".to_string() 13 | } 14 | } 15 | 16 | impl CollectFields for ID {} 17 | 18 | #[async_trait::async_trait] 19 | impl SelectionSetResolver for ID { 20 | async fn resolve_selection_set( 21 | &self, 22 | _ctx: &SelectionSetContext<'_>, 23 | ) -> ResolverResult { 24 | Ok(GqlValue::String(self.0.to_string())) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/axum/src/graphql/resolver/page_info.rs: -------------------------------------------------------------------------------- 1 | #![allow(warnings, unused)] 2 | use crate::graphql::*; 3 | use rusty_gql::*; 4 | 5 | #[derive(Clone)] 6 | pub struct PageInfo { 7 | pub startCursor: Option, 8 | pub endCursor: Option, 9 | pub hasPreviousPage: bool, 10 | pub hasNextPage: bool, 11 | } 12 | 13 | #[GqlType] 14 | impl PageInfo { 15 | pub async fn startCursor(&self, ctx: &Context<'_>) -> Option { 16 | self.startCursor.clone() 17 | } 18 | 19 | pub async fn endCursor(&self, ctx: &Context<'_>) -> Option { 20 | self.endCursor.clone() 21 | } 22 | 23 | pub async fn hasPreviousPage(&self, ctx: &Context<'_>) -> bool { 24 | self.hasPreviousPage 25 | } 26 | 27 | pub async fn hasNextPage(&self, ctx: &Context<'_>) -> bool { 28 | self.hasNextPage 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /cli/src/code_generate/root_mod_file.rs: -------------------------------------------------------------------------------- 1 | use crate::code_generate::FileDefinition; 2 | 3 | use super::path_str; 4 | 5 | pub struct RootModFile<'a> { 6 | pub filenames: Vec, 7 | pub path: &'a str, 8 | } 9 | 10 | impl<'a> FileDefinition for RootModFile<'a> { 11 | fn name(&self) -> String { 12 | "mod.rs".to_string() 13 | } 14 | 15 | fn content(&self) -> String { 16 | let mut mod_str = String::from(""); 17 | let mut pub_use_str = String::from(""); 18 | for name in &self.filenames { 19 | mod_str += format!("mod {};\n", &name).as_str(); 20 | pub_use_str += format!("pub use {}::*;\n", &name).as_str(); 21 | } 22 | 23 | format!("{}\n{}", mod_str, pub_use_str) 24 | } 25 | 26 | fn path(&self) -> String { 27 | path_str(vec![self.path, "mod"], true) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/types/union_type.rs: -------------------------------------------------------------------------------- 1 | use super::directive::GqlDirective; 2 | use graphql_parser::{schema::UnionType as ParserUnionType, Pos}; 3 | 4 | #[derive(Debug, Clone)] 5 | pub struct UnionType { 6 | pub name: String, 7 | pub description: Option, 8 | pub position: Pos, 9 | pub directives: Vec, 10 | pub types: Vec, 11 | } 12 | 13 | impl<'a> From> for UnionType { 14 | fn from(gql_union: ParserUnionType<'a, String>) -> Self { 15 | let directives = GqlDirective::from_vec_directive(gql_union.directives); 16 | 17 | let types = gql_union.types; 18 | 19 | UnionType { 20 | name: gql_union.name, 21 | description: gql_union.description, 22 | position: gql_union.position, 23 | directives, 24 | types, 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /frameworks/axum/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rusty-gql-axum" 3 | version = "0.1.2" 4 | description = "axum integration for rusty-gql" 5 | documentation = "https://github.com/Tak-Iwamoto/rusty-gql" 6 | edition = "2021" 7 | repository = "https://github.com/Tak-Iwamoto/rusty-gql" 8 | authors = ["Tak-Iwamoto"] 9 | homepage = "https://github.com/Tak-Iwamoto/rusty-gql" 10 | license = "MIT" 11 | keywords = ["graphql", "async", "web"] 12 | categories = ["asynchronous", "web-programming"] 13 | 14 | [dependencies] 15 | async-trait = "0.1.52" 16 | axum = {version = "0.4.2", features = ["ws", "headers"]} 17 | bytes = "1.1.0" 18 | futures-util = {version = "0.3.18", default-features = false, features = ["io", "sink"]} 19 | http-body = "0.4.4" 20 | rusty-gql = {path = "../..", version = "0.1.2"} 21 | serde_urlencoded = "0.7.0" 22 | tokio-util = {version = "0.6.9", default-features = false, features = ["io", "compat"]} 23 | -------------------------------------------------------------------------------- /macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "rusty-gql-macro" 4 | version = "0.1.2" 5 | authors = ["Tak-Iwamoto"] 6 | description = "Macro for rusty-gql" 7 | license = "MIT" 8 | documentation = "https://github.com/Tak-Iwamoto/rusty-gql" 9 | homepage = "https://github.com/Tak-Iwamoto/rusty-gql" 10 | repository = "https://github.com/Tak-Iwamoto/rusty-gql" 11 | keywords = ["graphql", "async", "web"] 12 | categories = ["asynchronous", "web-programming"] 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | [lib] 16 | proc-macro = true 17 | 18 | [dependencies] 19 | proc-macro2 = "1.0.29" 20 | quote = "1.0.10" 21 | syn = {version = "1.0.80", features = ["full", "extra-traits", "visit-mut", "visit"]} 22 | 23 | [dev-dependencies] 24 | tokio = {version = "1.12.0", features = ["fs", "io-std", "io-util", "rt-multi-thread", "sync", "signal", "macros"]} 25 | -------------------------------------------------------------------------------- /cli/src/code_generate/project/axum/cargo_toml_file.rs: -------------------------------------------------------------------------------- 1 | use crate::code_generate::FileDefinition; 2 | 3 | pub struct AxumCargoTomlFile<'a> { 4 | pub app_name: &'a str, 5 | } 6 | 7 | impl<'a> FileDefinition for AxumCargoTomlFile<'a> { 8 | fn path(&self) -> String { 9 | format!("{}/Cargo.toml", self.app_name) 10 | } 11 | 12 | fn content(&self) -> String { 13 | cargo_toml_content(self.app_name) 14 | } 15 | 16 | fn name(&self) -> String { 17 | "Cargo.toml".to_string() 18 | } 19 | } 20 | 21 | fn cargo_toml_content(app_name: &str) -> String { 22 | r#"[package] 23 | name = "APP_NAME" 24 | version = "0.1.2" 25 | edition = "2021" 26 | 27 | [dependencies] 28 | async-trait = "0.1.52" 29 | axum = {version = "0.4.2", features = ["headers"]} 30 | hyper = "0.14.16" 31 | rusty-gql = "0.1.0" 32 | rusty-gql-axum = "0.1.0" 33 | tokio = { version = "1.0", features = ["full"] } 34 | "# 35 | .replace("APP_NAME", app_name) 36 | } 37 | -------------------------------------------------------------------------------- /src/types/mod.rs: -------------------------------------------------------------------------------- 1 | mod argument; 2 | mod directive; 3 | mod enum_type; 4 | mod field; 5 | mod id; 6 | mod input_object; 7 | mod interface; 8 | mod introspection; 9 | mod object; 10 | mod scalar; 11 | mod type_definition; 12 | mod union_type; 13 | pub mod value; 14 | mod value_type; 15 | 16 | pub mod schema; 17 | pub use argument::InputValueType; 18 | pub use field::FieldType; 19 | pub use id::ID; 20 | pub use introspection::__Schema; 21 | pub use introspection::__Type; 22 | pub use introspection::build_schema_introspection; 23 | pub use scalar::ScalarType; 24 | pub use schema::Schema; 25 | pub use type_definition::TypeDefinition; 26 | pub use value::{GqlConstValue, GqlValue}; 27 | pub use value_type::GqlValueType; 28 | 29 | pub use directive::{DirectiveDefinition, GqlDirective}; 30 | pub use enum_type::{EnumType, EnumTypeValue}; 31 | pub use input_object::InputObjectType; 32 | pub use interface::InterfaceType; 33 | pub use object::ObjectType; 34 | pub use union_type::UnionType; 35 | -------------------------------------------------------------------------------- /tests/enum.rs: -------------------------------------------------------------------------------- 1 | use rusty_gql::*; 2 | 3 | #[tokio::test] 4 | pub async fn test_enum() { 5 | struct Query; 6 | 7 | #[derive(GqlEnum)] 8 | enum SampleEnum { 9 | Value0, 10 | #[allow(unused)] 11 | Value1, 12 | } 13 | 14 | #[GqlType] 15 | impl Query { 16 | async fn enum_value(&self) -> SampleEnum { 17 | SampleEnum::Value0 18 | } 19 | } 20 | let contents = schema_content("./tests/schemas/enum.graphql"); 21 | 22 | let container = Container::new( 23 | &vec![contents.as_str()], 24 | Query, 25 | EmptyMutation, 26 | EmptySubscription, 27 | Default::default(), 28 | ) 29 | .unwrap(); 30 | 31 | let query_doc = r#"{ enum_value }"#; 32 | let req = build_test_request(query_doc, None, Default::default()); 33 | let expected_response = r#"{"data":{"enum_value":"Value0"}}"#; 34 | check_gql_response(req, expected_response, &container).await; 35 | } 36 | -------------------------------------------------------------------------------- /src/input/mod.rs: -------------------------------------------------------------------------------- 1 | mod boolean; 2 | mod id; 3 | mod list; 4 | mod number; 5 | mod object; 6 | mod optional; 7 | mod string; 8 | 9 | use std::sync::Arc; 10 | 11 | use crate::GqlValue; 12 | 13 | pub trait GqlInputType: Send + Sync + Sized { 14 | fn from_gql_value(value: Option) -> Result; 15 | 16 | fn to_gql_value(&self) -> GqlValue; 17 | } 18 | 19 | impl GqlInputType for Arc { 20 | fn from_gql_value(value: Option) -> Result { 21 | T::from_gql_value(value).map(|v| Arc::new(v)) 22 | } 23 | 24 | fn to_gql_value(&self) -> GqlValue { 25 | T::to_gql_value(self) 26 | } 27 | } 28 | 29 | impl GqlInputType for Box { 30 | fn from_gql_value(value: Option) -> Result { 31 | T::from_gql_value(value).map(|v| Box::new(v)) 32 | } 33 | 34 | fn to_gql_value(&self) -> GqlValue { 35 | T::to_gql_value(self) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test_utils.rs: -------------------------------------------------------------------------------- 1 | use crate::{execute, Container, Request, SelectionSetResolver, Variables}; 2 | 3 | pub fn schema_content(path: &str) -> String { 4 | std::fs::read_to_string(path).unwrap() 5 | } 6 | 7 | pub fn build_test_request( 8 | query: &str, 9 | operation_name: Option, 10 | variables: Variables, 11 | ) -> Request { 12 | Request { 13 | query: query.to_string(), 14 | operation_name, 15 | variables, 16 | extensions: Default::default(), 17 | } 18 | } 19 | 20 | pub async fn check_gql_response< 21 | Query: SelectionSetResolver + 'static, 22 | Mutation: SelectionSetResolver + 'static, 23 | Subscription: SelectionSetResolver + 'static, 24 | >( 25 | request: Request, 26 | expected_response: &str, 27 | container: &Container, 28 | ) { 29 | let res = execute(container, request).await; 30 | assert_eq!(serde_json::to_string(&res).unwrap(), expected_response); 31 | } 32 | -------------------------------------------------------------------------------- /docs/src/types/scalar.md: -------------------------------------------------------------------------------- 1 | # Scalar 2 | 3 | We can define custom scalars. 4 | 5 | rusty-gql represents custom scalar by using `#[derive(GqlScalar)]` and `GqlInputType` trait. 6 | 7 | src/graphql/scalar/base64.rs 8 | 9 | ```rust 10 | #![allow(warnings, unused)] 11 | use crate::graphql::*; 12 | use rusty_gql::*; 13 | 14 | #[derive(GqlScalar)] 15 | pub struct Base64(String); 16 | 17 | impl GqlInputType for Base64 { 18 | fn from_gql_value(value: Option) -> Result { 19 | if let Some(GqlValue::String(v)) = value { 20 | let encoded = base64::encode(v); 21 | Ok(Base64(encoded)) 22 | } else { 23 | Err(format!( 24 | "{}: is invalid type for Base64", 25 | value.unwrap_or(GqlValue::Null).to_string() 26 | )) 27 | } 28 | } 29 | 30 | fn to_gql_value(&self) -> GqlValue { 31 | GqlValue::String(self.0.to_string()) 32 | } 33 | } 34 | ``` 35 | 36 | schema.graphql 37 | 38 | ```graphql 39 | scalar Base64 40 | ``` 41 | -------------------------------------------------------------------------------- /tests/schemas/test_schema.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | pets(first: Int): [Pet!] 3 | twice_value(value: Int!): Int! 4 | value: Int 5 | persons: [Person!]! 6 | person(id: ID): Person 7 | obj: Obj 8 | } 9 | 10 | type Mutation { 11 | testMutation(input: MutationInput!): Boolean 12 | } 13 | 14 | input MutationInput { 15 | value: String 16 | } 17 | 18 | type Person { 19 | name: String! 20 | description: String 21 | age: Int! 22 | } 23 | 24 | 25 | type Obj { 26 | key1: i32 27 | key2: i32 28 | } 29 | 30 | interface Pet { 31 | name: String 32 | } 33 | 34 | type Cat implements Pet { 35 | name: String 36 | meows: Boolean 37 | } 38 | 39 | type Dog implements Pet { 40 | name: String 41 | woofs: Boolean @authAdmin 42 | } 43 | 44 | input TestInput { 45 | int_field: Int! 46 | str_field: String 47 | } 48 | 49 | enum Country { 50 | JAPAN 51 | AMERICA 52 | CHINA 53 | } 54 | 55 | union Animal = Dog | Cat 56 | 57 | scalar DateTime 58 | 59 | directive @authAdmin on FIELD 60 | -------------------------------------------------------------------------------- /examples/axum/src/graphql/query/search.rs: -------------------------------------------------------------------------------- 1 | #![allow(warnings, unused)] 2 | use crate::{ 3 | graphql::*, 4 | starwars::{c3po, han, leia, luke, r2d2, vader}, 5 | }; 6 | use rusty_gql::*; 7 | 8 | pub async fn search( 9 | ctx: &Context<'_>, 10 | text: Option, 11 | episode: Option, 12 | ) -> Vec { 13 | if let Some(text) = text { 14 | if text == "luke" { 15 | vec![SearchResult::Human(luke())] 16 | } else if text == "vader" { 17 | vec![SearchResult::Human(vader())] 18 | } else if text == "han" { 19 | vec![SearchResult::Human(han())] 20 | } else if text == "leia" { 21 | vec![SearchResult::Human(leia())] 22 | } else if text == "r2d2" { 23 | vec![SearchResult::Droid(r2d2())] 24 | } else if text == "c3po" { 25 | vec![SearchResult::Droid(c3po())] 26 | } else { 27 | vec![] 28 | } 29 | } else { 30 | vec![] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/types/object.rs: -------------------------------------------------------------------------------- 1 | use graphql_parser::{schema::ObjectType as ParserObjectType, Pos}; 2 | 3 | use super::{directive::GqlDirective, field::FieldType}; 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct ObjectType { 7 | pub name: String, 8 | pub description: Option, 9 | pub position: Pos, 10 | pub implements_interfaces: Vec, 11 | pub directives: Vec, 12 | pub fields: Vec, 13 | } 14 | 15 | impl<'a> From> for ObjectType { 16 | fn from(object: ParserObjectType<'a, String>) -> Self { 17 | let directives = GqlDirective::from_vec_directive(object.directives); 18 | let fields = FieldType::from_vec_field(object.fields); 19 | 20 | ObjectType { 21 | name: object.name, 22 | description: object.description, 23 | position: object.position, 24 | implements_interfaces: object.implements_interfaces, 25 | directives, 26 | fields, 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rusty-gql-cli" 3 | edition = "2021" 4 | version = "0.1.2" 5 | authors = ["Tak-Iwamoto"] 6 | description = "A Command line tool for rusty-gql" 7 | license = "MIT" 8 | documentation = "https://github.com/Tak-Iwamoto/rusty-gql" 9 | homepage = "https://github.com/Tak-Iwamoto/rusty-gql" 10 | repository = "https://github.com/Tak-Iwamoto/rusty-gql" 11 | keywords = ["graphql", "async", "web", "cli"] 12 | categories = ["asynchronous", "web-programming"] 13 | 14 | [[bin]] 15 | name = "rusty-gql" 16 | path = "src/main.rs" 17 | 18 | [dependencies] 19 | anyhow = "1.0.51" 20 | async-recursion = "0.3.2" 21 | async-trait = "0.1.52" 22 | clap = {version = "3.0.5", features = ["cargo"]} 23 | codegen = "0.1.3" 24 | futures-util = "0.3.18" 25 | graphql-parser = "0.3.0" 26 | heck = "0.4.0" 27 | proc-macro2 = "1.0.36" 28 | quote = "1.0.15" 29 | rusty-gql = {path = "../", version = "0.1.2"} 30 | serde_json = "1.0.72" 31 | syn = {version = "1.0.86", features = ["full", "extra-traits", "visit-mut", "visit"]} 32 | tokio = "1.13.0" 33 | -------------------------------------------------------------------------------- /docs/src/types/interface.md: -------------------------------------------------------------------------------- 1 | # Interface 2 | 3 | GraphQL Interface is represented as Rust enum with different types and `#[derive(GqlInterface)`, `#[GqlType(interface)]`. 4 | 5 | Each variants is possible types of interface. 6 | 7 | src/graphql/resolver/pet.rs 8 | 9 | ```rust 10 | #![allow(warnings, unused)] 11 | use crate::graphql::*; 12 | use rusty_gql::*; 13 | 14 | #[derive(GqlInterface, Clone)] 15 | pub enum Pet { 16 | Cat(Cat), 17 | Dog(Dog), 18 | } 19 | 20 | #[GqlType(interface)] 21 | impl Pet { 22 | async fn name(&self, ctx: &Context<'_>) -> Result { 23 | match self { 24 | Pet::Cat(obj) => obj.name(&ctx).await, 25 | Pet::Dog(obj) => obj.name(&ctx).await, 26 | } 27 | } 28 | } 29 | 30 | ``` 31 | 32 | schema.graphql 33 | 34 | ```graphql 35 | interface Pet { 36 | name: String 37 | } 38 | 39 | type Cat implements Pet { 40 | name: String 41 | meows: Boolean 42 | } 43 | 44 | type Dog implements Pet { 45 | name: String 46 | woofs: Boolean 47 | } 48 | ``` 49 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rusty-gql" 3 | edition = "2021" 4 | version = "0.1.2" 5 | authors = ["Tak-Iwamoto"] 6 | description = "Schema first GraphQL Library for Rust" 7 | license = "MIT" 8 | documentation = "https://github.com/Tak-Iwamoto/rusty-gql" 9 | homepage = "https://github.com/Tak-Iwamoto/rusty-gql" 10 | repository = "https://github.com/Tak-Iwamoto/rusty-gql" 11 | keywords = ["graphql", "async", "web"] 12 | categories = ["asynchronous", "web-programming"] 13 | readme = "README.md" 14 | 15 | [workspace] 16 | members = ["examples/*", "macro", "cli", "frameworks/axum"] 17 | 18 | [dependencies] 19 | anyhow = "1.0.44" 20 | async-trait = "0.1.51" 21 | futures-util = {version = "0.3.18", default-features = false, features = ["io", "sink"]} 22 | graphql-parser = "0.3.0" 23 | http = "0.2.5" 24 | rusty-gql-macro = {path = "macro", version = "0.1.2"} 25 | serde = {version = "1.0.130", features = ["derive"]} 26 | serde_json = "1.0.68" 27 | tokio = {version = "1.12.0", features = ["fs", "io-std", "io-util", "rt-multi-thread", "sync", "signal", "macros"]} 28 | -------------------------------------------------------------------------------- /src/types/interface.rs: -------------------------------------------------------------------------------- 1 | use graphql_parser::{schema::InterfaceType as ParserInterfaceType, Pos}; 2 | 3 | use super::{directive::GqlDirective, field::FieldType}; 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct InterfaceType { 7 | pub name: String, 8 | pub description: Option, 9 | pub position: Pos, 10 | pub directives: Vec, 11 | pub fields: Vec, 12 | } 13 | 14 | impl<'a> From> for InterfaceType { 15 | fn from(interface_type: ParserInterfaceType<'a, String>) -> Self { 16 | let directives = GqlDirective::from_vec_directive(interface_type.directives); 17 | let fields = interface_type 18 | .fields 19 | .into_iter() 20 | .map(FieldType::from) 21 | .collect(); 22 | 23 | InterfaceType { 24 | name: interface_type.name, 25 | description: interface_type.description, 26 | position: interface_type.position, 27 | directives, 28 | fields, 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/input_object.rs: -------------------------------------------------------------------------------- 1 | use rusty_gql::*; 2 | 3 | #[tokio::test] 4 | pub async fn test_input_obj() { 5 | struct Query; 6 | 7 | #[derive(GqlInputObject)] 8 | pub struct InputObj { 9 | str_value: String, 10 | int_value: i64, 11 | } 12 | 13 | #[GqlType] 14 | impl Query { 15 | async fn input_test(&self, input: InputObj) -> String { 16 | format!("{}*{}", &input.str_value, &input.int_value) 17 | } 18 | } 19 | let contents = schema_content("./tests/schemas/input_object.graphql"); 20 | 21 | let container = Container::new( 22 | &vec![contents.as_str()], 23 | Query, 24 | EmptyMutation, 25 | EmptySubscription, 26 | Default::default(), 27 | ) 28 | .unwrap(); 29 | 30 | let query_doc = r#"{ input_test(input: {str_value: "test", int_value: 2} ) }"#; 31 | let req = build_test_request(query_doc, None, Default::default()); 32 | let expected_response = r#"{"data":{"input_test":"test*2"}}"#; 33 | check_gql_response(req, expected_response, &container).await; 34 | } 35 | -------------------------------------------------------------------------------- /src/resolver/optional.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | CollectFields, Context, FieldResolver, GqlValue, ResolverResult, SelectionSetContext, 3 | SelectionSetResolver, 4 | }; 5 | 6 | #[async_trait::async_trait] 7 | impl FieldResolver for Option { 8 | async fn resolve_field(&self, ctx: &Context<'_>) -> ResolverResult> { 9 | match self { 10 | Some(resolver) => resolver.resolve_field(ctx).await, 11 | None => Ok(Some(GqlValue::Null)), 12 | } 13 | } 14 | fn type_name() -> String { 15 | T::type_name() 16 | } 17 | } 18 | 19 | impl CollectFields for Option {} 20 | 21 | #[async_trait::async_trait] 22 | impl SelectionSetResolver for Option { 23 | async fn resolve_selection_set( 24 | &self, 25 | ctx: &SelectionSetContext<'_>, 26 | ) -> ResolverResult { 27 | match self { 28 | Some(resolver) => resolver.resolve_selection_set(ctx).await, 29 | None => Ok(GqlValue::Null), 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/types/id.rs: -------------------------------------------------------------------------------- 1 | use std::num::ParseIntError; 2 | use std::ops::{Deref, DerefMut}; 3 | 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Clone, Ord, PartialEq, PartialOrd, Hash, Serialize, Deserialize, Eq, Default, Debug)] 7 | pub struct ID(pub String); 8 | 9 | impl Deref for ID { 10 | type Target = String; 11 | 12 | fn deref(&self) -> &Self::Target { 13 | &self.0 14 | } 15 | } 16 | 17 | impl DerefMut for ID { 18 | fn deref_mut(&mut self) -> &mut Self::Target { 19 | &mut self.0 20 | } 21 | } 22 | 23 | impl> From for ID { 24 | fn from(v: T) -> Self { 25 | ID(v.into()) 26 | } 27 | } 28 | 29 | macro_rules! try_from_integers { 30 | ($($ty:ty),*) => { 31 | $( 32 | impl TryFrom for $ty { 33 | type Error = ParseIntError; 34 | 35 | fn try_from(id: ID) -> Result { 36 | id.0.parse() 37 | } 38 | } 39 | )* 40 | }; 41 | } 42 | 43 | try_from_integers!(i8, i16, i32, i64, i128, u8, u16, u32, u64, u128, isize, usize); 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Tak Iwamoto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/types/input_object.rs: -------------------------------------------------------------------------------- 1 | use graphql_parser::{schema::InputObjectType as ParserInputObjectType, Pos}; 2 | 3 | use super::{argument::InputValueType, directive::GqlDirective}; 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct InputObjectType { 7 | pub name: String, 8 | pub description: Option, 9 | pub position: Pos, 10 | pub directives: Vec, 11 | pub fields: Vec, 12 | } 13 | 14 | impl<'a> From> for InputObjectType { 15 | fn from(input_object: ParserInputObjectType<'a, String>) -> Self { 16 | let directives = input_object 17 | .directives 18 | .into_iter() 19 | .map(GqlDirective::from) 20 | .collect(); 21 | 22 | let fields = input_object 23 | .fields 24 | .into_iter() 25 | .map(InputValueType::from) 26 | .collect(); 27 | 28 | InputObjectType { 29 | name: input_object.name, 30 | description: input_object.description, 31 | position: input_object.position, 32 | directives, 33 | fields, 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/mutation.rs: -------------------------------------------------------------------------------- 1 | #![allow(warnings, unused)] 2 | use rusty_gql::*; 3 | 4 | #[tokio::test] 5 | pub async fn test_mutation() { 6 | struct Query; 7 | 8 | #[GqlType] 9 | impl Query { 10 | async fn value(&self) -> i32 { 11 | 10 12 | } 13 | } 14 | 15 | struct Mutation; 16 | 17 | #[derive(GqlInputObject)] 18 | struct MutationInput { 19 | value: String, 20 | } 21 | 22 | #[GqlType] 23 | impl Mutation { 24 | async fn testMutation(&self, input: MutationInput) -> bool { 25 | true 26 | } 27 | } 28 | let contents = schema_content("./tests/schemas/test_schema.graphql"); 29 | 30 | let container = Container::new( 31 | &vec![contents.as_str()], 32 | Query, 33 | Mutation, 34 | EmptySubscription, 35 | Default::default(), 36 | ) 37 | .unwrap(); 38 | 39 | let query_doc = r#"mutation { testMutation(input: {value: "test"}) }"#; 40 | let req = build_test_request(query_doc, None, Default::default()); 41 | let expected_response = r#"{"data":{"testMutation":true}}"#; 42 | check_gql_response(req, expected_response, &container).await; 43 | } 44 | -------------------------------------------------------------------------------- /cli/src/code_generate/mod_file.rs: -------------------------------------------------------------------------------- 1 | use heck::ToSnakeCase; 2 | use tokio::io::AsyncWriteExt; 3 | 4 | use super::{path_str, CreateFile}; 5 | 6 | pub struct ModFile<'a> { 7 | pub struct_names: Vec, 8 | pub path: &'a str, 9 | } 10 | 11 | impl<'a> ModFile<'a> { 12 | fn content(&self) -> String { 13 | let mut mod_str = String::from(""); 14 | let mut pub_use_str = String::from(""); 15 | for name in &self.struct_names { 16 | let filename = &name.to_snake_case(); 17 | mod_str += format!("mod {};\n", &filename).as_str(); 18 | pub_use_str += format!("pub use {}::{};\n", &filename, &name).as_str(); 19 | } 20 | 21 | format!("{}\n{}", mod_str, pub_use_str) 22 | } 23 | 24 | fn path(&self) -> String { 25 | path_str(vec![self.path, "mod"], true) 26 | } 27 | } 28 | 29 | #[async_trait::async_trait] 30 | impl<'a> CreateFile for ModFile<'a> { 31 | async fn create_file(&self) -> Result<(), std::io::Error> { 32 | let path = self.path(); 33 | let mut file = tokio::fs::File::create(&path).await?; 34 | file.write(self.content().as_bytes()).await?; 35 | Ok(()) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /cli/src/code_generate/operation/field_file.rs: -------------------------------------------------------------------------------- 1 | use codegen::{Scope, Type}; 2 | use rusty_gql::FieldType; 3 | 4 | use crate::code_generate::{use_gql_definitions, util::gql_value_ty_to_rust_ty, FileDefinition}; 5 | 6 | pub struct FieldFile<'a> { 7 | pub filename: String, 8 | pub def: &'a FieldType, 9 | pub path: String, 10 | } 11 | 12 | impl<'a> FileDefinition for FieldFile<'a> { 13 | fn name(&self) -> String { 14 | self.filename.clone() 15 | } 16 | 17 | fn path(&self) -> String { 18 | self.path.to_string() 19 | } 20 | 21 | fn content(&self) -> String { 22 | let mut scope = Scope::new(); 23 | let fn_scope = scope.new_fn(&self.def.name); 24 | fn_scope.vis("pub"); 25 | fn_scope.set_async(true); 26 | fn_scope.arg("ctx", "&Context<'_>"); 27 | 28 | for arg in &self.def.arguments { 29 | fn_scope.arg(&arg.name, gql_value_ty_to_rust_ty(&arg.meta_type)); 30 | } 31 | 32 | let return_ty = gql_value_ty_to_rust_ty(&self.def.meta_type); 33 | 34 | fn_scope.ret(Type::new(&return_ty)); 35 | fn_scope.line("todo!()"); 36 | 37 | format!("{}\n\n{}", use_gql_definitions(), scope.to_string()) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/variables.rs: -------------------------------------------------------------------------------- 1 | use rusty_gql::*; 2 | 3 | #[tokio::test] 4 | pub async fn test_variables() { 5 | struct Query; 6 | 7 | #[GqlType] 8 | impl Query { 9 | async fn twice_value(&self, value: i32) -> i32 { 10 | value * 2 11 | } 12 | } 13 | let contents = schema_content("./tests/schemas/test_schema.graphql"); 14 | 15 | let container = Container::new( 16 | &vec![contents.as_str()], 17 | Query, 18 | EmptyMutation, 19 | EmptySubscription, 20 | Default::default(), 21 | ) 22 | .unwrap(); 23 | 24 | let query_doc = r#"{ twice_value(value: 10) }"#; 25 | let req = build_test_request(query_doc, None, Default::default()); 26 | let expected_response = r#"{"data":{"twice_value":20}}"#; 27 | check_gql_response(req, expected_response, &container).await; 28 | 29 | let query_doc = r#"query TestQueryWithVars($value: Int!){twice_value(value: $value)}"#; 30 | let variables_str = r#"{"value": 20}"#; 31 | let variables = serde_json::from_str::(variables_str).unwrap(); 32 | let req = build_test_request(query_doc, None, variables); 33 | let expected_response = r#"{"data":{"twice_value":40}}"#; 34 | check_gql_response(req, expected_response, &container).await; 35 | } 36 | -------------------------------------------------------------------------------- /examples/axum/src/graphql/query/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(warnings, unused)] 2 | use crate::graphql::*; 3 | use rusty_gql::*; 4 | 5 | mod character; 6 | mod droid; 7 | mod hero; 8 | mod human; 9 | mod reviews; 10 | mod search; 11 | 12 | #[derive(Clone)] 13 | pub struct Query; 14 | 15 | #[GqlType] 16 | impl Query { 17 | pub async fn droid(&self, ctx: &Context<'_>, id: ID) -> Option { 18 | droid::droid(ctx, id).await 19 | } 20 | 21 | pub async fn character(&self, ctx: &Context<'_>, id: ID) -> Option { 22 | character::character(ctx, id).await 23 | } 24 | 25 | pub async fn search( 26 | &self, 27 | ctx: &Context<'_>, 28 | text: Option, 29 | episode: Option, 30 | ) -> Vec { 31 | search::search(ctx, text, episode).await 32 | } 33 | 34 | pub async fn human(&self, ctx: &Context<'_>, id: ID) -> Option { 35 | human::human(ctx, id).await 36 | } 37 | 38 | pub async fn hero(&self, ctx: &Context<'_>, episode: Option) -> Option { 39 | hero::hero(ctx, episode).await 40 | } 41 | 42 | pub async fn reviews(&self, ctx: &Context<'_>, episode: Episode) -> Vec { 43 | reviews::reviews(ctx, episode).await 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/types/argument.rs: -------------------------------------------------------------------------------- 1 | use graphql_parser::{schema::InputValue, Pos}; 2 | 3 | use super::{directive::GqlDirective, value::GqlValue, value_type::GqlValueType}; 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct InputValueType { 7 | pub name: String, 8 | pub description: Option, 9 | pub position: Pos, 10 | pub meta_type: GqlValueType, 11 | pub default_value: Option, 12 | pub directives: Vec, 13 | } 14 | 15 | impl InputValueType { 16 | pub fn from_vec_input_value(input_objects: Vec>) -> Vec { 17 | input_objects 18 | .into_iter() 19 | .map(InputValueType::from) 20 | .collect() 21 | } 22 | } 23 | 24 | impl<'a> From> for InputValueType { 25 | fn from(input_value: InputValue<'a, String>) -> Self { 26 | let meta_type = GqlValueType::from(input_value.value_type); 27 | let default_value = input_value.default_value.map(GqlValue::from); 28 | let directives = GqlDirective::from_vec_directive(input_value.directives); 29 | 30 | InputValueType { 31 | name: input_value.name, 32 | description: input_value.description, 33 | position: input_value.position, 34 | meta_type, 35 | default_value, 36 | directives, 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /cli/src/code_generate/project/mod.rs: -------------------------------------------------------------------------------- 1 | mod axum; 2 | mod example_schema_file; 3 | mod gitignore_file; 4 | 5 | use std::io::Error; 6 | 7 | use futures_util::future::try_join_all; 8 | 9 | use self::{ 10 | axum::{AxumCargoTomlFile, AxumMainFile}, 11 | example_schema_file::TodoSchemaFile, 12 | gitignore_file::GitignoreFile, 13 | }; 14 | 15 | use super::create_file; 16 | 17 | pub async fn create_project_files(app_name: &str) -> Result<(), Error> { 18 | try_join_all(vec![ 19 | tokio::fs::create_dir_all(format!("{}/src", app_name).as_str()), 20 | tokio::fs::create_dir_all(format!("{}/schema", app_name).as_str()), 21 | ]) 22 | .await?; 23 | create_main_file(app_name).await?; 24 | create_cargo_toml(app_name).await?; 25 | create_example_gql_schema(app_name).await?; 26 | create_gitignore_file(app_name).await 27 | } 28 | 29 | async fn create_main_file(app_name: &str) -> Result<(), Error> { 30 | create_file(AxumMainFile { app_name }).await 31 | } 32 | 33 | async fn create_cargo_toml(app_name: &str) -> Result<(), Error> { 34 | create_file(AxumCargoTomlFile { app_name }).await 35 | } 36 | 37 | async fn create_example_gql_schema(app_name: &str) -> Result<(), Error> { 38 | create_file(TodoSchemaFile { app_name }).await 39 | } 40 | 41 | async fn create_gitignore_file(app_name: &str) -> Result<(), Error> { 42 | create_file(GitignoreFile { app_name }).await 43 | } 44 | -------------------------------------------------------------------------------- /docs/src/schema/mutation.md: -------------------------------------------------------------------------------- 1 | # Mutation 2 | 3 | Mutation has a similar directory structure to Query. 4 | 5 | rusty-gql has Mutation files under `src/graphql/mutation/**`. 6 | 7 | ``` 8 | src 9 | ┣ graphql 10 | ┃ ┣ mutation 11 | ┃ ┃ ┣ mod.rs 12 | ┃ ┃ ┗ create_todo.rs 13 | ``` 14 | 15 | src/graphql/mutation/create_todo.rs 16 | 17 | ```rust 18 | #![allow(warnings, unused)] 19 | use crate::graphql::*; 20 | use rusty_gql::*; 21 | 22 | pub async fn createTodo(ctx: &Context<'_>, input: TodoInput) -> Todo { 23 | ... 24 | } 25 | ``` 26 | 27 | src/graphql/mutation/mod.rs 28 | 29 | ```rust 30 | #![allow(warnings, unused)] 31 | use crate::graphql::*; 32 | use rusty_gql::*; 33 | mod create_todo; 34 | 35 | #[derive(Clone)] 36 | pub struct Mutation; 37 | 38 | #[GqlType] 39 | impl Mutation { 40 | pub async fn todos(&self, ctx: &Context<'_>, input: TodoInput) -> Todo { 41 | create_todo::createTodo(ctx, input).await 42 | } 43 | } 44 | ``` 45 | 46 | Mutation is optional, so if we don't need Mutation, use `EmptyMutation` struct in `main.rs` 47 | 48 | main.rs 49 | 50 | ```rust 51 | mod graphql; 52 | ... 53 | 54 | #[tokio::main] 55 | async fn main() { 56 | ... 57 | let container = Container::new( 58 | schema_docs.as_slice(), 59 | Query, 60 | EmptyMutation, // or graphql::Mutation 61 | EmptySubscription, 62 | Default::default(), 63 | ) 64 | .unwrap(); 65 | } 66 | 67 | ``` 68 | -------------------------------------------------------------------------------- /examples/axum/src/graphql/resolver/character.rs: -------------------------------------------------------------------------------- 1 | #![allow(warnings, unused)] 2 | use crate::graphql::*; 3 | use rusty_gql::*; 4 | 5 | #[derive(GqlInterface, Debug, Clone)] 6 | pub enum Character { 7 | Human(Human), 8 | Droid(Droid), 9 | } 10 | 11 | #[GqlType(interface)] 12 | impl Character { 13 | async fn id(&self, ctx: &Context<'_>) -> Result { 14 | match self { 15 | Character::Human(obj) => obj.id(&ctx).await, 16 | Character::Droid(obj) => obj.id(&ctx).await, 17 | } 18 | } 19 | 20 | async fn name(&self, ctx: &Context<'_>) -> Result { 21 | match self { 22 | Character::Human(obj) => obj.name(&ctx).await, 23 | Character::Droid(obj) => obj.name(&ctx).await, 24 | } 25 | } 26 | 27 | async fn friends( 28 | &self, 29 | ctx: &Context<'_>, 30 | first: Option, 31 | after: Option, 32 | ) -> Result { 33 | match self { 34 | Character::Human(obj) => obj.friends(&ctx, first, after).await, 35 | Character::Droid(obj) => obj.friends(&ctx, first, after).await, 36 | } 37 | } 38 | 39 | async fn appearsIn(&self, ctx: &Context<'_>) -> Result, Error> { 40 | match self { 41 | Character::Human(obj) => obj.appearsIn(&ctx).await, 42 | Character::Droid(obj) => obj.appearsIn(&ctx).await, 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /cli/src/code_generate/operation/mod.rs: -------------------------------------------------------------------------------- 1 | mod field_file; 2 | mod operation_mod_file; 3 | 4 | use futures_util::future::try_join_all; 5 | use heck::ToSnakeCase; 6 | use rusty_gql::{self, FieldType, OperationType}; 7 | use std::{collections::HashMap, io::Error}; 8 | 9 | use self::{field_file::FieldFile, operation_mod_file::OperationModFile}; 10 | 11 | use super::{create_file, path_str}; 12 | 13 | pub async fn create_operation_files( 14 | operations: &HashMap, 15 | operation_type: OperationType, 16 | base_path: &str, 17 | ) -> Result, Error> { 18 | let mut futures = Vec::new(); 19 | 20 | for (_, field) in operations.iter() { 21 | let filename = field.name.to_snake_case(); 22 | let path = path_str( 23 | vec![ 24 | base_path, 25 | &operation_type.to_string().to_lowercase(), 26 | &filename, 27 | ], 28 | true, 29 | ); 30 | let task = create_file(FieldFile { 31 | filename, 32 | def: field, 33 | path, 34 | }); 35 | futures.push(task); 36 | } 37 | 38 | create_file(OperationModFile { 39 | operation_type: operation_type.clone(), 40 | operations, 41 | path: path_str( 42 | vec![base_path, &operation_type.to_string().to_lowercase(), "mod"], 43 | true, 44 | ), 45 | }) 46 | .await?; 47 | 48 | try_join_all(futures).await 49 | } 50 | -------------------------------------------------------------------------------- /docs/src/schema/query.md: -------------------------------------------------------------------------------- 1 | # Query 2 | 3 | rusty-gql has Query files under `src/graphql/query/**`. 4 | 5 | For example, 6 | 7 | ``` 8 | src 9 | ┣ graphql 10 | ┃ ┣ query 11 | ┃ ┃ ┣ mod.rs 12 | ┃ ┃ ┗ todos.rs 13 | ``` 14 | 15 | src/graphql/query/todos.rs 16 | 17 | ```rust 18 | #![allow(warnings, unused)] 19 | use crate::graphql::*; 20 | use rusty_gql::*; 21 | 22 | pub async fn todos(ctx: &Context<'_>, first: Option) -> Vec { 23 | let all_todos = vec![ 24 | Todo { 25 | title: "Programming".to_string(), 26 | content: Some("Learn Rust".to_string()), 27 | done: false, 28 | }, 29 | Todo { 30 | title: "Shopping".to_string(), 31 | content: None, 32 | done: true, 33 | }, 34 | ]; 35 | match first { 36 | Some(first) => all_todos.into_iter().take(first as usize).collect(), 37 | None => all_todos, 38 | } 39 | } 40 | ``` 41 | 42 | src/graphql/query/mod.rs 43 | 44 | ```rust 45 | #![allow(warnings, unused)] 46 | use crate::graphql::*; 47 | use rusty_gql::*; 48 | mod todos; 49 | 50 | #[derive(Clone)] 51 | pub struct Query; 52 | 53 | #[GqlType] 54 | impl Query { 55 | pub async fn todos(&self, ctx: &Context<'_>, first: Option) -> Vec { 56 | todos::todos(&ctx,first).await 57 | } 58 | } 59 | ``` 60 | 61 | Files except for `mod.rs` implements resolvers for each Query fields. 62 | 63 | `mod.rs` only bundles these files and defines `Query` struct. 64 | -------------------------------------------------------------------------------- /macro/src/scalar.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::{self, TokenStream}; 2 | use quote::quote; 3 | use syn::{ext::IdentExt, DeriveInput}; 4 | 5 | pub fn generate_scalar(derive_input: &DeriveInput) -> Result { 6 | let self_ty = &derive_input.ident; 7 | let crate_name = quote! { rusty_gql }; 8 | 9 | let type_name = self_ty.unraw().to_string(); 10 | 11 | let (impl_generics, _, where_clause) = &derive_input.generics.split_for_impl(); 12 | 13 | let expanded = quote! { 14 | #[#crate_name::async_trait::async_trait] 15 | impl #impl_generics #crate_name::FieldResolver for #self_ty #where_clause { 16 | async fn resolve_field(&self, ctx: &#crate_name::Context<'_>) -> #crate_name::ResolverResult<::std::option::Option<#crate_name::GqlValue>> { 17 | Ok(Some(self.to_gql_value())) 18 | } 19 | fn type_name() -> String { 20 | #type_name.to_string() 21 | } 22 | } 23 | 24 | impl #impl_generics #crate_name::CollectFields for #self_ty #where_clause {} 25 | 26 | #[#crate_name::async_trait::async_trait] 27 | impl #impl_generics #crate_name::SelectionSetResolver for #self_ty #where_clause { 28 | async fn resolve_selection_set(&self, ctx: &#crate_name::SelectionSetContext<'_>) -> #crate_name::ResolverResult<#crate_name::GqlValue> { 29 | Ok(self.to_gql_value()) 30 | } 31 | } 32 | }; 33 | 34 | Ok(expanded.into()) 35 | } 36 | -------------------------------------------------------------------------------- /src/validation/rules/possible_fragment_spreads.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use graphql_parser::query::{Definition, Document, FragmentSpread, TypeCondition}; 4 | 5 | use crate::validation::visitor::{ValidationContext, Visitor}; 6 | 7 | #[derive(Default)] 8 | pub struct PossibleFragmentSpreads<'a> { 9 | fragment_types: HashMap<&'a str, &'a TypeCondition<'a, String>>, 10 | } 11 | 12 | impl<'a> Visitor<'a> for PossibleFragmentSpreads<'a> { 13 | fn enter_document(&mut self, _ctx: &mut ValidationContext<'a>, doc: &'a Document<'a, String>) { 14 | for def in &doc.definitions { 15 | if let Definition::Fragment(fragment) = def { 16 | self.fragment_types 17 | .insert(&fragment.name, &fragment.type_condition); 18 | } 19 | } 20 | } 21 | 22 | fn enter_fragment_spread( 23 | &mut self, 24 | ctx: &mut ValidationContext, 25 | fragment_spread: &'a FragmentSpread<'a, String>, 26 | ) { 27 | if let Some(fragment_type) = self 28 | .fragment_types 29 | .get(&fragment_spread.fragment_name.as_str()) 30 | { 31 | if let Some(_current_type) = ctx.current_type() { 32 | match fragment_type { 33 | TypeCondition::On(on_type) => { 34 | if let Some(_schema_on_type) = ctx.schema.type_definitions.get(on_type) {} 35 | } 36 | } 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/types/field.rs: -------------------------------------------------------------------------------- 1 | use graphql_parser::schema::Field; 2 | use graphql_parser::Pos; 3 | 4 | use super::argument::InputValueType; 5 | use super::directive::GqlDirective; 6 | use super::value_type::GqlValueType; 7 | 8 | #[derive(Debug, Clone)] 9 | pub struct FieldType { 10 | pub name: String, 11 | pub description: Option, 12 | pub position: Pos, 13 | pub meta_type: GqlValueType, 14 | pub arguments: Vec, 15 | pub directives: Vec, 16 | } 17 | 18 | impl FieldType { 19 | pub fn from_vec_field(fields: Vec>) -> Vec { 20 | fields.into_iter().map(FieldType::from).collect() 21 | } 22 | 23 | pub fn is_deprecated(&self) -> bool { 24 | for dir in &self.directives { 25 | if dir.name == "deprecated" { 26 | return true; 27 | } 28 | continue; 29 | } 30 | false 31 | } 32 | } 33 | 34 | impl<'a> From> for FieldType { 35 | fn from(field: Field<'a, String>) -> Self { 36 | let meta_type = GqlValueType::from(field.field_type); 37 | let directives = GqlDirective::from_vec_directive(field.directives); 38 | let arguments = InputValueType::from_vec_input_value(field.arguments); 39 | 40 | FieldType { 41 | name: field.name, 42 | description: field.description, 43 | position: field.position, 44 | meta_type, 45 | directives, 46 | arguments, 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/axum/src/main.rs: -------------------------------------------------------------------------------- 1 | mod graphql; 2 | mod starwars; 3 | 4 | use rusty_gql::*; 5 | use rusty_gql_axum::*; 6 | use std::{net::SocketAddr, path::Path}; 7 | 8 | use axum::{ 9 | extract::Extension, 10 | response::{self, IntoResponse}, 11 | routing::get, 12 | AddExtensionLayer, Router, 13 | }; 14 | use graphql::{Mutation, Query}; 15 | 16 | type ContainerType = Container; 17 | 18 | async fn gql_handler(container: Extension, req: GqlRequest) -> GqlResponse { 19 | let result = execute(&container, req.0).await; 20 | GqlResponse::from(result) 21 | } 22 | 23 | async fn gql_playground() -> impl IntoResponse { 24 | response::Html(playground_html("/", None)) 25 | } 26 | 27 | #[tokio::main] 28 | async fn main() { 29 | let schema_docs = read_schemas(Path::new("./examples/axum/schemas")).unwrap(); 30 | let schema_docs: Vec<&str> = schema_docs.iter().map(|s| &**s).collect(); 31 | 32 | let container = Container::new( 33 | schema_docs.as_slice(), 34 | Query, 35 | Mutation, 36 | EmptySubscription, 37 | Default::default(), 38 | ) 39 | .unwrap(); 40 | let app = Router::new() 41 | .route("/graphiql", get(gql_playground)) 42 | .route("/", get(gql_handler).post(gql_handler)) 43 | .layer(AddExtensionLayer::new(container)); 44 | let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); 45 | axum::Server::bind(&addr) 46 | .serve(app.into_make_service()) 47 | .await 48 | .unwrap(); 49 | } 50 | -------------------------------------------------------------------------------- /docs/src/directory_structure.md: -------------------------------------------------------------------------------- 1 | # Directory Structure 2 | 3 | A rusty-gql project has the following directory structure. 4 | 5 | ``` 6 | rusty-gql-project 7 | ┣ schema 8 | ┃ ┗ schema.graphql 9 | ┣ src 10 | ┃ ┣ graphql 11 | ┃ ┃ ┣ directive 12 | ┃ ┃ ┃ ┗ mod.rs 13 | ┃ ┃ ┣ input 14 | ┃ ┃ ┃ ┗ mod.rs 15 | ┃ ┃ ┣ mutation 16 | ┃ ┃ ┃ ┗ mod.rs 17 | ┃ ┃ ┣ query 18 | ┃ ┃ ┃ ┣ mod.rs 19 | ┃ ┃ ┣ resolver 20 | ┃ ┃ ┃ ┣ mod.rs 21 | ┃ ┃ ┣ scalar 22 | ┃ ┃ ┃ ┗ mod.rs 23 | ┃ ┃ ┗ mod.rs 24 | ┃ ┗ main.rs 25 | ┗ Cargo.toml 26 | ``` 27 | 28 | ## schema 29 | 30 | GraphQL schema files are located under `schema/**`. 31 | 32 | We can also place multiple GraphQL files. 33 | 34 | For example, like this. 35 | 36 | ``` 37 | schema 38 | ┣ post 39 | ┃ ┗ post.graphql 40 | ┣ user 41 | ┃ ┗ user.graphql 42 | ┗ index.graphql 43 | ``` 44 | 45 | ## src/graphql/query 46 | 47 | Query resolvers. 48 | 49 | [Query](./schema/query.md) 50 | 51 | ## src/graphql/mutation 52 | 53 | Mutation resolvers. 54 | 55 | [Mutation](./schema/mutation.md) 56 | 57 | ## src/graphql/resolver 58 | 59 | GraphQL `Object`, `Enum`, `Union`, `Interface` types. 60 | 61 | - [Object](./types/object.md) 62 | - [Enum](./types/enum.md) 63 | - [Union](./types/union.md) 64 | - [Interface](./types/interface.md) 65 | 66 | ## src/graphql/input 67 | 68 | GraphQL InputObject. 69 | 70 | [InputObject](./types/input_object.md) 71 | 72 | ## src/graphql/scalar 73 | 74 | Custom scalars. 75 | 76 | [Scalar](./types/scalar.md) 77 | 78 | ## src/graphql/directive 79 | 80 | Custom directives. 81 | 82 | [Directive](./types/directive.md) 83 | -------------------------------------------------------------------------------- /docs/src/types/object.md: -------------------------------------------------------------------------------- 1 | # Object 2 | 3 | rusty-gql defines GraphQL Object as Rust struct and `#[GqlType]` like the following. 4 | 5 | src/graphql/resolver/todo.rs 6 | 7 | ```rust 8 | #[derive(Clone)] 9 | pub struct Todo { 10 | pub title: String, 11 | pub content: Option, 12 | pub done: bool, 13 | } 14 | 15 | #[GqlType] 16 | impl Todo { 17 | pub async fn title(&self, ctx: &Context<'_>) -> String { 18 | self.title.clone() 19 | } 20 | 21 | pub async fn content(&self, ctx: &Context<'_>) -> Option { 22 | self.content.clone() 23 | } 24 | 25 | pub async fn done(&self, ctx: &Context<'_>) -> bool { 26 | self.done 27 | } 28 | } 29 | ``` 30 | 31 | schema.graphql 32 | 33 | ```graphql 34 | type Todo { 35 | title: String! 36 | content: String 37 | done: Boolean! 38 | } 39 | ``` 40 | 41 | We'll implement `async fn` for each fields with `#[GqlType]`. 42 | 43 | If we want to execute only when the field is included in a operation, implement `async fn` without the struct field. 44 | 45 | src/graphql/resolver/todo.rs 46 | 47 | ```rust 48 | #[derive(Clone)] 49 | pub struct Todo { 50 | pub title: String, 51 | pub content: Option, 52 | pub done: bool, 53 | } 54 | 55 | #[GqlType] 56 | impl Todo { 57 | ... 58 | pub async fn user(&self, ctx: &Context<'_>) -> User { 59 | todo!() 60 | } 61 | } 62 | ``` 63 | 64 | ```graphql 65 | type Todo { 66 | title: String! 67 | content: String 68 | done: Boolean! 69 | user: User! 70 | } 71 | 72 | type User { 73 | name 74 | } 75 | ``` 76 | -------------------------------------------------------------------------------- /cli/src/code_generate/type_definition/scalar_file.rs: -------------------------------------------------------------------------------- 1 | use codegen::Scope; 2 | use rusty_gql::ScalarType; 3 | use tokio::io::AsyncWriteExt; 4 | 5 | use crate::code_generate::{use_gql_definitions, CreateFile}; 6 | 7 | pub struct ScalarFile<'a> { 8 | pub filename: &'a str, 9 | pub def: &'a ScalarType, 10 | pub path: &'a str, 11 | } 12 | 13 | #[async_trait::async_trait] 14 | impl<'a> CreateFile for ScalarFile<'a> { 15 | async fn create_file(&self) -> Result<(), std::io::Error> { 16 | let path = self.path; 17 | match tokio::fs::File::open(&path).await { 18 | Ok(_) => Ok(()), 19 | Err(_) => { 20 | let mut file = tokio::fs::File::create(&path).await?; 21 | file.write(new_file_content(self.def).as_bytes()).await?; 22 | Ok(()) 23 | } 24 | } 25 | } 26 | } 27 | 28 | fn new_file_content(scalar_def: &ScalarType) -> String { 29 | let mut scope = Scope::new(); 30 | let struct_name = &scalar_def.name; 31 | let scalar_scope = scope.new_struct(struct_name).vis("pub"); 32 | scalar_scope.derive("GqlScalar"); 33 | 34 | let scalar_impl = scope.new_impl(struct_name); 35 | scalar_impl.impl_trait("GqlInputType"); 36 | let from_gql_value_fn = scalar_impl.new_fn("from_gql_value"); 37 | from_gql_value_fn.arg("value", "Option"); 38 | from_gql_value_fn.ret("Result"); 39 | from_gql_value_fn.line("todo!()"); 40 | 41 | let to_gql_value_fn = scalar_impl.new_fn("to_gql_value"); 42 | to_gql_value_fn.arg_ref_self(); 43 | to_gql_value_fn.ret("GqlValue"); 44 | to_gql_value_fn.line("todo!()"); 45 | 46 | format!("{}\n\n{}", use_gql_definitions(), scope.to_string()) 47 | } 48 | -------------------------------------------------------------------------------- /examples/axum/schemas/schema.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | hero(episode: Episode): Character 3 | reviews(episode: Episode!): [Review] 4 | search(text: String, episode: Episode): [SearchResult] 5 | character(id: ID!): Character 6 | droid(id: ID!): Droid 7 | human(id: ID!): Human 8 | } 9 | 10 | type Mutation { 11 | createReview(episode: Episode, review: ReviewInput!): Review 12 | } 13 | 14 | enum Episode { 15 | NEWHOPE 16 | EMPIRE 17 | JEDI 18 | } 19 | 20 | interface Character { 21 | id: ID! 22 | name: String! 23 | friends(first: Int, after: ID): FriendsConnection! 24 | appearsIn: [Episode]! 25 | } 26 | 27 | enum LengthUnit { 28 | METER 29 | FOOT 30 | } 31 | 32 | type Human implements Character { 33 | id: ID! 34 | name: String! 35 | homePlanet: String 36 | height(unit: LengthUnit = METER): Float 37 | mass: Float 38 | episode: Episode 39 | friends(first: Int, after: ID): FriendsConnection! 40 | appearsIn: [Episode]! 41 | } 42 | 43 | type Droid implements Character { 44 | id: ID! 45 | name: String! 46 | friends(first: Int, after: ID): FriendsConnection! 47 | appearsIn: [Episode]! 48 | primaryFunction: String 49 | } 50 | 51 | type FriendsConnection { 52 | totalCount: Int 53 | edges: [FriendsEdge] 54 | pageInfo: PageInfo! 55 | } 56 | 57 | type FriendsEdge { 58 | cursor: ID! 59 | node: Character 60 | } 61 | 62 | type PageInfo { 63 | startCursor: ID 64 | endCursor: ID 65 | hasPreviousPage: Boolean! 66 | hasNextPage: Boolean! 67 | } 68 | 69 | type Review { 70 | episode: Episode 71 | stars: Int! 72 | commentary: String 73 | } 74 | 75 | input ReviewInput { 76 | stars: Int! 77 | commentary: String 78 | } 79 | 80 | union SearchResult = Human | Droid 81 | 82 | scalar DateTime 83 | -------------------------------------------------------------------------------- /tests/schemas/starwars.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | hero(episode: Episode): Character 3 | reviews(episode: Episode!): [Review] 4 | search(text: String, episode: Episode): [SearchResult!]! 5 | character(id: ID!): Character 6 | droid(id: ID!): Droid 7 | human(id: ID!): Human 8 | } 9 | 10 | type Mutation { 11 | createReview(episode: Episode, review: ReviewInput!): Review 12 | } 13 | 14 | enum Episode { 15 | NEWHOPE 16 | EMPIRE 17 | JEDI 18 | } 19 | 20 | interface Character { 21 | id: ID! 22 | name: String! 23 | friends(first: Int, after: ID): FriendsConnection! 24 | appearsIn: [Episode!]! 25 | } 26 | 27 | enum LengthUnit { 28 | METER 29 | FOOT 30 | } 31 | 32 | type Human implements Character { 33 | id: ID! 34 | name: String! 35 | homePlanet: String 36 | height(unit: LengthUnit = METER): Float 37 | mass: Float 38 | episode: Episode 39 | friends(first: Int, after: ID): FriendsConnection! 40 | appearsIn: [Episode!]! 41 | } 42 | 43 | type Droid implements Character { 44 | id: ID! 45 | name: String! 46 | friends(first: Int, after: ID): FriendsConnection! 47 | appearsIn: [Episode]! 48 | primaryFunction: String 49 | } 50 | 51 | type FriendsConnection { 52 | totalCount: Int 53 | edges: [FriendsEdge!]! 54 | pageInfo: PageInfo! 55 | } 56 | 57 | type FriendsEdge { 58 | cursor: ID! 59 | node: Character 60 | } 61 | 62 | type PageInfo { 63 | startCursor: ID 64 | endCursor: ID 65 | hasPreviousPage: Boolean! 66 | hasNextPage: Boolean! 67 | } 68 | 69 | type Review { 70 | episode: Episode 71 | stars: Int! 72 | commentary: String 73 | } 74 | 75 | input ReviewInput { 76 | stars: Int! 77 | commentary: String 78 | } 79 | 80 | union SearchResult = Human | Droid 81 | 82 | scalar DateTime 83 | -------------------------------------------------------------------------------- /docs/src/types/directive.md: -------------------------------------------------------------------------------- 1 | # Directive 2 | 3 | We can use directives as middleware. 4 | 5 | It is useful in the following use cases. 6 | 7 | - Authorization 8 | - Validation 9 | - Caching 10 | - Logging, metrics 11 | - etc. 12 | 13 | If we don't want to expose a specific field, we can define the following directive. 14 | 15 | src/graphql/directive/hidden.rs 16 | 17 | ```rust 18 | #![allow(warnings, unused)] 19 | use crate::graphql::*; 20 | use rusty_gql::*; 21 | 22 | #[derive(Clone)] 23 | struct Hidden; 24 | 25 | impl Hidden { 26 | fn new() -> Box { 27 | Box::new(Hidden {}) 28 | } 29 | } 30 | 31 | #[async_trait::async_trait] 32 | impl CustomDirective for Hidden { 33 | async fn resolve_field( 34 | &self, 35 | _ctx: &Context<'_>, 36 | _directive_args: &BTreeMap, 37 | resolve_fut: ResolveFut<'_>, 38 | ) -> ResolverResult> { 39 | resolve_fut.await.map(|_v| None) 40 | } 41 | } 42 | ``` 43 | 44 | schema.graphql 45 | 46 | ```graphql 47 | type User { 48 | name: String! 49 | password_hash: String @hidden 50 | } 51 | directive @hidden on FIELD_DEFINITION | OBJECT 52 | ``` 53 | 54 | Need to pass a HashMap of directives when Container::new in main.rs. 55 | 56 | A Key is the directive name, a value is the directive struct. 57 | 58 | main.rs 59 | 60 | ```rust 61 | async fn main() { 62 | ... 63 | let mut custom_directive_maps = HashMap::new(); 64 | custom_directive_maps.insert("hidden", Hidden::new()); 65 | 66 | let container = Container::new( 67 | schema_docs.as_slice(), 68 | Query, 69 | Mutation, 70 | EmptySubscription, 71 | custom_directive_maps, // path here 72 | ) 73 | .unwrap(); 74 | ... 75 | } 76 | ``` 77 | -------------------------------------------------------------------------------- /src/validation/rules/mod.rs: -------------------------------------------------------------------------------- 1 | mod arguments_of_correct_type; 2 | mod default_values_of_correct_type; 3 | mod fields_on_correct_type; 4 | mod fragments_on_composite_types; 5 | mod known_argument_names; 6 | mod known_directives; 7 | mod known_fragment_names; 8 | mod known_type_names; 9 | mod no_fragment_cycles; 10 | mod no_undefined_variables; 11 | mod no_unused_fragments; 12 | mod no_unused_variables; 13 | mod overlapping_fields_can_be_merged; 14 | mod possible_fragment_spreads; 15 | mod provided_non_null_arguments; 16 | mod scalar_leafs; 17 | mod unique_argument_names; 18 | mod unique_variable_names; 19 | mod variables_are_input_types; 20 | mod variables_in_allowed_position; 21 | 22 | pub use arguments_of_correct_type::ArgumentsOfCorrectType; 23 | pub use default_values_of_correct_type::DefaultValueOfCorrectType; 24 | pub use fields_on_correct_type::FieldsOnCorrectType; 25 | pub use fragments_on_composite_types::FragmentsOnCompositeTypes; 26 | pub use known_argument_names::KnownArgumentNames; 27 | pub use known_directives::KnownDirectives; 28 | pub use known_fragment_names::KnownFragmentName; 29 | pub use known_type_names::KnownTypeNames; 30 | pub use no_fragment_cycles::NoFragmentCycles; 31 | pub use no_undefined_variables::NoUndefinedVariables; 32 | pub use no_unused_fragments::NoUnusedFragment; 33 | pub use no_unused_variables::NoUnusedVariables; 34 | pub use overlapping_fields_can_be_merged::OverlappingFieldsCanBeMerged; 35 | pub use possible_fragment_spreads::PossibleFragmentSpreads; 36 | pub use provided_non_null_arguments::ProvidedNonNullArguments; 37 | pub use scalar_leafs::ScalarLeafs; 38 | pub use unique_argument_names::UniqueArgumentNames; 39 | pub use unique_variable_names::UniqueVariableNames; 40 | pub use variables_are_input_types::VariablesAreInputTypes; 41 | pub use variables_in_allowed_position::VariablesInAllowedPosition; 42 | -------------------------------------------------------------------------------- /src/validation/rules/unique_variable_names.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use crate::validation::visitor::{ValidationContext, Visitor}; 4 | 5 | #[derive(Default)] 6 | pub struct UniqueVariableNames<'a> { 7 | names: HashSet<&'a str>, 8 | } 9 | 10 | impl<'a> Visitor<'a> for UniqueVariableNames<'a> { 11 | fn enter_operation_definition( 12 | &mut self, 13 | _ctx: &mut ValidationContext<'a>, 14 | _name: Option<&'a str>, 15 | _operation_definition: &'a graphql_parser::query::OperationDefinition<'a, String>, 16 | ) { 17 | self.names.clear(); 18 | } 19 | 20 | fn enter_variable_definition( 21 | &mut self, 22 | ctx: &mut ValidationContext, 23 | variable_definition: &'a graphql_parser::query::VariableDefinition<'a, String>, 24 | ) { 25 | if !self.names.insert(&variable_definition.name) { 26 | ctx.add_error( 27 | format!("{} is already contained.", &variable_definition.name), 28 | vec![variable_definition.position], 29 | ); 30 | } 31 | } 32 | } 33 | 34 | #[cfg(test)] 35 | mod tests { 36 | use crate::{check_fails_rule, check_passes_rule}; 37 | 38 | use super::*; 39 | 40 | fn factory<'a>() -> UniqueVariableNames<'a> { 41 | UniqueVariableNames::default() 42 | } 43 | 44 | #[test] 45 | fn unique_var_names() { 46 | let query_doc = r#" 47 | query Test($a: Int, $b: String) { 48 | __typename 49 | } 50 | "#; 51 | check_passes_rule!(query_doc, factory); 52 | } 53 | 54 | #[test] 55 | fn duplicate_var_names() { 56 | let query_doc = r#" 57 | query Test($a: Int, $a: String) { 58 | __typename 59 | } 60 | "#; 61 | check_fails_rule!(query_doc, factory); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/validation/rules/known_fragment_names.rs: -------------------------------------------------------------------------------- 1 | use graphql_parser::query::FragmentSpread; 2 | 3 | use crate::validation::visitor::{ValidationContext, Visitor}; 4 | 5 | #[derive(Default)] 6 | pub struct KnownFragmentName; 7 | 8 | impl<'a> Visitor<'a> for KnownFragmentName { 9 | fn enter_fragment_spread( 10 | &mut self, 11 | ctx: &mut ValidationContext, 12 | fragment_spread: &'a FragmentSpread<'a, String>, 13 | ) { 14 | if !ctx.fragments.contains_key(&fragment_spread.fragment_name) { 15 | ctx.add_error( 16 | format!("{} is not known fragment", &fragment_spread.fragment_name), 17 | vec![fragment_spread.position], 18 | ) 19 | } 20 | } 21 | } 22 | 23 | #[cfg(test)] 24 | mod tests { 25 | use crate::{check_fails_rule, check_passes_rule}; 26 | 27 | use super::*; 28 | 29 | fn factory() -> KnownFragmentName { 30 | KnownFragmentName::default() 31 | } 32 | #[test] 33 | fn include_known_fragment() { 34 | let query_doc = r#" 35 | { 36 | hero { 37 | ...CharacterFragment1 38 | ... on Character { 39 | ...CharacterFragment2 40 | } 41 | } 42 | } 43 | fragment CharacterFragment1 on Character { 44 | name 45 | } 46 | fragment CharacterFragment2 on Character { 47 | friends 48 | } 49 | "#; 50 | check_passes_rule!(query_doc, factory); 51 | } 52 | 53 | #[test] 54 | fn include_unknown_fragment() { 55 | let query_doc = r#" 56 | { 57 | hero { 58 | ...CharacterFragment1 59 | ... on Character { 60 | ...CharacterFragment2 61 | } 62 | } 63 | } 64 | "#; 65 | check_fails_rule!(query_doc, factory); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /examples/axum/src/graphql/resolver/droid.rs: -------------------------------------------------------------------------------- 1 | #![allow(warnings, unused)] 2 | use crate::{ 3 | graphql::*, 4 | starwars::{han, luke}, 5 | }; 6 | use rusty_gql::*; 7 | 8 | #[derive(Debug, Clone)] 9 | pub struct Droid { 10 | pub id: ID, 11 | pub name: String, 12 | pub primaryFunction: Option, 13 | } 14 | 15 | #[GqlType] 16 | impl Droid { 17 | pub async fn id(&self, ctx: &Context<'_>) -> ID { 18 | self.id.clone() 19 | } 20 | 21 | pub async fn name(&self, ctx: &Context<'_>) -> String { 22 | self.name.clone() 23 | } 24 | 25 | pub async fn friends( 26 | &self, 27 | ctx: &Context<'_>, 28 | first: Option, 29 | after: Option, 30 | ) -> FriendsConnection { 31 | FriendsConnection { 32 | totalCount: Some(4), 33 | edges: vec![ 34 | FriendsEdge { 35 | cursor: ID::from("1"), 36 | node: Some(Character::Human(luke())), 37 | }, 38 | FriendsEdge { 39 | cursor: ID::from("3"), 40 | node: Some(Character::Human(han())), 41 | }, 42 | ], 43 | pageInfo: PageInfo { 44 | startCursor: None, 45 | endCursor: None, 46 | hasPreviousPage: false, 47 | hasNextPage: false, 48 | }, 49 | } 50 | } 51 | 52 | pub async fn appearsIn(&self, ctx: &Context<'_>) -> Vec { 53 | if self.name == "R2D2".to_string() { 54 | vec![Episode::EMPIRE, Episode::NEWHOPE, Episode::JEDI] 55 | } else if self.name == "C3PO".to_string() { 56 | vec![Episode::EMPIRE, Episode::NEWHOPE] 57 | } else { 58 | vec![] 59 | } 60 | } 61 | 62 | pub async fn primaryFunction(&self, ctx: &Context<'_>) -> Option { 63 | self.primaryFunction.clone() 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /macro/src/interface.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::{self, TokenStream}; 2 | use quote::quote; 3 | use syn::DeriveInput; 4 | 5 | pub fn generate_interface(derive_input: &DeriveInput) -> Result { 6 | let self_ty = &derive_input.ident; 7 | let crate_name = quote! { rusty_gql }; 8 | 9 | let (impl_generics, _, where_clause) = &derive_input.generics.split_for_impl(); 10 | 11 | let union_data = match &derive_input.data { 12 | syn::Data::Enum(v) => v, 13 | _ => { 14 | return Err(syn::Error::new_spanned( 15 | &derive_input.ident, 16 | "Union type must be enum rust type", 17 | )); 18 | } 19 | }; 20 | 21 | let mut introspection_type_names = Vec::new(); 22 | let mut collect_all_fields = Vec::new(); 23 | 24 | for variant in &union_data.variants { 25 | let enum_value_ident = &variant.ident; 26 | 27 | introspection_type_names.push(quote! { 28 | #self_ty::#enum_value_ident(obj) => obj.introspection_type_name() 29 | }); 30 | 31 | collect_all_fields.push(quote! { 32 | #self_ty::#enum_value_ident(obj) => obj.collect_all_fields(ctx, fields) 33 | }) 34 | } 35 | 36 | let expanded = quote! { 37 | impl #impl_generics #crate_name::CollectFields for #self_ty #where_clause { 38 | fn introspection_type_name(&self) -> String { 39 | match self { 40 | #(#introspection_type_names),* 41 | } 42 | } 43 | 44 | fn collect_all_fields<'union, 'ctx: 'union>( 45 | &'union self, 46 | ctx: &SelectionSetContext<'ctx>, 47 | fields: &mut Fields<'union>, 48 | ) -> ResolverResult<()> { 49 | match self { 50 | #(#collect_all_fields),* 51 | } 52 | } 53 | } 54 | }; 55 | 56 | Ok(expanded.into()) 57 | } 58 | -------------------------------------------------------------------------------- /src/validation/rules/variables_are_input_types.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | validation::visitor::{ValidationContext, Visitor}, 3 | GqlValueType, 4 | }; 5 | 6 | #[derive(Default)] 7 | pub struct VariablesAreInputTypes; 8 | 9 | impl<'a> Visitor<'a> for VariablesAreInputTypes { 10 | fn enter_variable_definition( 11 | &mut self, 12 | ctx: &mut ValidationContext, 13 | variable_definition: &'a graphql_parser::query::VariableDefinition<'a, String>, 14 | ) { 15 | let ty = ctx 16 | .schema 17 | .type_definitions 18 | .get(GqlValueType::from(variable_definition.var_type.clone()).name()); 19 | 20 | if let Some(variable_type) = ty { 21 | if !variable_type.is_input_type() { 22 | ctx.add_error( 23 | format!( 24 | "Variable {} cannot be non-input type {}", 25 | &variable_definition.name, 26 | variable_type.to_string() 27 | ), 28 | vec![variable_definition.position], 29 | ); 30 | } 31 | } 32 | } 33 | } 34 | 35 | #[cfg(test)] 36 | mod tests { 37 | use crate::{check_fails_rule, check_passes_rule}; 38 | 39 | use super::*; 40 | 41 | fn factory() -> VariablesAreInputTypes { 42 | VariablesAreInputTypes 43 | } 44 | 45 | #[test] 46 | fn valid_types() { 47 | let query_doc = r#" 48 | query Test($a: String, $b:[Int!]!, $c: ReviewInput) { 49 | test_vars(a: $a, b: $b, c: $c) 50 | } 51 | "#; 52 | check_passes_rule!(query_doc, factory); 53 | } 54 | 55 | #[test] 56 | fn invalid_types() { 57 | let query_doc = r#" 58 | query Test($a: Human, $b:[SearchResult!]!, $c: Character) { 59 | test_vars(a: $a, b: $b, c: $c) 60 | } 61 | "#; 62 | check_fails_rule!(query_doc, factory); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/custom_scalar.rs: -------------------------------------------------------------------------------- 1 | use rusty_gql::*; 2 | 3 | #[tokio::test] 4 | pub async fn test_custom_scalar() { 5 | struct Query; 6 | 7 | #[derive(Clone, GqlScalar)] 8 | struct CustomScalar(String); 9 | 10 | impl GqlInputType for CustomScalar { 11 | fn from_gql_value(value: Option) -> Result { 12 | if let Some(GqlValue::String(v)) = value { 13 | Ok(CustomScalar(format!("Custom-{}", v))) 14 | } else { 15 | Err(format!( 16 | "{}: is invalid type for Custom Scalar", 17 | value.unwrap_or(GqlValue::Null).to_string() 18 | )) 19 | } 20 | } 21 | 22 | fn to_gql_value(&self) -> GqlValue { 23 | GqlValue::String(format!("Custom-{}", self.0)) 24 | } 25 | } 26 | 27 | struct SampleResponse { 28 | test: CustomScalar, 29 | } 30 | 31 | #[GqlType] 32 | impl SampleResponse { 33 | async fn test(&self) -> CustomScalar { 34 | self.test.clone() 35 | } 36 | } 37 | 38 | #[GqlType] 39 | impl Query { 40 | #[allow(unused)] 41 | async fn test_custom_scalar(&self) -> SampleResponse { 42 | SampleResponse { 43 | test: CustomScalar("Sample".to_string()), 44 | } 45 | } 46 | } 47 | let contents = schema_content("./tests/schemas/custom_scalar.graphql"); 48 | 49 | let container = Container::new( 50 | &vec![contents.as_str()], 51 | Query, 52 | EmptyMutation, 53 | EmptySubscription, 54 | Default::default(), 55 | ) 56 | .unwrap(); 57 | 58 | let query_doc = r#"{ test_custom_scalar { test } }"#; 59 | let req = build_test_request(query_doc, None, Default::default()); 60 | let expected_response = r#"{"data":{"test_custom_scalar":{"test":"Custom-Sample"}}}"#; 61 | check_gql_response(req, expected_response, &container).await; 62 | } 63 | -------------------------------------------------------------------------------- /src/types/introspection/schema.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use rusty_gql_macro::GqlType; 3 | 4 | use crate::Schema; 5 | 6 | use super::{directive::__Directive, introspection_type::__Type}; 7 | 8 | pub struct __Schema<'a> { 9 | detail: &'a Schema, 10 | } 11 | 12 | pub fn build_schema_introspection(schema: &Schema) -> __Schema<'_> { 13 | __Schema { detail: schema } 14 | } 15 | 16 | #[allow(non_snake_case)] 17 | #[GqlType(internal)] 18 | impl<'a> __Schema<'a> { 19 | async fn types(&self) -> Vec<__Type<'a>> { 20 | let mut result = Vec::new(); 21 | for def in self.detail.type_definitions.values() { 22 | let ty = __Type::from_type_definition(self.detail, def); 23 | result.push(ty); 24 | } 25 | 26 | result 27 | } 28 | 29 | async fn queryType(&self) -> __Type<'a> { 30 | match self 31 | .detail 32 | .type_definitions 33 | .get(&self.detail.query_type_name) 34 | { 35 | Some(query) => __Type::from_type_definition(self.detail, query), 36 | None => panic!("Query is not defined."), 37 | } 38 | } 39 | 40 | async fn mutationType(&self) -> Option<__Type<'a>> { 41 | self.detail 42 | .type_definitions 43 | .get(&self.detail.mutation_type_name) 44 | .map(|mutation| __Type::from_type_definition(self.detail, mutation)) 45 | } 46 | 47 | async fn subscriptionType(&self) -> Option<__Type<'a>> { 48 | self.detail 49 | .type_definitions 50 | .get(&self.detail.subscription_type_name) 51 | .map(|subscription| __Type::from_type_definition(self.detail, subscription)) 52 | } 53 | 54 | async fn directives(&self) -> Vec<__Directive<'a>> { 55 | let mut result = Vec::new(); 56 | 57 | for dir in self.detail.directives.values() { 58 | let directive = __Directive::new(self.detail, dir); 59 | result.push(directive); 60 | } 61 | result 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/validation/mod.rs: -------------------------------------------------------------------------------- 1 | use graphql_parser::query::Document; 2 | 3 | use crate::{operation::Operation, types::schema::Schema, GqlError, Variables}; 4 | 5 | use self::visitor::{visit, NewVisitor, ValidationContext}; 6 | 7 | mod rules; 8 | mod test_utils; 9 | mod utils; 10 | mod visitor; 11 | 12 | pub fn apply_validation<'a>( 13 | schema: &'a Schema, 14 | query_doc: &'a Document<'a, String>, 15 | variables: Option<&'a Variables>, 16 | operation: &'a Operation<'a>, 17 | operation_name: Option<&'a str>, 18 | ) -> Result<(), Vec> { 19 | let mut ctx = ValidationContext::new(schema, variables, operation); 20 | let mut visitor = NewVisitor 21 | .with(rules::DefaultValueOfCorrectType::default()) 22 | .with(rules::FieldsOnCorrectType::default()) 23 | .with(rules::FragmentsOnCompositeTypes::default()) 24 | .with(rules::KnownArgumentNames::default()) 25 | .with(rules::KnownDirectives::default()) 26 | .with(rules::KnownFragmentName::default()) 27 | .with(rules::KnownTypeNames::default()) 28 | .with(rules::NoFragmentCycles::default()) 29 | .with(rules::NoUndefinedVariables::default()) 30 | // .with(rules::NoUnusedFragment::default()) 31 | .with(rules::NoUnusedVariables::default()) 32 | .with(rules::OverlappingFieldsCanBeMerged::default()) 33 | .with(rules::PossibleFragmentSpreads::default()) 34 | .with(rules::ProvidedNonNullArguments::default()) 35 | .with(rules::ScalarLeafs::default()) 36 | .with(rules::UniqueArgumentNames::default()) 37 | .with(rules::UniqueVariableNames::default()) 38 | .with(rules::VariablesAreInputTypes::default()) 39 | .with(rules::VariablesInAllowedPosition::default()); 40 | // .with(rules::ArgumentsOfCorrectType::default()) 41 | 42 | visit(&mut visitor, &mut ctx, query_doc, operation_name); 43 | 44 | if !ctx.errors.is_empty() { 45 | return Err(ctx.errors.into_iter().map(|v| v.into()).collect()); 46 | } 47 | 48 | Ok(()) 49 | } 50 | -------------------------------------------------------------------------------- /examples/axum/src/starwars.rs: -------------------------------------------------------------------------------- 1 | use rusty_gql::ID; 2 | 3 | use crate::graphql::{Droid, Episode, Human, Review}; 4 | 5 | pub fn luke() -> Human { 6 | Human { 7 | id: ID("1".to_string()), 8 | name: "Luke Skywalker".to_string(), 9 | homePlanet: Some("Tatooine".to_string()), 10 | height: Some(180.0), 11 | mass: Some(70.0), 12 | } 13 | } 14 | 15 | pub fn vader() -> Human { 16 | Human { 17 | id: ID("2".to_string()), 18 | name: "Anakin Skywalker".to_string(), 19 | homePlanet: Some("Tatooine".to_string()), 20 | height: Some(190.0), 21 | mass: Some(80.0), 22 | } 23 | } 24 | 25 | pub fn han() -> Human { 26 | Human { 27 | id: ID("3".to_string()), 28 | name: "Han Solo".to_string(), 29 | homePlanet: None, 30 | height: Some(175.0), 31 | mass: Some(70.0), 32 | } 33 | } 34 | 35 | pub fn leia() -> Human { 36 | Human { 37 | id: ID("4".to_string()), 38 | name: "Leia Organa".to_string(), 39 | homePlanet: None, 40 | height: None, 41 | mass: None, 42 | } 43 | } 44 | 45 | pub fn r2d2() -> Droid { 46 | Droid { 47 | id: ID("5".to_string()), 48 | name: "R2D2".to_string(), 49 | primaryFunction: Some("support jedi".to_string()), 50 | } 51 | } 52 | 53 | pub fn c3po() -> Droid { 54 | Droid { 55 | id: ID("6".to_string()), 56 | name: "C3PO".to_string(), 57 | primaryFunction: Some("communication".to_string()), 58 | } 59 | } 60 | 61 | pub fn all_reviews() -> Vec { 62 | vec![ 63 | Review { 64 | stars: 3, 65 | commentary: None, 66 | episode: Some(Episode::EMPIRE), 67 | }, 68 | Review { 69 | stars: 5, 70 | commentary: Some("Great!".to_string()), 71 | episode: Some(Episode::NEWHOPE), 72 | }, 73 | Review { 74 | stars: 4, 75 | commentary: None, 76 | episode: Some(Episode::JEDI), 77 | }, 78 | ] 79 | } 80 | -------------------------------------------------------------------------------- /src/container.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, ops::Deref, sync::Arc}; 2 | 3 | use crate::{ 4 | error::GqlError, 5 | types::schema::{build_schema, Schema}, 6 | CustomDirective, QueryRoot, SelectionSetResolver, 7 | }; 8 | 9 | pub struct ContainerInner< 10 | Query: SelectionSetResolver, 11 | Mutation: SelectionSetResolver, 12 | Subscription: SelectionSetResolver, 13 | > { 14 | pub query_resolvers: QueryRoot, 15 | pub mutation_resolvers: Mutation, 16 | pub subscription_resolvers: Subscription, 17 | pub schema: Schema, 18 | } 19 | 20 | #[derive(Clone)] 21 | pub struct Container< 22 | Query: SelectionSetResolver, 23 | Mutation: SelectionSetResolver, 24 | Subscription: SelectionSetResolver, 25 | >(Arc>); 26 | 27 | impl Deref for Container 28 | where 29 | Query: SelectionSetResolver + 'static, 30 | Mutation: SelectionSetResolver + 'static, 31 | Subscription: SelectionSetResolver + 'static, 32 | { 33 | type Target = ContainerInner; 34 | 35 | fn deref(&self) -> &Self::Target { 36 | &self.0 37 | } 38 | } 39 | 40 | impl Container 41 | where 42 | Query: SelectionSetResolver + 'static, 43 | Mutation: SelectionSetResolver + 'static, 44 | Subscription: SelectionSetResolver + 'static, 45 | { 46 | pub fn new( 47 | schema_doc: &[&str], 48 | query: Query, 49 | mutation: Mutation, 50 | subscription: Subscription, 51 | custom_directives: HashMap<&'static str, Box>, 52 | ) -> Result { 53 | let schema = build_schema(schema_doc, custom_directives)?; 54 | Ok(Container(Arc::new(ContainerInner { 55 | query_resolvers: QueryRoot { query }, 56 | mutation_resolvers: mutation, 57 | subscription_resolvers: subscription, 58 | schema, 59 | }))) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - release 6 | paths: 7 | - '**/Cargo.toml' 8 | - '.github/workflows/release.yml' 9 | 10 | jobs: 11 | publish: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | max-parallel: 1 16 | matrix: 17 | package: 18 | - name: rusty-gql-macro 19 | registryName: rusty-gql-macro 20 | path: macro 21 | - name: rusty-gql 22 | registryName: rusty-gql 23 | path: . 24 | - name: rusty-gql-cli 25 | registryName: rusty-gql-cli 26 | path: cli 27 | - name: rusty-gql-axum 28 | registryName: rusty-gql-axum 29 | path: frameworks/axum 30 | steps: 31 | - uses: actions-rs/toolchain@v1 32 | with: 33 | toolchain: stable 34 | override: true 35 | - name: Checkout 36 | uses: actions/checkout@v2 37 | - name: get version 38 | working-directory: ${{ matrix.package.path }} 39 | run: echo PACKAGE_VERSION=$(sed -nE 's/^\s*version = "(.*?)"/\1/p' Cargo.toml) >> $GITHUB_ENV 40 | - name: check published version 41 | run: echo PUBLISHED_VERSION=$(cargo search ${{ matrix.package.registryName }} --limit 1 | sed -nE 's/^[^"]*"//; s/".*//1p' -) >> $GITHUB_ENV 42 | - name: cargo login 43 | if: env.PACKAGE_VERSION != env.PUBLISHED_VERSION 44 | run: cargo login ${{ secrets.CRATES_TOKEN }} 45 | - name: cargo package 46 | if: env.PACKAGE_VERSION != env.PUBLISHED_VERSION 47 | working-directory: ${{ matrix.package.path }} 48 | run: | 49 | cargo package 50 | echo "We will publish:" $PACKAGE_VERSION 51 | echo "This is current latest:" $PUBLISHED_VERSION 52 | - name: Publish ${{ matrix.package.name }} 53 | if: env.PACKAGE_VERSION != env.PUBLISHED_VERSION 54 | working-directory: ${{ matrix.package.path }} 55 | run: | 56 | echo "# Cargo Publish" 57 | cargo publish --no-verify 58 | -------------------------------------------------------------------------------- /src/resolver/string.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | CollectFields, Context, FieldResolver, GqlValue, ResolverResult, SelectionSetContext, 3 | SelectionSetResolver, 4 | }; 5 | 6 | #[async_trait::async_trait] 7 | impl FieldResolver for str { 8 | async fn resolve_field(&self, _ctx: &Context<'_>) -> ResolverResult> { 9 | Ok(Some(GqlValue::String(self.to_string()))) 10 | } 11 | fn type_name() -> String { 12 | "String".to_string() 13 | } 14 | } 15 | 16 | impl CollectFields for str {} 17 | 18 | #[async_trait::async_trait] 19 | impl SelectionSetResolver for str { 20 | async fn resolve_selection_set( 21 | &self, 22 | _ctx: &SelectionSetContext<'_>, 23 | ) -> ResolverResult { 24 | Ok(GqlValue::String(self.to_string())) 25 | } 26 | } 27 | 28 | #[async_trait::async_trait] 29 | impl FieldResolver for &str { 30 | async fn resolve_field(&self, _ctx: &Context<'_>) -> ResolverResult> { 31 | Ok(Some(GqlValue::String(self.to_string()))) 32 | } 33 | fn type_name() -> String { 34 | "String".to_string() 35 | } 36 | } 37 | 38 | impl CollectFields for &str {} 39 | 40 | #[async_trait::async_trait] 41 | impl SelectionSetResolver for &str { 42 | async fn resolve_selection_set( 43 | &self, 44 | _ctx: &SelectionSetContext<'_>, 45 | ) -> ResolverResult { 46 | Ok(GqlValue::String(self.to_string())) 47 | } 48 | } 49 | 50 | #[async_trait::async_trait] 51 | impl FieldResolver for String { 52 | async fn resolve_field(&self, _ctx: &Context<'_>) -> ResolverResult> { 53 | Ok(Some(GqlValue::String(self.clone()))) 54 | } 55 | fn type_name() -> String { 56 | "String".to_string() 57 | } 58 | } 59 | 60 | impl CollectFields for String {} 61 | 62 | #[async_trait::async_trait] 63 | impl SelectionSetResolver for String { 64 | async fn resolve_selection_set( 65 | &self, 66 | _ctx: &SelectionSetContext<'_>, 67 | ) -> ResolverResult { 68 | Ok(GqlValue::String(self.clone())) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/response.rs: -------------------------------------------------------------------------------- 1 | use http::HeaderMap; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::{error::GqlError, GqlValue}; 5 | 6 | #[derive(Serialize, Deserialize, Default, Debug)] 7 | pub struct Response { 8 | #[serde(default)] 9 | pub data: GqlValue, 10 | #[serde(skip_serializing_if = "Vec::is_empty", default)] 11 | pub errors: Vec, 12 | #[serde(skip)] 13 | pub http_headers: HeaderMap, 14 | } 15 | 16 | impl Response { 17 | pub fn new(data: impl Into) -> Self { 18 | Self { 19 | data: data.into(), 20 | errors: vec![], 21 | http_headers: Default::default(), 22 | } 23 | } 24 | 25 | pub fn from_errors(errors: Vec) -> Self { 26 | Self { 27 | errors, 28 | data: Default::default(), 29 | http_headers: Default::default(), 30 | } 31 | } 32 | 33 | pub fn from_data_and_errors(data: impl Into, errors: Vec) -> Self { 34 | Self { 35 | data: data.into(), 36 | errors, 37 | http_headers: Default::default(), 38 | } 39 | } 40 | 41 | pub fn is_ok(&self) -> bool { 42 | self.errors.is_empty() 43 | } 44 | 45 | pub fn is_error(&self) -> bool { 46 | !self.is_ok() 47 | } 48 | } 49 | 50 | #[cfg(test)] 51 | mod tests { 52 | use std::collections::BTreeMap; 53 | 54 | use serde_json::Number; 55 | 56 | use crate::{GqlValue, Response}; 57 | 58 | #[test] 59 | fn test_json_serialize() { 60 | let boolean = Response::new(GqlValue::Boolean(true)); 61 | assert_eq!(serde_json::to_string(&boolean).unwrap(), r#"{"data":true}"#); 62 | 63 | let map = BTreeMap::from([ 64 | ("a".to_string(), GqlValue::Number(Number::from(1))), 65 | ("b".to_string(), GqlValue::Number(Number::from(2))), 66 | ]); 67 | let obj = Response::new(GqlValue::Object(map)); 68 | assert_eq!( 69 | serde_json::to_string(&obj).unwrap(), 70 | r#"{"data":{"a":1,"b":2}}"# 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/query_root.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | error::GqlError, 3 | types::{__Type, build_schema_introspection}, 4 | CollectFields, Context, FieldResolver, GqlValue, ResolverResult, SelectionSetResolver, 5 | }; 6 | 7 | pub struct QueryRoot { 8 | pub query: T, 9 | } 10 | 11 | #[async_trait::async_trait] 12 | impl FieldResolver for QueryRoot { 13 | async fn resolve_field(&self, ctx: &Context<'_>) -> ResolverResult> { 14 | if ctx.item.name == "__schema" { 15 | let ctx_selection_set = ctx.with_selection_set(&ctx.item.selection_set); 16 | let schema_intro = build_schema_introspection(ctx.schema); 17 | return schema_intro 18 | .resolve_selection_set(&ctx_selection_set) 19 | .await 20 | .map(Some); 21 | } else if ctx.item.name == "__type" { 22 | let type_name = ctx.get_arg_value::("name")?; 23 | let ctx_selection_set = ctx.with_selection_set(&ctx.item.selection_set); 24 | let ty = ctx_selection_set 25 | .schema 26 | .type_definitions 27 | .get(&type_name) 28 | .map(|ty| __Type::from_type_definition(ctx_selection_set.schema, ty)); 29 | match ty { 30 | Some(intro_ty) => intro_ty 31 | .resolve_selection_set(&ctx_selection_set) 32 | .await 33 | .map(Some), 34 | None => Err(GqlError::new(format!("{} is not defined", type_name), None)), 35 | } 36 | } else { 37 | self.query.resolve_field(ctx).await 38 | } 39 | } 40 | fn type_name() -> String { 41 | "Query".to_string() 42 | } 43 | } 44 | 45 | impl CollectFields for QueryRoot {} 46 | 47 | #[async_trait::async_trait] 48 | impl SelectionSetResolver for QueryRoot { 49 | async fn resolve_selection_set( 50 | &self, 51 | ctx: &crate::SelectionSetContext<'_>, 52 | ) -> ResolverResult { 53 | self.query.resolve_selection_set(ctx).await 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /docs/src/error_handling.md: -------------------------------------------------------------------------------- 1 | # Error Handling 2 | 3 | If errors occur while GraphQL operation, `errors` field will be included in the response. 4 | 5 | Add errors by using `add_error` of `Context`. 6 | 7 | A error is defined by `GqlError` struct. 8 | 9 | ```rust 10 | pub async fn todos(ctx: &Context<'_>, first: Option) -> Vec { 11 | let all_todos = vec![ 12 | Todo { 13 | title: "Programming".to_string(), 14 | content: Some("Learn Rust".to_string()), 15 | done: false, 16 | }, 17 | Todo { 18 | title: "Shopping".to_string(), 19 | content: None, 20 | done: true, 21 | }, 22 | ]; 23 | match first { 24 | Some(first) => { 25 | if first > 30 { 26 | // add error 27 | ctx.add_error(&GqlError::new("Up to 30 items at one time.", Some(ctx.item.position))); 28 | all_todos 29 | } else { 30 | all_todos.into_iter().take(first as usize).collect(), 31 | } 32 | } 33 | None => all_todos, 34 | } 35 | } 36 | ``` 37 | 38 | When we want to add a meta info, use `extensions`. 39 | 40 | ```rust 41 | ctx.add_error( 42 | &GqlError::new("Error happens", Some(ctx.item.position)).set_extentions( 43 | GqlTypedError { 44 | error_type: GqlErrorType::Internal, 45 | error_detail: Some("Internal Error".to_string()), 46 | origin: None, 47 | debug_info: None, 48 | debug_uri: None, 49 | }, 50 | ), 51 | ); 52 | ``` 53 | 54 | The GraphQL definition of rusty-gql error is as follows. 55 | Also see [GraphQL spec](https://spec.graphql.org/June2018/#sec-Errors). 56 | 57 | ```graphql 58 | type GqlError { 59 | message: String! 60 | locations: [Location!]! 61 | path: [String!]! 62 | extensions: GqlTypedError 63 | } 64 | 65 | type GqlTypedError { 66 | errorType: GqlErrorType! 67 | errorDetail: String 68 | origin: String 69 | debugInfo: DebugInfo 70 | debugUri: String 71 | } 72 | 73 | enum GqlErrorType { 74 | BadRequest 75 | FailedPreCondition 76 | Internal 77 | NotFound 78 | PermissionDenied 79 | Unauthenticated 80 | Unavailable 81 | Unknown 82 | } 83 | ``` 84 | -------------------------------------------------------------------------------- /src/types/enum_type.rs: -------------------------------------------------------------------------------- 1 | use super::directive::GqlDirective; 2 | use graphql_parser::{ 3 | schema::{EnumType as ParserEnumType, EnumValue}, 4 | Pos, 5 | }; 6 | 7 | #[derive(Debug, Clone)] 8 | pub struct EnumType { 9 | pub name: String, 10 | pub description: Option, 11 | pub position: Pos, 12 | pub directives: Vec, 13 | pub values: Vec, 14 | } 15 | 16 | impl<'a> From> for EnumType { 17 | fn from(gql_enum: ParserEnumType<'a, String>) -> Self { 18 | let directives = GqlDirective::from_vec_directive(gql_enum.directives); 19 | 20 | let values = gql_enum 21 | .values 22 | .into_iter() 23 | .map(EnumTypeValue::from) 24 | .collect(); 25 | 26 | EnumType { 27 | name: gql_enum.name, 28 | description: gql_enum.description, 29 | position: gql_enum.position, 30 | directives, 31 | values, 32 | } 33 | } 34 | } 35 | 36 | impl EnumType { 37 | pub fn contains(&self, name: &str) -> bool { 38 | self.values 39 | .iter() 40 | .map(|v| v.name.clone()) 41 | .any(|x| x == *name) 42 | } 43 | } 44 | 45 | #[derive(Debug, Clone)] 46 | pub struct EnumTypeValue { 47 | pub name: String, 48 | pub description: Option, 49 | pub position: Pos, 50 | pub directives: Vec, 51 | } 52 | 53 | impl<'a> From> for EnumTypeValue { 54 | fn from(enum_value: EnumValue<'a, String>) -> Self { 55 | let directives = enum_value 56 | .directives 57 | .into_iter() 58 | .map(GqlDirective::from) 59 | .collect(); 60 | 61 | EnumTypeValue { 62 | name: enum_value.name, 63 | description: enum_value.description, 64 | position: enum_value.position, 65 | directives, 66 | } 67 | } 68 | } 69 | 70 | impl EnumTypeValue { 71 | pub fn is_deprecated(&self) -> bool { 72 | for dir in &self.directives { 73 | if dir.name == "deprecated" { 74 | return true; 75 | } 76 | continue; 77 | } 78 | false 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/validation/rules/arguments_of_correct_type.rs: -------------------------------------------------------------------------------- 1 | use graphql_parser::schema::Value; 2 | 3 | use crate::validation::visitor::{ValidationContext, Visitor}; 4 | 5 | #[derive(Default)] 6 | pub struct ArgumentsOfCorrectType<'a> { 7 | pub current_args: Option<&'a Vec<(String, Value<'a, String>)>>, 8 | } 9 | 10 | impl<'a> Visitor<'a> for ArgumentsOfCorrectType<'a> { 11 | fn enter_directive( 12 | &mut self, 13 | _ctx: &mut ValidationContext, 14 | directive: &'a graphql_parser::schema::Directive<'a, String>, 15 | ) { 16 | self.current_args = Some(&directive.arguments); 17 | } 18 | 19 | fn exit_directive( 20 | &mut self, 21 | _ctx: &mut ValidationContext, 22 | _directive: &'a graphql_parser::schema::Directive<'a, String>, 23 | ) { 24 | self.current_args = None; 25 | } 26 | 27 | fn enter_field( 28 | &mut self, 29 | _ctx: &mut ValidationContext, 30 | field: &'a graphql_parser::query::Field<'a, String>, 31 | ) { 32 | self.current_args = Some(&field.arguments); 33 | } 34 | 35 | fn exit_field( 36 | &mut self, 37 | _ctx: &mut ValidationContext, 38 | _field: &'a graphql_parser::query::Field<'a, String>, 39 | ) { 40 | self.current_args = None; 41 | } 42 | 43 | fn enter_argument( 44 | &mut self, 45 | _ctx: &mut ValidationContext, 46 | arg_name: &'a str, 47 | _arg_value: &'a Value<'a, String>, 48 | ) { 49 | match &self.current_args { 50 | Some(args) => { 51 | let target_arg = args.iter().find(|arg| arg.0 == arg_name); 52 | if target_arg.is_none() {} 53 | 54 | // if let Some(vars) = &ctx.variables { 55 | // if let Some(def) = vars.0.get(arg_name) { 56 | // if let Some(err_msg) = 57 | // check_valid_input_value(&ctx.schema, &def.var_type, arg_value) 58 | // { 59 | // ctx.add_error(format!("Invalid value for argument {}", err_msg), vec![]) 60 | // } 61 | // } 62 | // } 63 | } 64 | None => {} 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/input/object.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{BTreeMap, HashMap}; 2 | 3 | use crate::GqlValue; 4 | 5 | use super::GqlInputType; 6 | 7 | impl GqlInputType for BTreeMap { 8 | fn from_gql_value(value: Option) -> Result { 9 | match value { 10 | Some(value) => match value { 11 | GqlValue::Object(v) => { 12 | let mut result = BTreeMap::new(); 13 | for (key, value) in v { 14 | result.insert(key, T::from_gql_value(Some(value))?); 15 | } 16 | Ok(result) 17 | } 18 | invalid_value => Err(format!( 19 | "Expected type: object, but found: {}", 20 | invalid_value.to_string() 21 | )), 22 | }, 23 | None => Err("Expected type: object, but not found".to_string()), 24 | } 25 | } 26 | 27 | fn to_gql_value(&self) -> GqlValue { 28 | let mut result = BTreeMap::new(); 29 | for (key, value) in self { 30 | result.insert(key.clone(), T::to_gql_value(value)); 31 | } 32 | GqlValue::Object(result) 33 | } 34 | } 35 | 36 | impl GqlInputType for HashMap { 37 | fn from_gql_value(value: Option) -> Result { 38 | match value { 39 | Some(value) => match value { 40 | GqlValue::Object(v) => { 41 | let mut result = HashMap::new(); 42 | for (key, value) in v { 43 | result.insert(key, T::from_gql_value(Some(value))?); 44 | } 45 | Ok(result) 46 | } 47 | invalid_value => Err(format!( 48 | "Expected type: object, but found: {}", 49 | invalid_value.to_string() 50 | )), 51 | }, 52 | None => Err("Expected type: object, but not found".to_string()), 53 | } 54 | } 55 | 56 | fn to_gql_value(&self) -> GqlValue { 57 | let mut result = BTreeMap::new(); 58 | for (key, value) in self { 59 | result.insert(key.clone(), T::to_gql_value(value)); 60 | } 61 | GqlValue::Object(result) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/playground_html.rs: -------------------------------------------------------------------------------- 1 | pub fn playground_html(endpoint: &str, subscription_endpoint: Option<&str>) -> String { 2 | r#" 3 | 4 | 5 | rusty gql 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 17 | 21 | 25 | 26 | 48 | 49 | 50 | "# 51 | .replace("GRAPHQL_URL", endpoint) 52 | .replace( 53 | "GRAPHQL_SUBSCRIPTION_URL", 54 | &match subscription_endpoint { 55 | Some(url) => format!("'{}'", url), 56 | None => "null".to_string(), 57 | }, 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /src/types/introspection/introspection_sdl.rs: -------------------------------------------------------------------------------- 1 | pub fn introspection_sdl() -> &'static str { 2 | r#" 3 | type __Schema { 4 | types: [__Type!]! 5 | queryType: __Type! 6 | mutationType: __Type 7 | subscriptionType: __Type 8 | directives: [__Directive!]! 9 | } 10 | 11 | type __Type { 12 | kind: __TypeKind! 13 | name: String 14 | description: String 15 | 16 | # OBJECT and INTERFACE only 17 | fields(includeDeprecated: Boolean = false): [__Field!] 18 | 19 | # OBJECT only 20 | interfaces: [__Type!] 21 | 22 | # INTERFACE and UNION only 23 | possibleTypes: [__Type!] 24 | 25 | # ENUM only 26 | enumValues(includeDeprecated: Boolean = false): [__EnumValue!] 27 | 28 | # INPUT_OBJECT only 29 | inputFields: [__InputValue!] 30 | 31 | # NON_NULL and LIST only 32 | ofType: __Type 33 | } 34 | 35 | type __Field { 36 | name: String! 37 | description: String 38 | args: [__InputValue!]! 39 | type: __Type! 40 | isDeprecated: Boolean! 41 | deprecationReason: String 42 | } 43 | 44 | type __InputValue { 45 | name: String! 46 | description: String 47 | type: __Type! 48 | defaultValue: String 49 | } 50 | 51 | type __EnumValue { 52 | name: String! 53 | description: String 54 | isDeprecated: Boolean! 55 | deprecationReason: String 56 | } 57 | 58 | enum __TypeKind { 59 | SCALAR 60 | OBJECT 61 | INTERFACE 62 | UNION 63 | ENUM 64 | INPUT_OBJECT 65 | LIST 66 | NON_NULL 67 | } 68 | 69 | type __Directive { 70 | name: String! 71 | description: String 72 | locations: [__DirectiveLocation!]! 73 | args: [__InputValue!]! 74 | } 75 | 76 | enum __DirectiveLocation { 77 | QUERY 78 | MUTATION 79 | SUBSCRIPTION 80 | FIELD 81 | FRAGMENT_DEFINITION 82 | FRAGMENT_SPREAD 83 | INLINE_FRAGMENT 84 | SCHEMA 85 | SCALAR 86 | OBJECT 87 | FIELD_DEFINITION 88 | ARGUMENT_DEFINITION 89 | INTERFACE 90 | UNION 91 | ENUM 92 | ENUM_VALUE 93 | INPUT_OBJECT 94 | INPUT_FIELD_DEFINITION 95 | } 96 | "# 97 | } 98 | -------------------------------------------------------------------------------- /docs/src/getting_started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Install rusty-gql-cli 4 | 5 | ``` 6 | cargo install rusty-gql-cli 7 | ``` 8 | 9 | ## Run new command 10 | 11 | ``` 12 | rusty-gql new gql-example 13 | cd gql-example 14 | ``` 15 | 16 | ## Start the GraphQL Server 17 | 18 | ``` 19 | cargo run 20 | ``` 21 | 22 | ## Creating a GraphQL Schema 23 | 24 | rusty-gql is designed for schema first development. 25 | It reads any graphql files under `schema/**`. 26 | 27 | `schema/schema.graphql` 28 | 29 | ```graphql 30 | type Query { 31 | todos(first: Int): [Todo!]! 32 | } 33 | 34 | type Todo { 35 | title: String! 36 | content: String 37 | done: Boolean! 38 | } 39 | ``` 40 | 41 | ## Implement Resolvers 42 | 43 | Let's edit `src/graphql/query/todos.rs`. 44 | 45 | ```rust 46 | pub async fn todos(ctx: &Context<'_>, first: Option) -> Vec { 47 | let all_todos = vec![ 48 | Todo { 49 | title: "Programming".to_string(), 50 | content: Some("Learn Rust".to_string()), 51 | done: false, 52 | }, 53 | Todo { 54 | title: "Shopping".to_string(), 55 | content: None, 56 | done: true, 57 | }, 58 | ]; 59 | match first { 60 | Some(first) => all_todos.into_iter().take(first as usize).collect(), 61 | None => all_todos, 62 | } 63 | } 64 | ``` 65 | 66 | ## Generate Rust code 67 | 68 | Edit schema.graphql. 69 | 70 | ```graphql 71 | type Query { 72 | todos(first: Int): [Todo!]! 73 | # added 74 | todo(id: ID!): Todo 75 | } 76 | 77 | type Todo { 78 | title: String! 79 | description: String 80 | done: Boolean! 81 | } 82 | ``` 83 | 84 | rusty-gql generates rust code from graphql schema files. 85 | 86 | ``` 87 | rusty-gql generate // or rusty-gql g 88 | ``` 89 | 90 | ### Directory Structure 91 | 92 | ``` 93 | src 94 | ┣ graphql 95 | ┃ ┣ directive 96 | ┃ ┃ ┗ mod.rs 97 | ┃ ┣ input 98 | ┃ ┃ ┗ mod.rs 99 | ┃ ┣ mutation 100 | ┃ ┃ ┗ mod.rs 101 | ┃ ┣ query 102 | ┃ ┃ ┣ mod.rs 103 | ┃ ┃ ┣ todo.rs 104 | ┃ ┃ ┗ todos.rs 105 | ┃ ┣ resolver 106 | ┃ ┃ ┣ mod.rs 107 | ┃ ┃ ┗ todo.rs 108 | ┃ ┣ scalar 109 | ┃ ┃ ┗ mod.rs 110 | ┃ ┗ mod.rs 111 | ┗ main.rs 112 | ``` 113 | 114 | ## GraphQL Playground 115 | 116 | rusty-gql supports GraphiQL playground. 117 | Open a browser to http://localhost:3000/graphiql. 118 | -------------------------------------------------------------------------------- /frameworks/axum/src/request.rs: -------------------------------------------------------------------------------- 1 | use axum::extract::{BodyStream, FromRequest}; 2 | use axum::http::{Method, StatusCode}; 3 | use axum::response::{IntoResponse, Response}; 4 | use axum::{body, BoxError}; 5 | use bytes::Bytes; 6 | use futures_util::TryStreamExt; 7 | use rusty_gql::{receive_http_request, HttpRequestError}; 8 | use tokio_util::compat::TokioAsyncReadCompatExt; 9 | 10 | pub struct GqlRequest(pub rusty_gql::Request); 11 | 12 | pub struct GqlRejection(pub HttpRequestError); 13 | 14 | impl IntoResponse for GqlRejection { 15 | fn into_response(self) -> Response { 16 | let body = body::boxed(body::Full::from(format!("{:?}", self.0))); 17 | Response::builder() 18 | .status(StatusCode::BAD_REQUEST) 19 | .body(body) 20 | .unwrap() 21 | } 22 | } 23 | 24 | impl From for GqlRejection { 25 | fn from(error: HttpRequestError) -> Self { 26 | GqlRejection(error) 27 | } 28 | } 29 | 30 | #[async_trait::async_trait] 31 | impl FromRequest for GqlRequest 32 | where 33 | B: http_body::Body + Unpin + Send + Sync + 'static, 34 | B::Data: Into, 35 | B::Error: Into, 36 | { 37 | type Rejection = GqlRejection; 38 | async fn from_request( 39 | req: &mut axum::extract::RequestParts, 40 | ) -> Result { 41 | if let (&Method::GET, uri) = (req.method(), req.uri()) { 42 | let res = serde_urlencoded::from_str(uri.query().unwrap_or_default()).map_err(|err| { 43 | HttpRequestError::Io(std::io::Error::new( 44 | std::io::ErrorKind::Other, 45 | format!("failed to parse graphql requst from query params: {}", err), 46 | )) 47 | }); 48 | Ok(Self(res?)) 49 | } else { 50 | let body_stream = BodyStream::from_request(req) 51 | .await 52 | .map_err(|err| { 53 | HttpRequestError::Io(std::io::Error::new( 54 | std::io::ErrorKind::Other, 55 | err.to_string(), 56 | )) 57 | })? 58 | .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err.to_string())); 59 | let body_reader = tokio_util::io::StreamReader::new(body_stream).compat(); 60 | Ok(Self(receive_http_request(body_reader).await?)) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/interface.rs: -------------------------------------------------------------------------------- 1 | use rusty_gql::*; 2 | 3 | #[tokio::test] 4 | pub async fn test_interface() { 5 | struct Query; 6 | 7 | struct Dog { 8 | name: String, 9 | woofs: bool, 10 | } 11 | 12 | #[GqlType] 13 | impl Dog { 14 | async fn name(&self) -> String { 15 | self.name.clone() 16 | } 17 | async fn woofs(&self) -> bool { 18 | self.woofs 19 | } 20 | } 21 | 22 | struct Cat { 23 | name: String, 24 | meow: bool, 25 | } 26 | 27 | #[GqlType] 28 | impl Cat { 29 | async fn name(&self) -> String { 30 | self.name.clone() 31 | } 32 | async fn meow(&self) -> bool { 33 | self.meow 34 | } 35 | } 36 | 37 | #[derive(GqlInterface)] 38 | enum Animal { 39 | Dog(Dog), 40 | Cat(Cat), 41 | } 42 | 43 | #[GqlType(interface)] 44 | impl Animal { 45 | async fn name(&self) -> String { 46 | match self { 47 | Animal::Dog(obj) => obj.name.clone(), 48 | Animal::Cat(obj) => obj.name.clone(), 49 | } 50 | } 51 | } 52 | 53 | #[GqlType] 54 | impl Query { 55 | async fn search_animal(&self, query: String) -> Option { 56 | if query.as_str() == "dog" { 57 | return Some(Animal::Dog(Dog { 58 | name: "Pochi".to_string(), 59 | woofs: true, 60 | })); 61 | } else if query.as_str() == "cat" { 62 | return Some(Animal::Cat(Cat { 63 | name: "Tama".to_string(), 64 | meow: true, 65 | })); 66 | } 67 | None 68 | } 69 | } 70 | let contents = schema_content("./tests/schemas/interface.graphql"); 71 | 72 | let container = Container::new( 73 | &vec![contents.as_str()], 74 | Query, 75 | EmptyMutation, 76 | EmptySubscription, 77 | Default::default(), 78 | ) 79 | .unwrap(); 80 | 81 | let query_doc = r#"{ search_animal(query: "dog") { 82 | name 83 | ... on Dog { 84 | woofs 85 | } 86 | }}"#; 87 | let req = build_test_request(query_doc, None, Default::default()); 88 | let expected_response = r#"{"data":{"search_animal":{"name":"Pochi","woofs":true}}}"#; 89 | check_gql_response(req, expected_response, &container).await; 90 | } 91 | -------------------------------------------------------------------------------- /macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod enum_type; 2 | mod input_object; 3 | mod interface; 4 | mod scalar; 5 | mod ty; 6 | mod union; 7 | mod utils; 8 | 9 | use enum_type::generate_enum; 10 | use input_object::generate_input_object; 11 | use interface::generate_interface; 12 | use proc_macro::{self, TokenStream}; 13 | use scalar::generate_scalar; 14 | use syn::{parse_macro_input, AttributeArgs, DeriveInput, ItemImpl}; 15 | use union::generate_union; 16 | 17 | use crate::ty::generate_type; 18 | 19 | #[proc_macro_attribute] 20 | #[allow(non_snake_case)] 21 | pub fn GqlType(args: TokenStream, input: TokenStream) -> TokenStream { 22 | let mut item_impl = parse_macro_input!(input as ItemImpl); 23 | let args = parse_macro_input!(args as AttributeArgs); 24 | 25 | match generate_type(&mut item_impl, &args[..]) { 26 | Ok(generated) => generated, 27 | Err(err) => err.to_compile_error().into(), 28 | } 29 | } 30 | 31 | #[proc_macro_derive(GqlScalar)] 32 | pub fn scalar_derive(input: TokenStream) -> TokenStream { 33 | let input = &parse_macro_input!(input as DeriveInput); 34 | match generate_scalar(input) { 35 | Ok(generated) => generated, 36 | Err(err) => err.to_compile_error().into(), 37 | } 38 | } 39 | 40 | #[proc_macro_derive(GqlUnion)] 41 | pub fn union_derive(input: TokenStream) -> TokenStream { 42 | let input = &parse_macro_input!(input as DeriveInput); 43 | match generate_union(input) { 44 | Ok(generated) => generated, 45 | Err(err) => err.to_compile_error().into(), 46 | } 47 | } 48 | 49 | #[proc_macro_derive(GqlInterface)] 50 | pub fn interface_derive(input: TokenStream) -> TokenStream { 51 | let input = &parse_macro_input!(input as DeriveInput); 52 | match generate_interface(input) { 53 | Ok(generated) => generated, 54 | Err(err) => err.to_compile_error().into(), 55 | } 56 | } 57 | 58 | #[proc_macro_derive(GqlEnum)] 59 | pub fn enum_derive(input: TokenStream) -> TokenStream { 60 | let input = &parse_macro_input!(input as DeriveInput); 61 | match generate_enum(input) { 62 | Ok(generated) => generated, 63 | Err(err) => err.to_compile_error().into(), 64 | } 65 | } 66 | 67 | #[proc_macro_derive(GqlInputObject)] 68 | pub fn input_object_derive(input: TokenStream) -> TokenStream { 69 | let input = &parse_macro_input!(input as DeriveInput); 70 | match generate_input_object(input) { 71 | Ok(generated) => generated, 72 | Err(err) => err.to_compile_error().into(), 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/types/scalar.rs: -------------------------------------------------------------------------------- 1 | use graphql_parser::{ 2 | schema::{ScalarType as ParserScalarType, Value}, 3 | Pos, 4 | }; 5 | 6 | use super::directive::GqlDirective; 7 | 8 | #[derive(Debug, Clone)] 9 | pub struct ScalarType { 10 | pub name: String, 11 | pub description: Option, 12 | pub position: Pos, 13 | pub directives: Vec, 14 | } 15 | 16 | impl<'a> From> for ScalarType { 17 | fn from(scalar_type: ParserScalarType<'a, String>) -> Self { 18 | let directives = GqlDirective::from_vec_directive(scalar_type.directives); 19 | ScalarType { 20 | name: scalar_type.name, 21 | description: scalar_type.description, 22 | position: scalar_type.position, 23 | directives, 24 | } 25 | } 26 | } 27 | 28 | impl ScalarType { 29 | pub fn is_valid_value(&self, value: &Value<'_, String>) -> bool { 30 | match value { 31 | Value::Variable(_) => false, 32 | Value::Int(_) => self.name == *"Int", 33 | Value::Float(_) => self.name == *"Float", 34 | Value::String(_) => self.name == *"String", 35 | Value::Boolean(_) => self.name == *"Boolean", 36 | Value::Null => true, 37 | Value::Enum(_) => false, 38 | Value::List(_) => false, 39 | Value::Object(_) => false, 40 | } 41 | } 42 | 43 | pub fn string_scalar() -> Self { 44 | ScalarType { 45 | name: "String".to_string(), 46 | description: None, 47 | position: Pos::default(), 48 | directives: vec![], 49 | } 50 | } 51 | 52 | pub fn int_scalar() -> Self { 53 | ScalarType { 54 | name: "Int".to_string(), 55 | description: None, 56 | position: Pos::default(), 57 | directives: vec![], 58 | } 59 | } 60 | 61 | pub fn float_scalar() -> Self { 62 | ScalarType { 63 | name: "Float".to_string(), 64 | description: None, 65 | position: Pos::default(), 66 | directives: vec![], 67 | } 68 | } 69 | 70 | pub fn boolean_scalar() -> Self { 71 | ScalarType { 72 | name: "Boolean".to_string(), 73 | description: None, 74 | position: Pos::default(), 75 | directives: vec![], 76 | } 77 | } 78 | 79 | pub fn id_scalar() -> Self { 80 | ScalarType { 81 | name: "ID".to_string(), 82 | description: None, 83 | position: Pos::default(), 84 | directives: vec![], 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/executor.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | container::Container, context::build_context, error::GqlError, operation::build_operation, 3 | request::Request, resolve_selection_parallelly, resolve_selection_serially, response::Response, 4 | validation::apply_validation, OperationType, SelectionSetResolver, 5 | }; 6 | 7 | pub async fn execute< 8 | Query: SelectionSetResolver + 'static, 9 | Mutation: SelectionSetResolver + 'static, 10 | Subscription: SelectionSetResolver + 'static, 11 | >( 12 | container: &Container, 13 | request: Request, 14 | ) -> Response { 15 | let query_doc = match graphql_parser::parse_query::(&request.query) { 16 | Ok(doc) => doc, 17 | Err(_) => { 18 | let err = GqlError::new("failed to parse query", None); 19 | return Response::from_errors(vec![err]); 20 | } 21 | }; 22 | let operation = build_operation( 23 | &query_doc, 24 | request.operation_name.clone(), 25 | request.variables.clone(), 26 | ); 27 | 28 | let operation = match operation { 29 | Ok(op) => op, 30 | Err(error) => return Response::from_errors(vec![error]), 31 | }; 32 | 33 | if let Err(errors) = apply_validation( 34 | &container.schema, 35 | &query_doc, 36 | Some(&request.variables), 37 | &operation, 38 | request.operation_name.as_deref(), 39 | ) { 40 | return Response::from_errors(errors); 41 | } 42 | 43 | let ctx = build_context(&container.schema, &operation); 44 | 45 | let result = match operation.operation_type { 46 | OperationType::Query => { 47 | resolve_selection_parallelly(&ctx, &container.query_resolvers).await 48 | } 49 | OperationType::Mutation => { 50 | resolve_selection_serially(&ctx, &container.mutation_resolvers).await 51 | } 52 | OperationType::Subscription => { 53 | let error = GqlError::new("Subscription cannot execute from this path", None); 54 | return Response::from_errors(vec![error]); 55 | } 56 | }; 57 | 58 | match result { 59 | Ok(value) => { 60 | if !ctx.operation.errors.lock().unwrap().is_empty() { 61 | Response::from_data_and_errors(value, ctx.operation.errors.lock().unwrap().clone()) 62 | } else { 63 | Response::new(value) 64 | } 65 | } 66 | Err(error) => { 67 | let mut errors = vec![error]; 68 | errors.extend(ctx.operation.errors.lock().unwrap().clone()); 69 | Response::from_errors(errors) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /examples/axum/src/graphql/resolver/human.rs: -------------------------------------------------------------------------------- 1 | #![allow(warnings, unused)] 2 | use crate::{ 3 | graphql::*, 4 | starwars::{han, luke}, 5 | }; 6 | use rusty_gql::*; 7 | 8 | #[derive(Debug, Clone)] 9 | pub struct Human { 10 | pub id: ID, 11 | pub name: String, 12 | pub homePlanet: Option, 13 | pub height: Option, 14 | pub mass: Option, 15 | } 16 | 17 | #[GqlType] 18 | impl Human { 19 | pub async fn id(&self, ctx: &Context<'_>) -> ID { 20 | self.id.clone() 21 | } 22 | 23 | pub async fn name(&self, ctx: &Context<'_>) -> String { 24 | self.name.clone() 25 | } 26 | 27 | pub async fn homePlanet(&self, ctx: &Context<'_>) -> Option { 28 | self.homePlanet.clone() 29 | } 30 | 31 | pub async fn height(&self, ctx: &Context<'_>, unit: Option) -> Option { 32 | self.height 33 | } 34 | 35 | pub async fn mass(&self, ctx: &Context<'_>) -> Option { 36 | self.mass 37 | } 38 | 39 | pub async fn episode(&self, ctx: &Context<'_>) -> Option { 40 | Some(Episode::JEDI) 41 | } 42 | 43 | pub async fn friends( 44 | &self, 45 | ctx: &Context<'_>, 46 | first: Option, 47 | after: Option, 48 | ) -> FriendsConnection { 49 | if self.id.0 == "2".to_string() { 50 | FriendsConnection { 51 | totalCount: Some(0), 52 | edges: vec![], 53 | pageInfo: PageInfo { 54 | startCursor: None, 55 | endCursor: None, 56 | hasPreviousPage: false, 57 | hasNextPage: false, 58 | }, 59 | } 60 | } else { 61 | FriendsConnection { 62 | totalCount: Some(2), 63 | edges: vec![ 64 | FriendsEdge { 65 | cursor: ID::from("1"), 66 | node: Some(Character::Human(luke())), 67 | }, 68 | FriendsEdge { 69 | cursor: ID::from("3"), 70 | node: Some(Character::Human(han())), 71 | }, 72 | ], 73 | pageInfo: PageInfo { 74 | startCursor: None, 75 | endCursor: None, 76 | hasPreviousPage: false, 77 | hasNextPage: false, 78 | }, 79 | } 80 | } 81 | } 82 | 83 | pub async fn appearsIn(&self, ctx: &Context<'_>) -> Vec { 84 | vec![Episode::NEWHOPE, Episode::JEDI, Episode::EMPIRE] 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /cli/src/code_generate/operation/operation_mod_file.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use codegen::{Scope, Type}; 4 | use heck::ToSnakeCase; 5 | use rusty_gql::{FieldType, OperationType}; 6 | 7 | use crate::code_generate::{use_gql_definitions, util::gql_value_ty_to_rust_ty, FileDefinition}; 8 | 9 | pub struct OperationModFile<'a> { 10 | pub operations: &'a HashMap, 11 | pub operation_type: OperationType, 12 | pub path: String, 13 | } 14 | 15 | impl<'a> FileDefinition for OperationModFile<'a> { 16 | fn name(&self) -> String { 17 | "mod.rs".to_string() 18 | } 19 | 20 | fn path(&self) -> String { 21 | self.path.to_string() 22 | } 23 | 24 | fn content(&self) -> String { 25 | self.build_content() 26 | } 27 | } 28 | 29 | impl<'a> OperationModFile<'a> { 30 | fn build_content(&self) -> String { 31 | let mut scope = Scope::new(); 32 | let struct_name = self.operation_type.to_string(); 33 | let struct_scope = scope.new_struct(&struct_name).vis("pub"); 34 | struct_scope.derive("Clone"); 35 | let imp = scope.new_impl(&struct_name); 36 | imp.r#macro("#[GqlType]"); 37 | 38 | let mut mod_str = "".to_string(); 39 | for (operation_name, _) in self.operations.iter() { 40 | let filename = operation_name.to_snake_case(); 41 | mod_str += format!("mod {};\n", filename,).as_str(); 42 | } 43 | 44 | for (operation_name, method) in self.operations.iter() { 45 | let fn_scope = imp.new_fn(operation_name); 46 | fn_scope.set_async(true); 47 | fn_scope.vis("pub"); 48 | fn_scope.arg_ref_self(); 49 | fn_scope.arg("ctx", "&Context<'_>"); 50 | 51 | let mut args_str = String::from(""); 52 | for arg in &method.arguments { 53 | fn_scope.arg(&arg.name, gql_value_ty_to_rust_ty(&arg.meta_type)); 54 | args_str += format!("{},", &arg.name).as_str(); 55 | } 56 | // remove last `,` 57 | args_str.pop(); 58 | 59 | let return_ty = gql_value_ty_to_rust_ty(&method.meta_type); 60 | fn_scope.ret(Type::new(&return_ty)); 61 | 62 | let filename = operation_name.to_snake_case(); 63 | fn_scope.line(format!( 64 | "{filename}::{method}(&ctx,{args}).await", 65 | filename = filename, 66 | method = method.name, 67 | args = args_str 68 | )); 69 | } 70 | 71 | format!( 72 | "{}\n{}\n\n{}", 73 | use_gql_definitions(), 74 | mod_str, 75 | scope.to_string() 76 | ) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /cli/src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use app::build_app; 3 | use async_recursion::async_recursion; 4 | use exit_codes::ExitCode; 5 | use std::process::Command; 6 | use std::{path::Path, process}; 7 | 8 | use crate::code_generate::{create_gql_files, create_project_files}; 9 | 10 | mod app; 11 | mod code_generate; 12 | mod exit_codes; 13 | 14 | #[async_recursion] 15 | async fn visit_dirs(path: &Path) -> std::io::Result> { 16 | let mut dir = tokio::fs::read_dir(path).await?; 17 | let mut schemas = Vec::new(); 18 | while let Some(child) = dir.next_entry().await? { 19 | if child.metadata().await?.is_dir() { 20 | visit_dirs(&child.path()).await?; 21 | } else { 22 | let content = tokio::fs::read_to_string(child.path()).await?; 23 | schemas.push(content) 24 | } 25 | } 26 | 27 | Ok(schemas) 28 | } 29 | 30 | fn gql_files_path(app_name: Option<&str>) -> String { 31 | match app_name { 32 | Some(path) => format!("{}/src/graphql", path), 33 | None => "src/graphql".to_string(), 34 | } 35 | } 36 | 37 | async fn create_graphql_files(app_name: Option<&str>) -> Result<(), std::io::Error> { 38 | let path = app_name 39 | .map(|name| format!("{}/schema", name)) 40 | .unwrap_or_else(|| "schema".to_string()); 41 | let schema_contents = visit_dirs(Path::new(&path)).await?; 42 | 43 | let schema_contents: Vec<&str> = schema_contents.iter().map(|s| &**s).collect(); 44 | 45 | let gql_files_path = gql_files_path(app_name); 46 | create_gql_files(&schema_contents, &gql_files_path).await 47 | } 48 | 49 | fn run_fmt() { 50 | Command::new("cargo") 51 | .arg("fmt") 52 | .spawn() 53 | .expect("Failed to run cargo fmt."); 54 | } 55 | 56 | async fn run() -> Result { 57 | let matches = build_app().get_matches(); 58 | if matches.subcommand_matches("generate").is_some() { 59 | create_graphql_files(None).await?; 60 | run_fmt(); 61 | return Ok(ExitCode::Success); 62 | } 63 | 64 | if let Some(new_matches) = matches.subcommand_matches("new") { 65 | if let Some(app_name) = new_matches.value_of("name") { 66 | create_project_files(app_name).await?; 67 | create_graphql_files(Some(app_name)).await?; 68 | println!("Successfully created the rusty-gql project!"); 69 | return Ok(ExitCode::Success); 70 | } 71 | } 72 | 73 | Ok(ExitCode::Success) 74 | } 75 | 76 | #[tokio::main] 77 | async fn main() { 78 | let result = run().await; 79 | match result { 80 | Ok(code) => process::exit(code.into()), 81 | Err(err) => { 82 | eprintln!("rusty-gql: {:#}", err); 83 | process::exit(ExitCode::Failure.into()) 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/types/value_type.rs: -------------------------------------------------------------------------------- 1 | use graphql_parser::schema::Type; 2 | 3 | use crate::GqlValue; 4 | 5 | #[derive(Debug, Clone)] 6 | pub enum GqlValueType { 7 | NamedType(String), 8 | ListType(Box), 9 | NonNullType(Box), 10 | } 11 | 12 | impl GqlValueType { 13 | pub fn name(&self) -> &str { 14 | match self { 15 | GqlValueType::NamedType(name) => name, 16 | GqlValueType::ListType(list_type) => list_type.name(), 17 | GqlValueType::NonNullType(non_null_type) => non_null_type.name(), 18 | } 19 | } 20 | 21 | pub fn to_parser_type<'a>(&self) -> Type<'a, String> { 22 | match self { 23 | GqlValueType::NamedType(name) => Type::NamedType(name.clone()), 24 | GqlValueType::ListType(list) => Type::ListType(Box::new(list.to_parser_type())), 25 | GqlValueType::NonNullType(non_null) => { 26 | Type::NonNullType(Box::new(non_null.to_parser_type())) 27 | } 28 | } 29 | } 30 | 31 | pub fn is_non_null(&self) -> bool { 32 | matches!(self, &GqlValueType::NonNullType(_)) 33 | } 34 | 35 | pub fn is_sub_type(&self, sub: &GqlValueType, default_value: &Option) -> bool { 36 | match (self, sub) { 37 | (GqlValueType::NonNullType(base_type), GqlValueType::NonNullType(sub_type)) => { 38 | base_type.is_sub_type(&*sub_type, default_value) 39 | } 40 | (GqlValueType::NamedType(base_type_name), GqlValueType::NonNullType(sub_type)) => { 41 | base_type_name.eq(&sub_type.name()) 42 | } 43 | (GqlValueType::NonNullType(base_type), GqlValueType::NamedType(sub_type)) => { 44 | if default_value.is_some() { 45 | base_type.name().eq(sub_type) 46 | } else { 47 | false 48 | } 49 | } 50 | (GqlValueType::NamedType(base_type_name), GqlValueType::NamedType(sub_type_name)) => { 51 | base_type_name.eq(sub_type_name) 52 | } 53 | (GqlValueType::ListType(base_type), GqlValueType::ListType(sub_type)) => { 54 | base_type.is_sub_type(&*sub_type, default_value) 55 | } 56 | _ => false, 57 | } 58 | } 59 | } 60 | 61 | impl<'a> From> for GqlValueType { 62 | fn from(meta_type: Type<'a, String>) -> Self { 63 | match meta_type { 64 | Type::NamedType(named_type) => GqlValueType::NamedType(named_type), 65 | Type::ListType(list_type) => GqlValueType::ListType(Box::new(Self::from(*list_type))), 66 | Type::NonNullType(non_null) => { 67 | GqlValueType::NonNullType(Box::new(Self::from(*non_null))) 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /cli/src/code_generate/project/axum/main_file.rs: -------------------------------------------------------------------------------- 1 | use crate::code_generate::FileDefinition; 2 | use codegen::Scope; 3 | 4 | pub struct AxumMainFile<'a> { 5 | pub app_name: &'a str, 6 | } 7 | 8 | impl<'a> FileDefinition for AxumMainFile<'a> { 9 | fn name(&self) -> String { 10 | "main.rs".to_string() 11 | } 12 | 13 | fn path(&self) -> String { 14 | format!("{}/src/main.rs", self.app_name) 15 | } 16 | 17 | fn content(&self) -> String { 18 | main_file_content() 19 | } 20 | } 21 | 22 | fn main_file_content() -> String { 23 | let contents = vec![ 24 | axum_import_str(), 25 | axum_gql_handler(), 26 | axum_gql_playground(), 27 | axum_main_function(), 28 | ]; 29 | contents.join("\n\n") 30 | } 31 | 32 | fn axum_import_str() -> String { 33 | let statements = vec![ 34 | "mod graphql;", 35 | "use rusty_gql::*;", 36 | "use rusty_gql_axum::*;", 37 | "use std::{net::SocketAddr, path::Path};", 38 | "use axum::{routing::get, AddExtensionLayer, Router};", 39 | "use graphql::Query;", 40 | "type ContainerType = Container;", 41 | ]; 42 | statements.join("\n") 43 | } 44 | 45 | fn axum_gql_handler() -> String { 46 | let mut scope = Scope::new(); 47 | let f = scope.new_fn("gql_handler"); 48 | f.set_async(true); 49 | f.arg("container", "axum::extract::Extension"); 50 | f.arg("req", "GqlRequest"); 51 | f.ret("GqlResponse"); 52 | f.line("let result = execute(&container, req.0).await;"); 53 | f.line("GqlResponse::from(result)"); 54 | 55 | scope.to_string() 56 | } 57 | 58 | fn axum_gql_playground() -> String { 59 | let mut scope = Scope::new(); 60 | let f = scope.new_fn("gql_playground"); 61 | f.set_async(true); 62 | f.ret("impl axum::response::IntoResponse"); 63 | f.line("axum::response::Html(playground_html(\"/\", None))"); 64 | 65 | scope.to_string() 66 | } 67 | 68 | fn axum_main_function() -> String { 69 | let mut scope = Scope::new(); 70 | let f = scope.new_fn("main"); 71 | f.set_async(true); 72 | f.line("let schema_docs = read_schemas(Path::new(\"./schema\")).unwrap();"); 73 | f.line("let schema_docs: Vec<&str> = schema_docs.iter().map(|s| &**s).collect();"); 74 | f.line("let container = Container::new(schema_docs.as_slice(), Query, EmptyMutation, EmptySubscription, Default::default(),).unwrap();"); 75 | f.line("let app = Router::new().route(\"/graphiql\", get(gql_playground)).route(\"/\", get(gql_handler).post(gql_handler)).layer(AddExtensionLayer::new(container));"); 76 | f.line("let addr = SocketAddr::from(([127, 0, 0, 1], 3000));"); 77 | f.line("axum::Server::bind(&addr).serve(app.into_make_service()).await.unwrap();"); 78 | 79 | format!("#[tokio::main]\n{}", scope.to_string()) 80 | } 81 | -------------------------------------------------------------------------------- /macro/src/union.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::{self, TokenStream}; 2 | use quote::quote; 3 | use syn::{ext::IdentExt, DeriveInput}; 4 | 5 | pub fn generate_union(derive_input: &DeriveInput) -> Result { 6 | let self_ty = &derive_input.ident; 7 | let crate_name = quote! { rusty_gql }; 8 | 9 | let type_name = self_ty.unraw().to_string(); 10 | 11 | let (impl_generics, _, where_clause) = &derive_input.generics.split_for_impl(); 12 | 13 | let union_data = match &derive_input.data { 14 | syn::Data::Enum(v) => v, 15 | _ => { 16 | return Err(syn::Error::new_spanned( 17 | &derive_input.ident, 18 | "Union type must be enum rust type", 19 | )); 20 | } 21 | }; 22 | 23 | let mut introspection_type_names = Vec::new(); 24 | let mut collect_all_fields = Vec::new(); 25 | 26 | for variant in &union_data.variants { 27 | let enum_value_ident = &variant.ident; 28 | 29 | introspection_type_names.push(quote! { 30 | #self_ty::#enum_value_ident(obj) => obj.introspection_type_name() 31 | }); 32 | 33 | collect_all_fields.push(quote! { 34 | #self_ty::#enum_value_ident(obj) => obj.collect_all_fields(ctx, fields) 35 | }) 36 | } 37 | 38 | let expanded = quote! { 39 | #[#crate_name::async_trait::async_trait] 40 | impl #impl_generics #crate_name::FieldResolver for #self_ty #where_clause { 41 | async fn resolve_field(&self, ctx: &#crate_name::Context<'_>) -> #crate_name::ResolverResult<::std::option::Option<#crate_name::GqlValue>> { 42 | Ok(None) 43 | } 44 | fn type_name() -> String { 45 | #type_name.to_string() 46 | } 47 | } 48 | 49 | impl #impl_generics #crate_name::CollectFields for #self_ty #where_clause { 50 | fn introspection_type_name(&self) -> String { 51 | match self { 52 | #(#introspection_type_names),* 53 | } 54 | } 55 | 56 | fn collect_all_fields<'union, 'ctx: 'union>( 57 | &'union self, 58 | ctx: &SelectionSetContext<'ctx>, 59 | fields: &mut Fields<'union>, 60 | ) -> ResolverResult<()> { 61 | match self { 62 | #(#collect_all_fields),* 63 | } 64 | } 65 | } 66 | 67 | #[#crate_name::async_trait::async_trait] 68 | impl #impl_generics #crate_name::SelectionSetResolver for #self_ty #where_clause { 69 | async fn resolve_selection_set(&self, ctx: &#crate_name::SelectionSetContext<'_>) -> #crate_name::ResolverResult<#crate_name::GqlValue> { 70 | #crate_name::resolve_selection_parallelly(ctx, self).await 71 | } 72 | } 73 | }; 74 | 75 | Ok(expanded.into()) 76 | } 77 | -------------------------------------------------------------------------------- /macro/src/utils.rs: -------------------------------------------------------------------------------- 1 | use syn::{FnArg, ImplItemMethod, Meta, NestedMeta, Pat, PatIdent, Type, TypeReference}; 2 | 3 | pub fn is_internal(args: &[NestedMeta]) -> bool { 4 | for arg in args { 5 | if let NestedMeta::Meta(Meta::Path(path)) = arg { 6 | let ident = &path.segments.last().unwrap().ident; 7 | if ident == "internal" { 8 | return true; 9 | } 10 | } 11 | } 12 | false 13 | } 14 | 15 | pub fn is_interface(args: &[NestedMeta]) -> bool { 16 | for arg in args { 17 | if let NestedMeta::Meta(Meta::Path(path)) = arg { 18 | let ident = &path.segments.last().unwrap().ident; 19 | if ident == "interface" { 20 | return true; 21 | } 22 | } 23 | } 24 | false 25 | } 26 | 27 | pub fn get_method_args_without_context( 28 | method: &ImplItemMethod, 29 | ) -> Result, syn::Error> { 30 | let mut args = Vec::new(); 31 | if method.sig.inputs.is_empty() { 32 | return Err(syn::Error::new_spanned( 33 | &method.sig, 34 | "self must be the first argument.", 35 | )); 36 | } 37 | 38 | for (index, arg) in method.sig.inputs.iter().enumerate() { 39 | if is_context_type(arg) { 40 | continue; 41 | } 42 | 43 | match arg { 44 | FnArg::Receiver(receiver) => { 45 | if index != 0 { 46 | return Err(syn::Error::new_spanned( 47 | receiver, 48 | "self must be the first argument.", 49 | )); 50 | } 51 | } 52 | 53 | FnArg::Typed(pat_type) => { 54 | if index == 0 { 55 | return Err(syn::Error::new_spanned( 56 | pat_type, 57 | "self must be the first argument.", 58 | )); 59 | } 60 | 61 | if let Pat::Ident(ident) = &*pat_type.pat { 62 | args.push((ident.clone(), pat_type.ty.as_ref().clone())); 63 | } else { 64 | return Err(syn::Error::new_spanned(pat_type, "Invalid arg")); 65 | } 66 | } 67 | } 68 | } 69 | Ok(args) 70 | } 71 | 72 | pub fn is_context_type(arg: &FnArg) -> bool { 73 | let mut is_context = false; 74 | if let FnArg::Typed(pat) = arg { 75 | if let Type::Reference(TypeReference { elem, .. }) = &*pat.ty { 76 | if let Type::Path(path) = elem.as_ref() { 77 | is_context = path.path.segments.last().unwrap().ident == "Context"; 78 | } 79 | } 80 | } 81 | is_context 82 | } 83 | 84 | pub fn is_result_type(return_type: &Type) -> bool { 85 | if let Type::Path(ty_path) = return_type { 86 | if ty_path.path.segments.last().unwrap().ident == "Result" { 87 | return true; 88 | } 89 | } 90 | false 91 | } 92 | -------------------------------------------------------------------------------- /cli/src/code_generate/util.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use rusty_gql::{GqlValueType, TypeDefinition}; 4 | use syn::{ext::IdentExt, ItemUse}; 5 | 6 | pub fn get_interface_impl_object_map( 7 | type_definitions: &HashMap, 8 | ) -> HashMap> { 9 | let mut map = HashMap::new(); 10 | 11 | for ty_def in type_definitions.values() { 12 | if let TypeDefinition::Object(obj) = ty_def { 13 | for interface_name in &obj.implements_interfaces { 14 | map.entry(interface_name.to_string()) 15 | .or_insert_with(Vec::new) 16 | .push(obj.name.to_string()); 17 | } 18 | } 19 | } 20 | map 21 | } 22 | 23 | pub fn gql_value_ty_to_rust_ty(gql_value: &GqlValueType) -> String { 24 | value_ty_to_str(gql_value, true) 25 | } 26 | 27 | fn value_ty_to_str(gql_value: &GqlValueType, optional: bool) -> String { 28 | match gql_value { 29 | GqlValueType::NamedType(name) => gql_to_rust_type_str(name, optional), 30 | GqlValueType::ListType(list_type) => { 31 | if optional { 32 | format!("Option>", value_ty_to_str(list_type, true)) 33 | } else { 34 | format!("Vec<{}>", value_ty_to_str(list_type, true)) 35 | } 36 | } 37 | GqlValueType::NonNullType(non_null_type) => value_ty_to_str(non_null_type, false), 38 | } 39 | } 40 | 41 | fn gql_to_rust_type_str(gql_type: &str, optional: bool) -> String { 42 | let name = match gql_type { 43 | "Int" => "i32".to_string(), 44 | "Float" => "f32".to_string(), 45 | "String" => "String".to_string(), 46 | "Boolean" => "bool".to_string(), 47 | _ => gql_type.to_string(), 48 | }; 49 | if optional { 50 | format!("Option<{}>", name) 51 | } else { 52 | name 53 | } 54 | } 55 | 56 | pub fn is_gql_primitive_ty(type_name: &str) -> bool { 57 | vec!["String", "Int", "Float", "Boolean", "ID"].contains(&type_name) 58 | } 59 | 60 | pub fn is_introspection_type_names(type_name: &str) -> bool { 61 | vec![ 62 | "__Directive", 63 | "__DirectiveLocation", 64 | "__EnumValue", 65 | "__Field", 66 | "__InputValue", 67 | "__Schema", 68 | "__Type", 69 | "__TypeKind", 70 | ] 71 | .contains(&type_name) 72 | } 73 | 74 | pub fn is_default_item_use(item_use: &ItemUse) -> bool { 75 | if let syn::UseTree::Path(use_path) = &item_use.tree { 76 | let ident = use_path.ident.unraw().to_string(); 77 | if ident.eq("rusty_gql") { 78 | return true; 79 | } 80 | 81 | if ident.eq("crate") { 82 | if let syn::UseTree::Path(child_path) = &*use_path.tree { 83 | let ident = child_path.ident.unraw().to_string(); 84 | if ident.eq("graphql") { 85 | return true; 86 | } 87 | } 88 | } 89 | } 90 | false 91 | } 92 | -------------------------------------------------------------------------------- /macro/src/input_object.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::{self, TokenStream}; 2 | use quote::quote; 3 | use syn::{ext::IdentExt, DeriveInput}; 4 | 5 | pub fn generate_input_object(derive_input: &DeriveInput) -> Result { 6 | let self_ty = &derive_input.ident; 7 | let crate_name = quote! { rusty_gql }; 8 | 9 | let type_name = self_ty.unraw().to_string(); 10 | 11 | let (impl_generics, _, where_clause) = &derive_input.generics.split_for_impl(); 12 | 13 | let struct_data = match &derive_input.data { 14 | syn::Data::Struct(v) => v, 15 | _ => { 16 | return Err(syn::Error::new_spanned( 17 | &derive_input.ident, 18 | "Input Object type must be struct type", 19 | )); 20 | } 21 | }; 22 | 23 | let mut fields = Vec::new(); 24 | let mut get_fields = Vec::new(); 25 | let mut set_fields = Vec::new(); 26 | for field in &struct_data.fields { 27 | let ident = field.ident.as_ref().unwrap(); 28 | let ty = &field.ty; 29 | let field_name = ident.unraw().to_string(); 30 | 31 | get_fields.push(quote! { 32 | let #ident: #ty = #crate_name::GqlInputType::from_gql_value(obj.get(#field_name).cloned())?; 33 | }); 34 | fields.push(ident); 35 | 36 | set_fields.push(quote! { 37 | obj.insert(#field_name.to_string(), #crate_name::GqlInputType::to_gql_value(&self.#ident)); 38 | }) 39 | } 40 | 41 | let expanded = quote! { 42 | impl #impl_generics #crate_name::GqlInputType for #self_ty #where_clause { 43 | fn from_gql_value(value: Option) -> Result { 44 | if let Some(GqlValue::Object(obj)) = value { 45 | #(#get_fields)* 46 | Ok(Self { #(#fields),* }) 47 | } else { 48 | Err("Invalid type, Expected type: object".to_string()) 49 | } 50 | } 51 | 52 | fn to_gql_value(&self) -> GqlValue { 53 | let mut obj = std::collections::BTreeMap::new(); 54 | #(#set_fields)* 55 | #crate_name::GqlValue::Object(obj) 56 | } 57 | } 58 | 59 | #[#crate_name::async_trait::async_trait] 60 | impl #impl_generics #crate_name::FieldResolver for #self_ty #where_clause { 61 | async fn resolve_field(&self, ctx: &#crate_name::Context<'_>) -> #crate_name::ResolverResult<::std::option::Option<#crate_name::GqlValue>> { 62 | Ok(Some(self.to_gql_value())) 63 | } 64 | fn type_name() -> String { 65 | #type_name.to_string() 66 | } 67 | } 68 | 69 | impl #impl_generics #crate_name::CollectFields for #self_ty #where_clause {} 70 | 71 | #[#crate_name::async_trait::async_trait] 72 | impl #impl_generics #crate_name::SelectionSetResolver for #self_ty #where_clause { 73 | async fn resolve_selection_set(&self, ctx: &#crate_name::SelectionSetContext<'_>) -> #crate_name::ResolverResult<#crate_name::GqlValue> { 74 | Ok(self.to_gql_value()) 75 | } 76 | } 77 | }; 78 | 79 | Ok(expanded.into()) 80 | } 81 | -------------------------------------------------------------------------------- /src/request.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use futures_util::{pin_mut, AsyncRead, AsyncReadExt}; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::{variables::Variables, GqlValue}; 7 | 8 | #[derive(Serialize, Deserialize, Debug)] 9 | #[serde(rename_all = "camelCase")] 10 | pub struct Request { 11 | #[serde(default)] 12 | pub query: String, 13 | #[serde(default)] 14 | pub operation_name: Option, 15 | #[serde(default)] 16 | pub variables: Variables, 17 | #[serde(default)] 18 | pub extensions: HashMap, 19 | } 20 | 21 | pub async fn receive_http_request( 22 | body: impl AsyncRead + Send, 23 | ) -> Result { 24 | receive_json_body(body).await 25 | } 26 | 27 | pub async fn receive_json_body(body: impl AsyncRead) -> Result { 28 | let mut data = Vec::new(); 29 | pin_mut!(body); 30 | 31 | body.read_to_end(&mut data) 32 | .await 33 | .map_err(HttpRequestError::Io)?; 34 | Ok(serde_json::from_slice::(&data) 35 | .map_err(|err| HttpRequestError::InvalidRequest(Box::new(err)))?) 36 | } 37 | 38 | #[derive(Debug)] 39 | pub enum HttpRequestError { 40 | Io(std::io::Error), 41 | InvalidRequest(Box), 42 | } 43 | 44 | #[cfg(test)] 45 | mod tests { 46 | use serde_json::Number; 47 | 48 | use crate::{GqlValue, Request}; 49 | 50 | #[test] 51 | fn test_operation_name() { 52 | let query_doc = r#"{"query": "{ hero droids jedi }", "operationName": "hero"}"#; 53 | let req = serde_json::from_str::(query_doc).unwrap(); 54 | assert_eq!(req.query, "{ hero droids jedi }"); 55 | assert_eq!(req.operation_name, Some("hero".to_string())); 56 | assert!(req.variables.0.is_empty()); 57 | } 58 | 59 | #[test] 60 | fn test_variables() { 61 | let query_doc = r#"{"query": "{ hero droids jedi }", "variables": {"var1": 100, "var2": "value", "var3": [1,1,1]}}"#; 62 | let req = serde_json::from_str::(query_doc).unwrap(); 63 | assert_eq!(req.query, "{ hero droids jedi }"); 64 | assert!(req.operation_name.is_none()); 65 | 66 | assert_eq!( 67 | req.variables.0.get("var1"), 68 | Some(&GqlValue::Number(Number::from(100 as i32))) 69 | ); 70 | assert_eq!( 71 | req.variables.0.get("var2"), 72 | Some(&GqlValue::String(String::from("value"))) 73 | ); 74 | assert_eq!( 75 | req.variables.0.get("var3"), 76 | Some(&GqlValue::List(vec![ 77 | GqlValue::Number(Number::from(1 as i32)), 78 | GqlValue::Number(Number::from(1 as i32)), 79 | GqlValue::Number(Number::from(1 as i32)) 80 | ])) 81 | ); 82 | } 83 | 84 | #[test] 85 | fn test_null_variables() { 86 | let query_doc = r#"{"query": "{ hero droids jedi }", "variables": null}"#; 87 | let req = serde_json::from_str::(query_doc).unwrap(); 88 | assert_eq!(req.query, "{ hero droids jedi }"); 89 | assert!(req.operation_name.is_none()); 90 | assert!(req.variables.0.is_empty()); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/validation/rules/known_type_names.rs: -------------------------------------------------------------------------------- 1 | use graphql_parser::Pos; 2 | 3 | use crate::{ 4 | validation::visitor::{ValidationContext, Visitor}, 5 | GqlValueType, 6 | }; 7 | 8 | #[derive(Default)] 9 | pub struct KnownTypeNames; 10 | 11 | impl<'a> Visitor<'a> for KnownTypeNames { 12 | fn enter_fragment_definition( 13 | &mut self, 14 | ctx: &mut ValidationContext, 15 | _name: &'a str, 16 | fragment_definition: &'a graphql_parser::query::FragmentDefinition<'a, String>, 17 | ) { 18 | match &fragment_definition.type_condition { 19 | graphql_parser::query::TypeCondition::On(on_ty) => { 20 | validate(ctx, on_ty, fragment_definition.position) 21 | } 22 | } 23 | } 24 | fn enter_variable_definition( 25 | &mut self, 26 | ctx: &mut ValidationContext, 27 | variable_definition: &'a graphql_parser::query::VariableDefinition<'a, String>, 28 | ) { 29 | validate( 30 | ctx, 31 | GqlValueType::from(variable_definition.var_type.clone()).name(), 32 | variable_definition.position, 33 | ) 34 | } 35 | 36 | fn enter_inline_fragment( 37 | &mut self, 38 | ctx: &mut ValidationContext, 39 | fragment_spread: &'a graphql_parser::query::InlineFragment<'a, String>, 40 | ) { 41 | if let Some(ty_condition) = &fragment_spread.type_condition { 42 | match ty_condition { 43 | graphql_parser::query::TypeCondition::On(on_ty) => { 44 | validate(ctx, on_ty, fragment_spread.position) 45 | } 46 | } 47 | } 48 | } 49 | } 50 | 51 | fn validate(ctx: &mut ValidationContext, name: &str, position: Pos) { 52 | if !ctx.schema.type_definitions.contains_key(name) { 53 | ctx.add_error(format!("Unknown type {}", name), vec![position]) 54 | } 55 | } 56 | 57 | #[cfg(test)] 58 | mod tests { 59 | use crate::{check_fails_rule, check_passes_rule}; 60 | 61 | use super::*; 62 | 63 | fn factory() -> KnownTypeNames { 64 | KnownTypeNames::default() 65 | } 66 | 67 | #[test] 68 | fn include_known_types() { 69 | let query_doc = r#" 70 | query { 71 | hero { 72 | ...CharacterField 73 | } 74 | droid(id: 1) { 75 | ...CharacterField 76 | } 77 | } 78 | fragment CharacterField on Character { 79 | name 80 | } 81 | "#; 82 | check_passes_rule!(query_doc, factory); 83 | } 84 | 85 | #[test] 86 | fn include_unknown_types() { 87 | let query_doc = r#" 88 | query { 89 | hero { 90 | ...CharacterField 91 | friends { 92 | ... on Test { name } 93 | } 94 | } 95 | droid(id: 1) { 96 | ...CharacterField 97 | } 98 | } 99 | fragment CharacterField on Characterrr { 100 | name 101 | } 102 | "#; 103 | check_fails_rule!(query_doc, factory); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/validation/rules/unique_argument_names.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use graphql_parser::schema::Value; 4 | 5 | use crate::validation::visitor::Visitor; 6 | 7 | #[derive(Default)] 8 | pub struct UniqueArgumentNames<'a> { 9 | names: HashSet<&'a str>, 10 | } 11 | 12 | impl<'a> Visitor<'a> for UniqueArgumentNames<'a> { 13 | fn enter_directive( 14 | &mut self, 15 | _ctx: &mut crate::validation::visitor::ValidationContext, 16 | _directive: &'a graphql_parser::schema::Directive<'a, String>, 17 | ) { 18 | self.names.clear(); 19 | } 20 | 21 | fn enter_field( 22 | &mut self, 23 | _ctx: &mut crate::validation::visitor::ValidationContext, 24 | _field: &'a graphql_parser::query::Field<'a, String>, 25 | ) { 26 | self.names.clear(); 27 | } 28 | 29 | fn enter_argument( 30 | &mut self, 31 | ctx: &mut crate::validation::visitor::ValidationContext, 32 | arg_name: &'a str, 33 | _arg_value: &'a Value<'a, String>, 34 | ) { 35 | if !self.names.insert(arg_name) { 36 | ctx.add_error(format!("{} is already contained.", arg_name), vec![]) 37 | } 38 | } 39 | } 40 | 41 | #[cfg(test)] 42 | mod tests { 43 | use crate::{check_fails_rule, check_passes_rule}; 44 | 45 | use super::*; 46 | 47 | fn factory<'a>() -> UniqueArgumentNames<'a> { 48 | UniqueArgumentNames::default() 49 | } 50 | 51 | #[test] 52 | fn no_args_on_field() { 53 | let query_doc = r#" 54 | { 55 | human { 56 | name 57 | } 58 | } 59 | "#; 60 | check_passes_rule!(query_doc, factory); 61 | } 62 | 63 | #[test] 64 | fn no_args_on_directive() { 65 | let query_doc = r#" 66 | { 67 | human { 68 | name @deprecated 69 | } 70 | } 71 | "#; 72 | check_passes_rule!(query_doc, factory); 73 | } 74 | 75 | #[test] 76 | fn args_on_field() { 77 | let query_doc = r#" 78 | { 79 | droid(id: 1) { 80 | name 81 | } 82 | } 83 | "#; 84 | check_passes_rule!(query_doc, factory); 85 | } 86 | 87 | #[test] 88 | fn args_on_directive() { 89 | let query_doc = r#" 90 | { 91 | human { 92 | name @skip(if: true) 93 | } 94 | } 95 | "#; 96 | check_passes_rule!(query_doc, factory); 97 | } 98 | 99 | #[test] 100 | fn duplicate_args_on_field() { 101 | let query_doc = r#" 102 | { 103 | droid(id: 1, id: 2, id: 3) { 104 | name 105 | } 106 | } 107 | "#; 108 | check_fails_rule!(query_doc, factory); 109 | } 110 | 111 | #[test] 112 | fn duplicate_args_on_directive() { 113 | let query_doc = r#" 114 | { 115 | human { 116 | name @skip(if: true, if: false, if: true) 117 | } 118 | } 119 | "#; 120 | check_fails_rule!(query_doc, factory); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /cli/src/code_generate/directive/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, io::Error}; 2 | 3 | use codegen::Scope; 4 | use futures_util::future::try_join_all; 5 | use heck::{ToSnakeCase, ToUpperCamelCase}; 6 | use rusty_gql::DirectiveDefinition; 7 | 8 | use crate::code_generate::{use_gql_definitions, FileDefinition}; 9 | 10 | use super::{create_file, mod_file::ModFile, path_str, CreateFile}; 11 | 12 | pub struct DirectiveFile<'a> { 13 | pub def: &'a DirectiveDefinition, 14 | pub path: String, 15 | pub filename: String, 16 | } 17 | 18 | impl<'a> FileDefinition for DirectiveFile<'a> { 19 | fn content(&self) -> String { 20 | let mut scope = Scope::new(); 21 | let struct_name = &self.def.name.to_upper_camel_case(); 22 | scope.new_struct(struct_name).vis("pub"); 23 | let new_impl = scope.new_impl(struct_name); 24 | let new_fn = new_impl.new_fn("new"); 25 | new_fn.ret("Box"); 26 | new_fn.line(format!("Box::new({} {{}})", struct_name)); 27 | 28 | let directive_impl = scope.new_impl(struct_name); 29 | directive_impl.impl_trait("CustomDirective"); 30 | directive_impl.r#macro("#[async_trait::async_trait]"); 31 | 32 | let resolve_fn = directive_impl.new_fn("resolve_field"); 33 | resolve_fn.set_async(true); 34 | resolve_fn.arg_ref_self(); 35 | resolve_fn.arg("ctx", "&Context<'_>"); 36 | resolve_fn.arg("directive_args", "&BTreeMap"); 37 | resolve_fn.arg("resolve_fut", "ResolveFut<'_>"); 38 | resolve_fn.ret("ResolverResult>"); 39 | resolve_fn.line("todo!()"); 40 | 41 | format!( 42 | "{}\nuse std::collections::BTreeMap;\n\n{}", 43 | use_gql_definitions(), 44 | scope.to_string() 45 | ) 46 | } 47 | 48 | fn path(&self) -> String { 49 | self.path.to_string() 50 | } 51 | 52 | fn name(&self) -> String { 53 | self.filename.clone() 54 | } 55 | } 56 | 57 | pub async fn create_directive_files( 58 | directives: &HashMap, 59 | base_path: &str, 60 | ) -> Result, Error> { 61 | let mut futures = Vec::new(); 62 | let mut directive_names = Vec::new(); 63 | for (_, directive) in directives.iter() { 64 | if is_default_directive(&directive.name) { 65 | continue; 66 | } 67 | let filename = &directive.name.to_snake_case(); 68 | let path = path_str(vec![base_path, "directive", filename], true); 69 | futures.push(create_file(DirectiveFile { 70 | def: directive, 71 | path, 72 | filename: filename.clone(), 73 | })); 74 | directive_names.push(directive.name.clone()); 75 | } 76 | let struct_names = directive_names 77 | .iter() 78 | .map(|name| name.to_upper_camel_case()) 79 | .collect::>(); 80 | ModFile { 81 | path: &path_str(vec![base_path, "directive"], false), 82 | struct_names, 83 | } 84 | .create_file() 85 | .await?; 86 | 87 | try_join_all(futures).await 88 | } 89 | 90 | fn is_default_directive(name: &str) -> bool { 91 | vec!["skip", "include", "deprecated"].contains(&name) 92 | } 93 | -------------------------------------------------------------------------------- /src/resolver/object.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{BTreeMap, HashMap}; 2 | 3 | use serde::Serialize; 4 | 5 | use crate::{ 6 | types::value::serialize_into_gql_value, CollectFields, Context, FieldResolver, GqlValue, 7 | ResolverResult, SelectionSetContext, SelectionSetResolver, 8 | }; 9 | 10 | #[async_trait::async_trait] 11 | impl FieldResolver for BTreeMap 12 | where 13 | K: ToString + Eq + Send + Sync, 14 | V: Serialize + Send + Sync, 15 | { 16 | async fn resolve_field(&self, _ctx: &Context<'_>) -> ResolverResult> { 17 | let mut map = BTreeMap::new(); 18 | for (name, v) in self { 19 | map.insert( 20 | name.to_string(), 21 | serialize_into_gql_value(v).unwrap_or_default(), 22 | ); 23 | } 24 | Ok(Some(GqlValue::Object(map))) 25 | } 26 | fn type_name() -> String { 27 | "Object".to_string() 28 | } 29 | } 30 | 31 | impl CollectFields for BTreeMap 32 | where 33 | K: ToString + Eq + Send + Sync, 34 | V: Serialize + Send + Sync, 35 | { 36 | } 37 | 38 | #[async_trait::async_trait] 39 | impl SelectionSetResolver for BTreeMap 40 | where 41 | K: ToString + Eq + Send + Sync, 42 | V: Serialize + Send + Sync, 43 | { 44 | async fn resolve_selection_set( 45 | &self, 46 | _ctx: &SelectionSetContext<'_>, 47 | ) -> ResolverResult { 48 | let mut map = BTreeMap::new(); 49 | for (name, v) in self { 50 | map.insert( 51 | name.to_string(), 52 | serialize_into_gql_value(v).unwrap_or_default(), 53 | ); 54 | } 55 | Ok(GqlValue::Object(map)) 56 | } 57 | } 58 | 59 | #[async_trait::async_trait] 60 | impl FieldResolver for HashMap 61 | where 62 | K: ToString + Eq + Send + Sync, 63 | V: Serialize + Send + Sync, 64 | { 65 | async fn resolve_field(&self, _ctx: &Context<'_>) -> ResolverResult> { 66 | let mut map = BTreeMap::new(); 67 | for (name, v) in self { 68 | map.insert( 69 | name.to_string(), 70 | serialize_into_gql_value(v).unwrap_or_default(), 71 | ); 72 | } 73 | Ok(Some(GqlValue::Object(map))) 74 | } 75 | fn type_name() -> String { 76 | "Object".to_string() 77 | } 78 | } 79 | 80 | impl CollectFields for HashMap 81 | where 82 | K: ToString + Eq + Send + Sync, 83 | V: Serialize + Send + Sync, 84 | { 85 | } 86 | 87 | #[async_trait::async_trait] 88 | impl SelectionSetResolver for HashMap 89 | where 90 | K: ToString + Eq + Send + Sync, 91 | V: Serialize + Send + Sync, 92 | { 93 | async fn resolve_selection_set( 94 | &self, 95 | _ctx: &SelectionSetContext<'_>, 96 | ) -> ResolverResult { 97 | let mut map = BTreeMap::new(); 98 | for (name, v) in self { 99 | map.insert( 100 | name.to_string(), 101 | serialize_into_gql_value(v).unwrap_or_default(), 102 | ); 103 | } 104 | Ok(GqlValue::Object(map)) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /tests/custom_directive.rs: -------------------------------------------------------------------------------- 1 | #![allow(warnings, unused)] 2 | use std::collections::{BTreeMap, HashMap}; 3 | 4 | use rusty_gql::*; 5 | 6 | #[tokio::test] 7 | pub async fn test_directive() { 8 | #[derive(Clone, Debug)] 9 | struct Auth; 10 | 11 | impl Auth { 12 | fn new() -> Box { 13 | Box::new(Auth {}) 14 | } 15 | } 16 | 17 | #[async_trait::async_trait] 18 | impl CustomDirective for Auth { 19 | async fn resolve_field( 20 | &self, 21 | _ctx: &Context<'_>, 22 | directive_args: &BTreeMap, 23 | resolve_fut: ResolveFut<'_>, 24 | ) -> ResolverResult> { 25 | resolve_fut.await.map(|v| { 26 | if let Some(GqlValue::Enum(arg_value)) = directive_args.get("requires") { 27 | if arg_value == "ADMIN" { 28 | return None; 29 | } else { 30 | v 31 | } 32 | } else { 33 | v 34 | } 35 | }) 36 | } 37 | } 38 | struct Person { 39 | name: String, 40 | description: Option, 41 | age: i32, 42 | } 43 | 44 | #[GqlType] 45 | impl Person { 46 | async fn name(&self) -> String { 47 | self.name.clone() 48 | } 49 | async fn description(&self) -> Option { 50 | self.description.clone() 51 | } 52 | async fn age(&self) -> i32 { 53 | self.age 54 | } 55 | } 56 | 57 | struct Query; 58 | 59 | #[GqlType] 60 | impl Query { 61 | async fn persons(&self) -> Vec { 62 | vec![ 63 | Person { 64 | name: "Tom".to_string(), 65 | description: Some("test person".to_string()), 66 | age: 20, 67 | }, 68 | Person { 69 | name: "Mary".to_string(), 70 | description: Some("test person mary".to_string()), 71 | age: 28, 72 | }, 73 | ] 74 | } 75 | 76 | #[allow(unused)] 77 | async fn person(&self, id: ID) -> Person { 78 | Person { 79 | name: "Tom".to_string(), 80 | description: Some("test person".to_string()), 81 | age: 20, 82 | } 83 | } 84 | } 85 | let contents = schema_content("./tests/schemas/custom_directive.graphql"); 86 | 87 | let mut custom_directive_maps = HashMap::new(); 88 | custom_directive_maps.insert("auth", Auth::new()); 89 | 90 | let container = Container::new( 91 | &vec![contents.as_str()], 92 | Query, 93 | EmptyMutation, 94 | EmptySubscription, 95 | custom_directive_maps, 96 | ) 97 | .unwrap(); 98 | 99 | let query_doc = r#"{ person(id: 1) {name age} }"#; 100 | let req = build_test_request(query_doc, None, Default::default()); 101 | let expected_response = r#"{"data":{"person":{"age":20,"name":null}}}"#; 102 | check_gql_response(req, expected_response, &container).await; 103 | } 104 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | tests: 13 | name: tests - Rust (${{ matrix.rust }}) on ${{ matrix.os }} 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | include: 19 | - { rust: 1.58.1, os: ubuntu-latest } 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v2 23 | with: 24 | submodules: true 25 | - uses: actions-rs/toolchain@v1 26 | with: 27 | toolchain: ${{ matrix.rust }} 28 | override: true 29 | - name: Build with all features 30 | run: cargo build --all-features 31 | - name: Build 32 | run: cargo build --all --verbose 33 | - name: Run tests 34 | run: cargo test --all --all-features 35 | - name: Clean 36 | run: cargo clean 37 | 38 | rustfmt: 39 | name: rustfmt - Rust (${{ matrix.rust }}) on ${{ matrix.os }} 40 | runs-on: ${{ matrix.os }} 41 | strategy: 42 | fail-fast: false 43 | matrix: 44 | include: 45 | - { rust: stable, os: ubuntu-latest } 46 | steps: 47 | - name: Checkout 48 | uses: actions/checkout@v2 49 | with: 50 | submodules: true 51 | - uses: actions-rs/toolchain@v1 52 | with: 53 | toolchain: ${{ matrix.rust }} 54 | override: true 55 | components: rustfmt 56 | - name: Check format 57 | run: cargo fmt --all -- --check 58 | 59 | clippy: 60 | name: clippy - Rust (${{ matrix.rust }}) on ${{ matrix.os }} 61 | runs-on: ${{ matrix.os }} 62 | strategy: 63 | fail-fast: false 64 | matrix: 65 | include: 66 | - { rust: stable, os: ubuntu-latest } 67 | steps: 68 | - name: Checkout 69 | uses: actions/checkout@v2 70 | with: 71 | submodules: true 72 | - uses: actions-rs/toolchain@v1 73 | with: 74 | toolchain: ${{ matrix.rust }} 75 | override: true 76 | components: clippy 77 | - name: Check with clippy 78 | run: cargo clippy --all 79 | 80 | examples: 81 | name: Build examples - Rust (${{ matrix.rust }}) on ${{ matrix.os }} 82 | runs-on: ${{ matrix.os }} 83 | strategy: 84 | fail-fast: false 85 | matrix: 86 | include: 87 | - { rust: stable, os: ubuntu-latest } 88 | steps: 89 | - name: Checkout 90 | uses: actions/checkout@v2 91 | with: 92 | submodules: true 93 | - uses: actions-rs/toolchain@v1 94 | with: 95 | toolchain: ${{ matrix.rust }} 96 | override: true 97 | components: clippy, rustfmt 98 | - name: Check examples format 99 | run: cargo fmt --all -- --check 100 | working-directory: ./examples 101 | - name: Check examples with clippy 102 | run: cargo clippy --all 103 | working-directory: ./examples 104 | - name: Build examples 105 | run: cargo build --all --verbose 106 | working-directory: ./examples 107 | - name: Clean examples 108 | run: cargo clean 109 | working-directory: ./examples 110 | -------------------------------------------------------------------------------- /tests/fragment.rs: -------------------------------------------------------------------------------- 1 | #[allow(dead_code)] 2 | use rusty_gql::*; 3 | 4 | #[tokio::test] 5 | async fn test_inline_framgnet() { 6 | struct Person { 7 | name: String, 8 | description: Option, 9 | age: i32, 10 | } 11 | 12 | #[GqlType] 13 | impl Person { 14 | async fn name(&self) -> String { 15 | self.name.clone() 16 | } 17 | async fn description(&self) -> Option { 18 | self.description.clone() 19 | } 20 | async fn age(&self) -> i32 { 21 | self.age.clone() 22 | } 23 | } 24 | 25 | struct Query; 26 | 27 | #[GqlType] 28 | impl Query { 29 | #[allow(unused)] 30 | async fn person(&self, id: ID) -> Person { 31 | let person = Person { 32 | name: "Tom".to_string(), 33 | description: Some("description".to_string()), 34 | age: 20, 35 | }; 36 | person 37 | } 38 | } 39 | 40 | let contents = schema_content("./tests/schemas/test_schema.graphql"); 41 | 42 | let container = Container::new( 43 | &vec![contents.as_str()], 44 | Query, 45 | EmptyMutation, 46 | EmptySubscription, 47 | Default::default(), 48 | ) 49 | .unwrap(); 50 | 51 | let query = r#"{ person(id: 1) { ... on Person {name, age, description} } }"#; 52 | let req = build_test_request(query, None, Default::default()); 53 | let expected = r#"{"data":{"person":{"age":20,"description":"description","name":"Tom"}}}"#; 54 | check_gql_response(req, expected, &container).await; 55 | } 56 | 57 | #[tokio::test] 58 | async fn test_framgnet_spread() { 59 | struct Person { 60 | name: String, 61 | description: Option, 62 | age: i32, 63 | } 64 | 65 | #[GqlType] 66 | impl Person { 67 | async fn name(&self) -> String { 68 | self.name.clone() 69 | } 70 | async fn description(&self) -> Option { 71 | self.description.clone() 72 | } 73 | async fn age(&self) -> i32 { 74 | self.age.clone() 75 | } 76 | } 77 | 78 | struct Query; 79 | 80 | #[GqlType] 81 | impl Query { 82 | #[allow(unused)] 83 | async fn person(&self, id: ID) -> Person { 84 | let person = Person { 85 | name: "Tom".to_string(), 86 | description: Some("description".to_string()), 87 | age: 20, 88 | }; 89 | person 90 | } 91 | } 92 | 93 | let contents = schema_content("./tests/schemas/test_schema.graphql"); 94 | 95 | let container = Container::new( 96 | &vec![contents.as_str()], 97 | Query, 98 | EmptyMutation, 99 | EmptySubscription, 100 | Default::default(), 101 | ) 102 | .unwrap(); 103 | 104 | let query = r#" 105 | query { 106 | person(id: 1) { 107 | ...PersonFragment 108 | } 109 | } 110 | 111 | fragment PersonFragment on Person { 112 | name age 113 | } 114 | "#; 115 | let req = build_test_request(query, None, Default::default()); 116 | let expected = r#"{"data":{"person":{"age":20,"name":"Tom"}}}"#; 117 | check_gql_response(req, expected, &container).await; 118 | } 119 | -------------------------------------------------------------------------------- /src/types/introspection/input_value.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | resolve_selection_parallelly, CollectFields, FieldResolver, GqlValue, InputValueType, 3 | ResolverResult, Schema, SelectionSetContext, SelectionSetResolver, 4 | }; 5 | 6 | use super::introspection_type::__Type; 7 | 8 | pub struct __InputValue<'a> { 9 | schema: &'a Schema, 10 | detail: InputValueType, 11 | } 12 | pub fn build_input_value_introspection<'a>( 13 | schema: &'a Schema, 14 | value: &'a InputValueType, 15 | ) -> __InputValue<'a> { 16 | __InputValue { 17 | schema, 18 | detail: value.clone(), 19 | } 20 | } 21 | 22 | impl<'a> __InputValue<'a> { 23 | async fn name(&self) -> &str { 24 | self.detail.name.as_str() 25 | } 26 | 27 | async fn description(&self) -> Option<&str> { 28 | self.detail.description.as_deref() 29 | } 30 | 31 | async fn ty(&'a self) -> __Type<'a> { 32 | __Type::from_value_type(self.schema, &self.detail.meta_type) 33 | } 34 | 35 | async fn default_value(&self) -> Option { 36 | self.detail.default_value.as_ref().map(|v| v.to_string()) 37 | } 38 | } 39 | 40 | #[async_trait::async_trait] 41 | impl<'a> FieldResolver for __InputValue<'a> { 42 | async fn resolve_field( 43 | &self, 44 | ctx: &crate::Context<'_>, 45 | ) -> crate::ResolverResult> { 46 | if ctx.item.name == "name" { 47 | let name = self.name().await; 48 | let ctx_selection_set = ctx.with_selection_set(&ctx.item.selection_set); 49 | 50 | return SelectionSetResolver::resolve_selection_set(name, &ctx_selection_set) 51 | .await 52 | .map(Some); 53 | } 54 | 55 | if ctx.item.name == "description" { 56 | let desc = self.description().await; 57 | let ctx_selection_set = ctx.with_selection_set(&ctx.item.selection_set); 58 | 59 | match desc { 60 | Some(v) => { 61 | return SelectionSetResolver::resolve_selection_set(v, &ctx_selection_set) 62 | .await 63 | .map(Some); 64 | } 65 | None => return Ok(None), 66 | } 67 | } 68 | 69 | if ctx.item.name == "type" { 70 | let ty = self.ty().await; 71 | let ctx_selection_set = ctx.with_selection_set(&ctx.item.selection_set); 72 | 73 | return SelectionSetResolver::resolve_selection_set(&ty, &ctx_selection_set) 74 | .await 75 | .map(Some); 76 | } 77 | 78 | if ctx.item.name == "defaultValue" { 79 | let is_deprecated = self.default_value().await; 80 | let ctx_selection_set = ctx.with_selection_set(&ctx.item.selection_set); 81 | 82 | return SelectionSetResolver::resolve_selection_set(&is_deprecated, &ctx_selection_set) 83 | .await 84 | .map(Some); 85 | } 86 | Ok(None) 87 | } 88 | fn type_name() -> String { 89 | "__InputValue".to_string() 90 | } 91 | } 92 | 93 | impl<'a> CollectFields for __InputValue<'a> {} 94 | 95 | #[async_trait::async_trait] 96 | impl<'a> SelectionSetResolver for __InputValue<'a> { 97 | async fn resolve_selection_set( 98 | &self, 99 | ctx: &SelectionSetContext<'_>, 100 | ) -> ResolverResult { 101 | resolve_selection_parallelly(ctx, self).await 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/validation/test_utils.rs: -------------------------------------------------------------------------------- 1 | use core::panic; 2 | 3 | use graphql_parser::query::Document; 4 | 5 | use crate::{ 6 | build_schema, 7 | operation::{build_operation, Operation}, 8 | types::schema::Schema, 9 | }; 10 | 11 | use super::visitor::{visit, ValidationContext, ValidationError, Visitor}; 12 | 13 | #[allow(dead_code)] 14 | pub(crate) fn validate<'a, V, F>( 15 | doc: &'a Document<'a, String>, 16 | schema: &'a Schema, 17 | operation: &'a Operation<'a>, 18 | factory: F, 19 | ) -> Result<(), Vec> 20 | where 21 | V: Visitor<'a> + 'a, 22 | F: Fn() -> V, 23 | { 24 | let mut ctx = ValidationContext::new(schema, None, operation); 25 | let mut visitor = factory(); 26 | visit(&mut visitor, &mut ctx, doc, None); 27 | 28 | if ctx.errors.is_empty() { 29 | Ok(()) 30 | } else { 31 | Err(ctx.errors) 32 | } 33 | } 34 | 35 | #[macro_export] 36 | macro_rules! check_passes_rule { 37 | ($query_doc: expr, $factory: expr $(,)?) => { 38 | let schema = &crate::validation::test_utils::test_schema(); 39 | let doc = &crate::validation::test_utils::parse_test_query($query_doc); 40 | let operation = crate::validation::test_utils::build_test_operation(doc); 41 | crate::validation::test_utils::assert_passes_rule(doc, schema, &operation, $factory); 42 | }; 43 | } 44 | #[macro_export] 45 | macro_rules! check_fails_rule { 46 | ($query_doc: expr, $factory: expr $(,)?) => { 47 | let schema = &crate::validation::test_utils::test_schema(); 48 | let doc = &crate::validation::test_utils::parse_test_query($query_doc); 49 | let operation = crate::validation::test_utils::build_test_operation(doc); 50 | crate::validation::test_utils::assert_fails_rule(doc, schema, &operation, $factory); 51 | }; 52 | } 53 | 54 | #[allow(dead_code)] 55 | pub(crate) fn assert_passes_rule<'a, V, F>( 56 | doc: &'a Document<'a, String>, 57 | schema: &'a Schema, 58 | operation: &'a Operation<'a>, 59 | factory: F, 60 | ) where 61 | V: Visitor<'a> + 'a, 62 | F: Fn() -> V, 63 | { 64 | if let Err(errors) = validate(doc, schema, operation, factory) { 65 | for err in errors { 66 | if let Some(pos) = err.locations.first() { 67 | println!("[{}:{}]", pos.line, pos.column); 68 | } 69 | println!("{}", err.message); 70 | } 71 | panic!("The rule passes, but errors found"); 72 | } 73 | } 74 | 75 | #[allow(dead_code)] 76 | pub(crate) fn assert_fails_rule<'a, V, F>( 77 | doc: &'a Document<'a, String>, 78 | schema: &'a Schema, 79 | operation: &'a Operation<'a>, 80 | factory: F, 81 | ) where 82 | V: Visitor<'a> + 'a, 83 | F: Fn() -> V, 84 | { 85 | if validate(doc, schema, operation, factory).is_ok() { 86 | panic!("should fail, but the rule passes"); 87 | } 88 | } 89 | 90 | #[allow(dead_code)] 91 | pub(crate) fn test_schema() -> Schema { 92 | let contents = std::fs::read_to_string("tests/schemas/validation_test.graphql").unwrap(); 93 | build_schema(&[contents.as_str()], Default::default()).unwrap() 94 | } 95 | 96 | #[allow(dead_code)] 97 | pub(crate) fn parse_test_query(query_doc: &str) -> Document<'_, String> { 98 | graphql_parser::parse_query::(query_doc).unwrap() 99 | } 100 | 101 | #[allow(dead_code)] 102 | pub(crate) fn build_test_operation<'a>(doc: &'a Document<'a, String>) -> Operation<'a> { 103 | build_operation(doc, None, Default::default()).unwrap() 104 | } 105 | -------------------------------------------------------------------------------- /tests/union.rs: -------------------------------------------------------------------------------- 1 | use rusty_gql::*; 2 | 3 | #[tokio::test] 4 | pub async fn test_union() { 5 | struct Query; 6 | struct Person { 7 | name: String, 8 | description: Option, 9 | age: i32, 10 | } 11 | 12 | #[GqlType] 13 | impl Person { 14 | async fn name(&self) -> String { 15 | self.name.clone() 16 | } 17 | async fn description(&self) -> Option { 18 | self.description.clone() 19 | } 20 | async fn age(&self) -> i32 { 21 | self.age 22 | } 23 | } 24 | 25 | struct Dog { 26 | name: String, 27 | woofs: bool, 28 | } 29 | 30 | #[GqlType] 31 | impl Dog { 32 | async fn name(&self) -> String { 33 | self.name.clone() 34 | } 35 | async fn woofs(&self) -> bool { 36 | self.woofs 37 | } 38 | } 39 | 40 | struct Cat { 41 | name: String, 42 | meow: bool, 43 | } 44 | 45 | #[GqlType] 46 | impl Cat { 47 | async fn name(&self) -> String { 48 | self.name.clone() 49 | } 50 | async fn meow(&self) -> bool { 51 | self.meow 52 | } 53 | } 54 | 55 | #[derive(GqlUnion)] 56 | enum SearchAnimal { 57 | Person(Person), 58 | Dog(Dog), 59 | Cat(Cat), 60 | } 61 | 62 | #[GqlType] 63 | impl Query { 64 | async fn search_animal(&self, query: String) -> Option { 65 | if query.as_str() == "dog" { 66 | return Some(SearchAnimal::Dog(Dog { 67 | name: "Pochi".to_string(), 68 | woofs: true, 69 | })); 70 | } else if query.as_str() == "cat" { 71 | return Some(SearchAnimal::Cat(Cat { 72 | name: "Tama".to_string(), 73 | meow: true, 74 | })); 75 | } else if query.as_str() == "person" { 76 | return Some(SearchAnimal::Person(Person { 77 | name: "Tom".to_string(), 78 | description: None, 79 | age: 20, 80 | })); 81 | } 82 | None 83 | } 84 | } 85 | let contents = schema_content("./tests/schemas/union.graphql"); 86 | 87 | let container = Container::new( 88 | &vec![contents.as_str()], 89 | Query, 90 | EmptyMutation, 91 | EmptySubscription, 92 | Default::default(), 93 | ) 94 | .unwrap(); 95 | 96 | let query_doc = r#"{ search_animal(query: "dog") { 97 | ... on Dog { 98 | name 99 | woofs 100 | } 101 | ... on Cat { 102 | name 103 | meows 104 | } 105 | ... on Person { 106 | name 107 | age 108 | } 109 | }}"#; 110 | let req = build_test_request(query_doc, None, Default::default()); 111 | let expected_response = r#"{"data":{"search_animal":{"name":"Pochi","woofs":true}}}"#; 112 | check_gql_response(req, expected_response, &container).await; 113 | 114 | let query_doc = r#"{ search_animal(query: "person") { 115 | ... on Person { 116 | name 117 | age 118 | } 119 | }}"#; 120 | let req = build_test_request(query_doc, None, Default::default()); 121 | let expected_response = r#"{"data":{"search_animal":{"age":20,"name":"Tom"}}}"#; 122 | check_gql_response(req, expected_response, &container).await; 123 | } 124 | -------------------------------------------------------------------------------- /cli/src/code_generate/mod.rs: -------------------------------------------------------------------------------- 1 | mod directive; 2 | mod mod_file; 3 | mod operation; 4 | mod project; 5 | mod root_mod_file; 6 | mod type_definition; 7 | mod util; 8 | 9 | use std::io::Error; 10 | 11 | use futures_util::future::try_join_all; 12 | use rusty_gql::{build_schema, OperationType}; 13 | 14 | use self::{ 15 | directive::create_directive_files, operation::create_operation_files, 16 | root_mod_file::RootModFile, type_definition::create_type_definition_files, 17 | util::get_interface_impl_object_map, 18 | }; 19 | 20 | pub use project::create_project_files; 21 | use tokio::io::AsyncWriteExt; 22 | 23 | #[async_trait::async_trait] 24 | pub(crate) trait CreateFile { 25 | async fn create_file(&self) -> Result<(), std::io::Error>; 26 | } 27 | pub(crate) trait FileDefinition { 28 | fn name(&self) -> String; 29 | 30 | fn path(&self) -> String; 31 | 32 | fn content(&self) -> String; 33 | } 34 | 35 | pub(crate) async fn create_file(file_def: T) -> Result<(), Error> { 36 | let path = file_def.path(); 37 | if tokio::fs::File::open(&path).await.is_err() { 38 | let mut file = tokio::fs::File::create(&path).await?; 39 | file.write(file_def.content().as_bytes()).await?; 40 | Ok(()) 41 | } else if file_def.name() == *"mod.rs" { 42 | let mut file = tokio::fs::File::create(&path).await?; 43 | file.write(file_def.content().as_bytes()).await?; 44 | Ok(()) 45 | } else { 46 | Ok(()) 47 | } 48 | } 49 | 50 | pub(crate) fn path_str(paths: Vec<&str>, is_file: bool) -> String { 51 | if is_file { 52 | let path_str = paths.join("/"); 53 | format!("{}.rs", path_str) 54 | } else { 55 | paths.join("/") 56 | } 57 | } 58 | 59 | pub(crate) async fn create_gql_files(schema_documents: &[&str], path: &str) -> Result<(), Error> { 60 | let schema = match build_schema(schema_documents, Default::default()) { 61 | Ok(v) => v, 62 | Err(err) => return Err(Error::new(std::io::ErrorKind::InvalidInput, err.message)), 63 | }; 64 | 65 | create_root_dirs(path).await?; 66 | create_root_mod_file(path).await?; 67 | 68 | let query_task = create_operation_files(&schema.queries, OperationType::Query, path); 69 | let mutation_task = create_operation_files(&schema.mutations, OperationType::Mutation, path); 70 | 71 | try_join_all(vec![query_task, mutation_task]).await?; 72 | 73 | let interface_obj_maps = get_interface_impl_object_map(&schema.type_definitions); 74 | create_type_definition_files(&schema, path, &interface_obj_maps).await?; 75 | create_directive_files(&schema.directives, path).await?; 76 | Ok(()) 77 | } 78 | 79 | fn gql_file_types() -> Vec { 80 | vec![ 81 | "query".to_string(), 82 | "mutation".to_string(), 83 | "resolver".to_string(), 84 | "directive".to_string(), 85 | "scalar".to_string(), 86 | "input".to_string(), 87 | ] 88 | } 89 | async fn create_root_mod_file(path: &str) -> tokio::io::Result<()> { 90 | let filenames = gql_file_types(); 91 | create_file(RootModFile { path, filenames }).await 92 | } 93 | 94 | async fn create_root_dirs(path: &str) -> Result, Error> { 95 | let mut futures = Vec::new(); 96 | for name in gql_file_types() { 97 | futures.push(tokio::fs::create_dir_all(format!("{}/{}", path, name))); 98 | } 99 | try_join_all(futures).await 100 | } 101 | 102 | pub(crate) fn use_gql_definitions() -> &'static str { 103 | r#"#![allow(warnings, unused)] 104 | use crate::graphql::*; 105 | use rusty_gql::*;"# 106 | } 107 | -------------------------------------------------------------------------------- /src/validation/rules/fields_on_correct_type.rs: -------------------------------------------------------------------------------- 1 | use graphql_parser::query::Field; 2 | 3 | use crate::validation::visitor::{ValidationContext, Visitor}; 4 | 5 | #[derive(Default)] 6 | pub struct FieldsOnCorrectType; 7 | 8 | impl<'a> Visitor<'a> for FieldsOnCorrectType { 9 | fn enter_field(&mut self, ctx: &mut ValidationContext, field: &'a Field<'a, String>) { 10 | if let Some(parent_type) = ctx.parent_type() { 11 | if field.name == "__typename" || field.name == "__type" || field.name == "__schema" { 12 | return; 13 | } 14 | 15 | if parent_type.get_field_by_name(&field.name).is_none() { 16 | ctx.add_error( 17 | format!( 18 | "Unknown field \"{}\" on type \"{}\"", 19 | field.name, 20 | parent_type.name() 21 | ), 22 | vec![field.position], 23 | ) 24 | } 25 | } 26 | } 27 | } 28 | 29 | #[cfg(test)] 30 | mod tests { 31 | use crate::{check_fails_rule, check_passes_rule}; 32 | 33 | use super::*; 34 | 35 | pub fn factory() -> FieldsOnCorrectType { 36 | FieldsOnCorrectType::default() 37 | } 38 | 39 | #[test] 40 | fn object_selection() { 41 | let query_doc = r#" 42 | fragment objectFieldSelection on Human { 43 | __typename 44 | name 45 | } 46 | { __typename } 47 | "#; 48 | check_passes_rule!(query_doc, factory); 49 | } 50 | 51 | #[test] 52 | fn interface_unknown_field() { 53 | let query_doc = r#" 54 | fragment unknownField on Character { 55 | unknownField 56 | } 57 | { __typename } 58 | "#; 59 | check_fails_rule!(query_doc, factory); 60 | } 61 | 62 | #[test] 63 | fn nested_unknown_fields() { 64 | let query_doc = r#" 65 | fragment unknownField on Character { 66 | unknownField { 67 | ... on Human { 68 | unknown_human_field 69 | } 70 | } 71 | } 72 | { __typename } 73 | "#; 74 | check_fails_rule!(query_doc, factory); 75 | } 76 | 77 | #[test] 78 | fn unknown_sub_fields() { 79 | let query_doc = r#" 80 | fragment unknownSubField on Character { 81 | friends { 82 | unknownField 83 | } 84 | } 85 | { __typename } 86 | "#; 87 | check_fails_rule!(query_doc, factory); 88 | } 89 | 90 | #[test] 91 | fn union_typename() { 92 | let query_doc = r#" 93 | fragment objectSelection on SearchResult { 94 | __typename 95 | ... on Human { 96 | name 97 | } 98 | ... on Droid { 99 | name 100 | } 101 | } 102 | { __typename } 103 | "#; 104 | check_passes_rule!(query_doc, factory); 105 | } 106 | 107 | #[test] 108 | fn union_field_name() { 109 | let query_doc = r#" 110 | fragment objectSelection on SearchResult { 111 | name 112 | } 113 | { __typename } 114 | "#; 115 | check_fails_rule!(query_doc, factory); 116 | } 117 | 118 | #[test] 119 | fn union_meta_field() { 120 | let query_doc = r#" 121 | fragment objectSelection on SearchResult { 122 | __typename 123 | } 124 | { __typename } 125 | "#; 126 | check_passes_rule!(query_doc, factory); 127 | } 128 | } 129 | --------------------------------------------------------------------------------