diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index c4964722..8993a56b 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -72,7 +72,7 @@ jobs: with: command: clippy args: "-- -D warnings" - integration-sqlite: + integration-sqlite-poem: name: SQLite integration tests runs-on: ubuntu-latest needs: @@ -94,7 +94,36 @@ jobs: command: run args: > --package seaography-cli -- - sqlite://sakila.db seaography-sqlite-example ./examples/sqlite + -f poem sqlite://sakila.db seaography-sqlite-example ./examples/sqlite + - name: Depends on local seaography + run: sed -i '/^\[dependencies.seaography\]$/a \path = "..\/..\/"' ./examples/sqlite/Cargo.toml + - name: Integration tests + working-directory: ./examples/sqlite + run: cargo test + + integration-sqlite-actix: + name: SQLite integration tests + runs-on: ubuntu-latest + needs: + - check + - test + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + - name: Remove generated folder + run: rm -rf ./examples/sqlite/src + - name: Copy sample database + run: cp ./examples/sqlite/sakila.db . + - uses: actions-rs/cargo@v1 + with: + command: run + args: > + --package seaography-cli -- + -f actix sqlite://sakila.db seaography-sqlite-example ./examples/sqlite - name: Depends on local seaography run: sed -i '/^\[dependencies.seaography\]$/a \path = "..\/..\/"' ./examples/sqlite/Cargo.toml - name: Integration tests diff --git a/cli/src/main.rs b/cli/src/main.rs index ed1d6aca..653a75a0 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,5 +1,5 @@ use clap::Parser; -use seaography_generator::write_project; +use seaography_generator::{write_project, WebFrameworkEnum}; #[derive(clap::Parser)] #[clap(author, version, about, long_about = None)] @@ -27,6 +27,9 @@ pub struct Args { #[clap(short, long)] pub hidden_tables: Option, + + #[clap(short, long)] + pub framework: Option, } /** @@ -140,6 +143,7 @@ async fn main() { expanded_format, tables, sql_library, + args.framework.unwrap_or_else(|| WebFrameworkEnum::Poem), args.depth_limit, args.complexity_limit, ) diff --git a/examples/sqlite/Cargo.toml b/examples/sqlite/Cargo.toml index 8bd1c1b6..d36916c6 100644 --- a/examples/sqlite/Cargo.toml +++ b/examples/sqlite/Cargo.toml @@ -4,11 +4,11 @@ name = 'seaography-sqlite-example' version = '0.2.0' [dependencies] +poem = { version = "1.3.29" } async-graphql = { version = "4.0.14", features = ["decimal", "chrono", "dataloader"] } async-graphql-poem = { version = "4.0.14" } async-trait = { version = "0.1.53" } dotenv = "0.15.0" -poem = { version = "1.3.29" } sea-orm = { version = "^0.9", features = ["sqlx-sqlite", "runtime-async-std-native-tls"] } tokio = { version = "1.17.0", features = ["macros", "rt-multi-thread"] } tracing = { version = "0.1.34" } diff --git a/examples/sqlite/src/main.rs b/examples/sqlite/src/main.rs index 2bfe1430..fcc7285a 100644 --- a/examples/sqlite/src/main.rs +++ b/examples/sqlite/src/main.rs @@ -1,12 +1,12 @@ +use actix_web::{guard, web, web::Data, App, HttpResponse, HttpServer, Result}; use async_graphql::{ dataloader::DataLoader, http::{playground_source, GraphQLPlaygroundConfig}, EmptyMutation, EmptySubscription, Schema, }; -use async_graphql_poem::GraphQL; +use async_graphql_actix_web::{GraphQLRequest, GraphQLResponse}; use dotenv::dotenv; use lazy_static::lazy_static; -use poem::{get, handler, listener::TcpListener, web::Html, IntoResponse, Route, Server}; use sea_orm::Database; use seaography_sqlite_example::*; use std::env; @@ -25,19 +25,27 @@ lazy_static! { }); } -#[handler] -async fn graphql_playground() -> impl IntoResponse { - Html(playground_source(GraphQLPlaygroundConfig::new(&*ENDPOINT))) +type AppSchema = Schema; + +async fn index(schema: web::Data, req: GraphQLRequest) -> GraphQLResponse { + schema.execute(req.into_inner()).await.into() +} + +async fn graphql_playground() -> Result { + Ok(HttpResponse::Ok() + .content_type("text/html; charset=utf-8") + .body(playground_source(GraphQLPlaygroundConfig::new( + "http://localhost:8000", + )))) } -#[tokio::main] -async fn main() { +#[actix_web::main] +async fn main() -> std::io::Result<()> { dotenv().ok(); tracing_subscriber::fmt() .with_max_level(tracing::Level::INFO) .with_test_writer() .init(); - let database = Database::connect(&*DATABASE_URL) .await .expect("Fail to initialize database connection"); @@ -57,14 +65,18 @@ async fn main() { schema = schema.limit_complexity(complexity); } let schema = schema.finish(); - let app = Route::new().at( - &*ENDPOINT, - get(graphql_playground).post(GraphQL::new(schema)), - ); - println!("Visit GraphQL Playground at http://{}", *URL); - Server::new(TcpListener::bind(&*URL)) - .run(app) - .await - .expect("Fail to start web server"); + HttpServer::new(move || { + App::new() + .app_data(Data::new(schema.clone())) + .service(web::resource("/").guard(guard::Post()).to(index)) + .service( + web::resource("/") + .guard(guard::Get()) + .to(graphql_playground), + ) + }) + .bind("127.0.0.1:8000")? + .run() + .await } diff --git a/generator/src/lib.rs b/generator/src/lib.rs index e0b0ddc8..2249d3f8 100644 --- a/generator/src/lib.rs +++ b/generator/src/lib.rs @@ -4,10 +4,41 @@ pub mod error; pub use error::{Error, Result}; pub mod inject_graphql; pub mod sea_orm_codegen; +pub mod templates; pub mod writer; mod util; +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum WebFrameworkEnum { + Actix, + Poem, +} + +impl std::str::FromStr for WebFrameworkEnum { + type Err = String; + + fn from_str(input: &str) -> std::result::Result { + match input { + "actix" => Ok(Self::Actix), + "poem" => Ok(Self::Poem), + _ => Err(format!( + "Invalid framework '{}', 'actix' and 'poem' are supported!", + input + )), + } + } +} + +impl std::fmt::Display for WebFrameworkEnum { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + WebFrameworkEnum::Actix => f.write_str("actix"), + WebFrameworkEnum::Poem => f.write_str("poem"), + } + } +} + pub async fn write_project>( path: &P, db_url: &str, @@ -15,12 +46,13 @@ pub async fn write_project>( expanded_format: bool, tables: std::collections::BTreeMap, sql_library: &str, + framework: WebFrameworkEnum, depth_limit: Option, complexity_limit: Option, ) -> Result<()> { std::fs::create_dir_all(&path.as_ref().join("src/entities"))?; - writer::write_cargo_toml(path, crate_name, &sql_library)?; + writer::write_cargo_toml(path, crate_name, &sql_library, framework)?; let src_path = &path.as_ref().join("src"); @@ -32,7 +64,14 @@ pub async fn write_project>( writer::write_query_root(src_path, &entities_hashmap).unwrap(); writer::write_lib(src_path)?; - writer::write_main(src_path, crate_name)?; + + match framework { + WebFrameworkEnum::Actix => { + crate::templates::actix::write_main(src_path, crate_name).unwrap() + } + WebFrameworkEnum::Poem => crate::templates::poem::write_main(src_path, crate_name).unwrap(), + } + writer::write_env(&path.as_ref(), db_url, depth_limit, complexity_limit)?; sea_orm_codegen::write_entities(&src_path.join("entities"), entities_hashmap).unwrap(); diff --git a/generator/src/templates/actix.rs b/generator/src/templates/actix.rs new file mode 100644 index 00000000..8896b8e8 --- /dev/null +++ b/generator/src/templates/actix.rs @@ -0,0 +1,103 @@ +use proc_macro2::TokenStream; +use quote::quote; + +use crate::util::add_line_break; + +/// +/// Used to generate project/src/main.rs file content +/// +pub fn generate_main(crate_name: &str) -> TokenStream { + let crate_name_token: TokenStream = crate_name.replace('-', "_").parse().unwrap(); + + quote! { + use actix_web::{guard, web, web::Data, App, HttpResponse, HttpServer, Result}; + use async_graphql::{ + dataloader::DataLoader, http::{playground_source, GraphQLPlaygroundConfig}, EmptyMutation, EmptySubscription, Schema, + }; + use async_graphql_actix_web::{GraphQLRequest, GraphQLResponse}; + use dotenv::dotenv; + use lazy_static::lazy_static; + use sea_orm::Database; + use #crate_name_token::*; + use std::env; + + lazy_static! { + static ref URL: String = env::var("URL").unwrap_or("0.0.0.0:8000".into()); + static ref ENDPOINT: String = env::var("ENDPOINT").unwrap_or("/".into()); + static ref DATABASE_URL: String = + env::var("DATABASE_URL").expect("DATABASE_URL environment variable not set"); + static ref DEPTH_LIMIT: Option = env::var("DEPTH_LIMIT").map_or(None, |data| Some( + data.parse().expect("DEPTH_LIMIT is not a number") + )); + static ref COMPLEXITY_LIMIT: Option = env::var("COMPLEXITY_LIMIT") + .map_or(None, |data| { + Some(data.parse().expect("COMPLEXITY_LIMIT is not a number")) + }); + } + + type AppSchema = Schema; + + async fn index(schema: web::Data, req: GraphQLRequest) -> GraphQLResponse { + schema.execute(req.into_inner()).await.into() + } + + async fn graphql_playground() -> Result { + Ok(HttpResponse::Ok() + .content_type("text/html; charset=utf-8") + .body( + playground_source(GraphQLPlaygroundConfig::new("http://localhost:8000")) + )) + } + + #[actix_web::main] + async fn main() -> std::io::Result<()> { + dotenv().ok(); + tracing_subscriber::fmt() + .with_max_level(tracing::Level::INFO) + .with_test_writer() + .init(); + + let database = Database::connect(&*DATABASE_URL) + .await + .expect("Fail to initialize database connection"); + let orm_dataloader: DataLoader = DataLoader::new( + OrmDataloader { + db: database.clone(), + }, + tokio::spawn, + ); + let mut schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription) + .data(database) + .data(orm_dataloader); + if let Some(depth) = *DEPTH_LIMIT { + schema = schema.limit_depth(depth); + } + if let Some(complexity) = *COMPLEXITY_LIMIT { + schema = schema.limit_complexity(complexity); + } + let schema = schema.finish(); + + println!("Visit GraphQL Playground at http://{}", *URL); + + HttpServer::new(move || { + App::new() + .app_data(Data::new(schema.clone())) + .service(web::resource("/").guard(guard::Post()).to(index)) + .service(web::resource("/").guard(guard::Get()).to(graphql_playground)) + }) + .bind("127.0.0.1:8000")? + .run() + .await + } + } +} + +pub fn write_main>(path: &P, crate_name: &str) -> std::io::Result<()> { + let tokens = generate_main(crate_name); + + let file_name = path.as_ref().join("main.rs"); + + std::fs::write(file_name, add_line_break(tokens))?; + + Ok(()) +} diff --git a/generator/src/templates/actix_cargo.toml b/generator/src/templates/actix_cargo.toml new file mode 100644 index 00000000..ec99ebbe --- /dev/null +++ b/generator/src/templates/actix_cargo.toml @@ -0,0 +1,26 @@ +[package] +edition = '2021' +name = '' +version = '0.1.0' + +[dependencies] +actix-web = { version = "4.0.1", default-features = false, features = ["macros"] } +async-graphql = { version = "4.0.14", features = ["decimal", "chrono", "dataloader"] } +async-graphql-actix-web = { version = "4.0.14" } +async-trait = { version = "0.1.53" } +dotenv = "0.15.0" +sea-orm = { version = "^0.9", features = ["", "runtime-async-std-native-tls"] } +tokio = { version = "1.17.0", features = ["macros", "rt-multi-thread"] } +tracing = { version = "0.1.34" } +tracing-subscriber = { version = "0.3.11" } +lazy_static = { version = "1.4.0" } + +[dependencies.seaography] +version = "" # seaography version +features = ["with-decimal", "with-chrono"] + +[dev-dependencies] +serde_json = { version = '1.0.82' } + +[workspace] +members = [] \ No newline at end of file diff --git a/generator/src/templates/mod.rs b/generator/src/templates/mod.rs new file mode 100644 index 00000000..fa0b4158 --- /dev/null +++ b/generator/src/templates/mod.rs @@ -0,0 +1,2 @@ +pub mod actix; +pub mod poem; diff --git a/generator/src/templates/poem.rs b/generator/src/templates/poem.rs new file mode 100644 index 00000000..34f30896 --- /dev/null +++ b/generator/src/templates/poem.rs @@ -0,0 +1,94 @@ +use proc_macro2::TokenStream; +use quote::quote; + +use crate::util::add_line_break; + +/// +/// Used to generate project/src/main.rs file content +/// +pub fn generate_main(crate_name: &str) -> TokenStream { + let crate_name_token: TokenStream = crate_name.replace('-', "_").parse().unwrap(); + + quote! { + use async_graphql::{ + dataloader::DataLoader, + http::{playground_source, GraphQLPlaygroundConfig}, + EmptyMutation, EmptySubscription, Schema, + }; + use async_graphql_poem::GraphQL; + use dotenv::dotenv; + use lazy_static::lazy_static; + use poem::{get, handler, listener::TcpListener, web::Html, IntoResponse, Route, Server}; + use sea_orm::Database; + use #crate_name_token::*; + use std::env; + + lazy_static! { + static ref URL: String = env::var("URL").unwrap_or("0.0.0.0:8000".into()); + static ref ENDPOINT: String = env::var("ENDPOINT").unwrap_or("/".into()); + static ref DATABASE_URL: String = + env::var("DATABASE_URL").expect("DATABASE_URL environment variable not set"); + static ref DEPTH_LIMIT: Option = env::var("DEPTH_LIMIT").map_or(None, |data| Some( + data.parse().expect("DEPTH_LIMIT is not a number") + )); + static ref COMPLEXITY_LIMIT: Option = env::var("COMPLEXITY_LIMIT") + .map_or(None, |data| { + Some(data.parse().expect("COMPLEXITY_LIMIT is not a number")) + }); + } + + #[handler] + async fn graphql_playground() -> impl IntoResponse { + Html(playground_source(GraphQLPlaygroundConfig::new(&*ENDPOINT))) + } + + #[tokio::main] + async fn main() { + dotenv().ok(); + tracing_subscriber::fmt() + .with_max_level(tracing::Level::INFO) + .with_test_writer() + .init(); + + let database = Database::connect(&*DATABASE_URL) + .await + .expect("Fail to initialize database connection"); + let orm_dataloader: DataLoader = DataLoader::new( + OrmDataloader { + db: database.clone(), + }, + tokio::spawn, + ); + let mut schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription) + .data(database) + .data(orm_dataloader); + if let Some(depth) = *DEPTH_LIMIT { + schema = schema.limit_depth(depth); + } + if let Some(complexity) = *COMPLEXITY_LIMIT { + schema = schema.limit_complexity(complexity); + } + let schema = schema.finish(); + let app = Route::new().at( + &*ENDPOINT, + get(graphql_playground).post(GraphQL::new(schema)), + ); + + println!("Visit GraphQL Playground at http://{}", *URL); + Server::new(TcpListener::bind(&*URL)) + .run(app) + .await + .expect("Fail to start web server"); + } + } +} + +pub fn write_main>(path: &P, crate_name: &str) -> std::io::Result<()> { + let tokens = generate_main(crate_name); + + let file_name = path.as_ref().join("main.rs"); + + std::fs::write(file_name, add_line_break(tokens))?; + + Ok(()) +} diff --git a/generator/src/_Cargo.toml b/generator/src/templates/poem_cargo.toml similarity index 100% rename from generator/src/_Cargo.toml rename to generator/src/templates/poem_cargo.toml index 093ab1a4..4fbf1679 100644 --- a/generator/src/_Cargo.toml +++ b/generator/src/templates/poem_cargo.toml @@ -4,11 +4,11 @@ name = '' version = '0.2.0' [dependencies] +poem = { version = "1.3.29" } async-graphql = { version = "4.0.14", features = ["decimal", "chrono", "dataloader"] } async-graphql-poem = { version = "4.0.14" } async-trait = { version = "0.1.53" } dotenv = "0.15.0" -poem = { version = "1.3.29" } sea-orm = { version = "^0.9", features = ["", "runtime-async-std-native-tls"] } tokio = { version = "1.17.0", features = ["macros", "rt-multi-thread"] } tracing = { version = "0.1.34" } diff --git a/generator/src/writer.rs b/generator/src/writer.rs index e517a173..ad6f6487 100644 --- a/generator/src/writer.rs +++ b/generator/src/writer.rs @@ -1,7 +1,7 @@ use proc_macro2::TokenStream; use quote::quote; -use crate::util::add_line_break; +use crate::{util::add_line_break, WebFrameworkEnum}; pub fn generate_query_root( entities_hashmap: &crate::sea_orm_codegen::EntityHashMap, @@ -44,6 +44,7 @@ pub fn write_cargo_toml>( path: &P, crate_name: &str, sql_library: &str, + framework: WebFrameworkEnum, ) -> std::io::Result<()> { let file_path = path.as_ref().join("Cargo.toml"); @@ -53,7 +54,12 @@ pub fn write_cargo_toml>( env!("CARGO_PKG_VERSION_MINOR") ); - let content = include_str!("_Cargo.toml") + let content = match framework { + WebFrameworkEnum::Actix => include_str!("./templates/actix_cargo.toml"), + WebFrameworkEnum::Poem => include_str!("./templates/poem_cargo.toml"), + }; + + let content = content .replace("", crate_name) .replace("", sql_library) .replace("", &ver); @@ -91,96 +97,6 @@ pub fn write_lib>(path: &P) -> std::io::Result<()> { Ok(()) } -/// -/// Used to generate project/src/main.rs file content -/// -pub fn generate_main(crate_name: &str) -> TokenStream { - let crate_name_token: TokenStream = crate_name.replace('-', "_").parse().unwrap(); - - quote! { - use async_graphql::{ - dataloader::DataLoader, - http::{playground_source, GraphQLPlaygroundConfig}, - EmptyMutation, EmptySubscription, Schema, - }; - use async_graphql_poem::GraphQL; - use dotenv::dotenv; - use lazy_static::lazy_static; - use poem::{get, handler, listener::TcpListener, web::Html, IntoResponse, Route, Server}; - use sea_orm::Database; - use #crate_name_token::*; - use std::env; - - lazy_static! { - static ref URL: String = env::var("URL").unwrap_or("0.0.0.0:8000".into()); - static ref ENDPOINT: String = env::var("ENDPOINT").unwrap_or("/".into()); - static ref DATABASE_URL: String = - env::var("DATABASE_URL").expect("DATABASE_URL environment variable not set"); - static ref DEPTH_LIMIT: Option = env::var("DEPTH_LIMIT").map_or(None, |data| Some( - data.parse().expect("DEPTH_LIMIT is not a number") - )); - static ref COMPLEXITY_LIMIT: Option = env::var("COMPLEXITY_LIMIT") - .map_or(None, |data| { - Some(data.parse().expect("COMPLEXITY_LIMIT is not a number")) - }); - } - - #[handler] - async fn graphql_playground() -> impl IntoResponse { - Html(playground_source(GraphQLPlaygroundConfig::new(&*ENDPOINT))) - } - - #[tokio::main] - async fn main() { - dotenv().ok(); - tracing_subscriber::fmt() - .with_max_level(tracing::Level::INFO) - .with_test_writer() - .init(); - - let database = Database::connect(&*DATABASE_URL) - .await - .expect("Fail to initialize database connection"); - let orm_dataloader: DataLoader = DataLoader::new( - OrmDataloader { - db: database.clone(), - }, - tokio::spawn, - ); - let mut schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription) - .data(database) - .data(orm_dataloader); - if let Some(depth) = *DEPTH_LIMIT { - schema = schema.limit_depth(depth); - } - if let Some(complexity) = *COMPLEXITY_LIMIT { - schema = schema.limit_complexity(complexity); - } - let schema = schema.finish(); - let app = Route::new().at( - &*ENDPOINT, - get(graphql_playground).post(GraphQL::new(schema)), - ); - - println!("Visit GraphQL Playground at http://{}", *URL); - Server::new(TcpListener::bind(&*URL)) - .run(app) - .await - .expect("Fail to start web server"); - } - } -} - -pub fn write_main>(path: &P, crate_name: &str) -> std::io::Result<()> { - let tokens = generate_main(crate_name); - - let file_name = path.as_ref().join("main.rs"); - - std::fs::write(file_name, add_line_break(tokens))?; - - Ok(()) -} - pub fn write_env>( path: &P, db_url: &str,