From 17a3ad9620f1f97da4d00899233fc208bffda345 Mon Sep 17 00:00:00 2001 From: Giri Priyadarshan <43517605+giripriyadarshan@users.noreply.github.com> Date: Tue, 5 Apr 2022 18:05:33 +0530 Subject: [PATCH] tonic gRPC example (#659) * tonic gRPC example * minor change to client output Co-authored-by: Chris Tsang --- .github/workflows/rust.yml | 2 +- examples/tonic_grpc_example/Cargo.toml | 32 +++++ examples/tonic_grpc_example/README.md | 15 ++ examples/tonic_grpc_example/build.rs | 10 ++ examples/tonic_grpc_example/entity/Cargo.toml | 25 ++++ examples/tonic_grpc_example/entity/src/lib.rs | 3 + .../tonic_grpc_example/entity/src/post.rs | 18 +++ .../tonic_grpc_example/migration/Cargo.toml | 13 ++ .../tonic_grpc_example/migration/README.md | 37 +++++ .../tonic_grpc_example/migration/src/lib.rs | 12 ++ .../src/m20220120_000001_create_post_table.rs | 39 ++++++ .../tonic_grpc_example/migration/src/main.rs | 7 + examples/tonic_grpc_example/proto/post.proto | 25 ++++ examples/tonic_grpc_example/src/client.rs | 28 ++++ examples/tonic_grpc_example/src/lib.rs | 3 + examples/tonic_grpc_example/src/server.rs | 132 ++++++++++++++++++ 16 files changed, 400 insertions(+), 1 deletion(-) create mode 100644 examples/tonic_grpc_example/Cargo.toml create mode 100644 examples/tonic_grpc_example/README.md create mode 100644 examples/tonic_grpc_example/build.rs create mode 100644 examples/tonic_grpc_example/entity/Cargo.toml create mode 100644 examples/tonic_grpc_example/entity/src/lib.rs create mode 100644 examples/tonic_grpc_example/entity/src/post.rs create mode 100644 examples/tonic_grpc_example/migration/Cargo.toml create mode 100644 examples/tonic_grpc_example/migration/README.md create mode 100644 examples/tonic_grpc_example/migration/src/lib.rs create mode 100644 examples/tonic_grpc_example/migration/src/m20220120_000001_create_post_table.rs create mode 100644 examples/tonic_grpc_example/migration/src/main.rs create mode 100644 examples/tonic_grpc_example/proto/post.proto create mode 100644 examples/tonic_grpc_example/src/client.rs create mode 100644 examples/tonic_grpc_example/src/lib.rs create mode 100644 examples/tonic_grpc_example/src/server.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f9d7280f..24cc1c96 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -293,7 +293,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - path: [basic, actix_example, actix3_example, axum_example, graphql_example, rocket_example, poem_example, jsonrpsee_example] + path: [basic, actix_example, actix3_example, axum_example, graphql_example, rocket_example, poem_example, jsonrpsee_example, tonic_grpc_example] steps: - uses: actions/checkout@v2 diff --git a/examples/tonic_grpc_example/Cargo.toml b/examples/tonic_grpc_example/Cargo.toml new file mode 100644 index 00000000..d7b21933 --- /dev/null +++ b/examples/tonic_grpc_example/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "tonic_grpc_example" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[workspace] +members = [".", "entity", "migration"] + +[dependencies] +tonic = "0.7" +tokio = { version = "1.17", features = ["macros", "rt-multi-thread", "full"] } +entity = { path = "entity" } +migration = { path = "migration" } +sea-orm = { version = "0.7.1", features = [ "sqlx-postgres", "runtime-tokio-rustls", "macros" ], default-features = false } +prost = "0.10.0" +serde = "1.0" + +[lib] +path = "./src/lib.rs" + +[[bin]] +name="server" +path="./src/server.rs" + +[[bin]] +name="client" +path="./src/client.rs" + +[build-dependencies] +tonic-build = "0.7" diff --git a/examples/tonic_grpc_example/README.md b/examples/tonic_grpc_example/README.md new file mode 100644 index 00000000..4356cecf --- /dev/null +++ b/examples/tonic_grpc_example/README.md @@ -0,0 +1,15 @@ +# Tonic.rs + gRPC + SeaORM + +Simple implementation of gRPC using SeaORM. + +uses models actix_example + +run server using +```bash +cargo run --bin server +``` + +run client using +```bash +cargo run --bin client +``` \ No newline at end of file diff --git a/examples/tonic_grpc_example/build.rs b/examples/tonic_grpc_example/build.rs new file mode 100644 index 00000000..4534705a --- /dev/null +++ b/examples/tonic_grpc_example/build.rs @@ -0,0 +1,10 @@ +fn main() { + let proto_file = "./proto/post.proto"; + + tonic_build::configure() + .build_server(true) + .compile(&[proto_file], &["."]) + .unwrap_or_else(|e| panic!("protobuf compile error: {}", e)); + + println!("cargo:rerun-if-changed={}", proto_file); +} diff --git a/examples/tonic_grpc_example/entity/Cargo.toml b/examples/tonic_grpc_example/entity/Cargo.toml new file mode 100644 index 00000000..14890041 --- /dev/null +++ b/examples/tonic_grpc_example/entity/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "entity" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +name = "entity" +path = "src/lib.rs" + +[dependencies] +serde = { version = "1", features = ["derive"] } + +[dependencies.sea-orm] +# path = "../../../" # remove this line in your own project +version = "^0.7.0" +features = [ + "macros", + "debug-print", + "runtime-tokio-rustls", + # "sqlx-mysql", + "sqlx-postgres", + # "sqlx-sqlite", +] +default-features = false \ No newline at end of file diff --git a/examples/tonic_grpc_example/entity/src/lib.rs b/examples/tonic_grpc_example/entity/src/lib.rs new file mode 100644 index 00000000..263f05b4 --- /dev/null +++ b/examples/tonic_grpc_example/entity/src/lib.rs @@ -0,0 +1,3 @@ +pub mod post; + +pub use sea_orm; diff --git a/examples/tonic_grpc_example/entity/src/post.rs b/examples/tonic_grpc_example/entity/src/post.rs new file mode 100644 index 00000000..37a99c2f --- /dev/null +++ b/examples/tonic_grpc_example/entity/src/post.rs @@ -0,0 +1,18 @@ +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize)] +#[sea_orm(table_name = "posts")] +pub struct Model { + #[sea_orm(primary_key)] + #[serde(skip_deserializing)] + pub id: i32, + pub title: String, + #[sea_orm(column_type = "Text")] + pub text: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/tonic_grpc_example/migration/Cargo.toml b/examples/tonic_grpc_example/migration/Cargo.toml new file mode 100644 index 00000000..89174a73 --- /dev/null +++ b/examples/tonic_grpc_example/migration/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "migration" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +name = "migration" +path = "src/lib.rs" + +[dependencies] +sea-schema = { version = "^0.7.0", default-features = false, features = [ "migration", "debug-print" ] } +entity = { path = "../entity" } diff --git a/examples/tonic_grpc_example/migration/README.md b/examples/tonic_grpc_example/migration/README.md new file mode 100644 index 00000000..963caaeb --- /dev/null +++ b/examples/tonic_grpc_example/migration/README.md @@ -0,0 +1,37 @@ +# Running Migrator CLI + +- Apply all pending migrations + ```sh + cargo run + ``` + ```sh + cargo run -- up + ``` +- Apply first 10 pending migrations + ```sh + cargo run -- up -n 10 + ``` +- Rollback last applied migrations + ```sh + cargo run -- down + ``` +- Rollback last 10 applied migrations + ```sh + cargo run -- down -n 10 + ``` +- Drop all tables from the database, then reapply all migrations + ```sh + cargo run -- fresh + ``` +- Rollback all applied migrations, then reapply all migrations + ```sh + cargo run -- refresh + ``` +- Rollback all applied migrations + ```sh + cargo run -- reset + ``` +- Check the status of all migrations + ```sh + cargo run -- status + ``` diff --git a/examples/tonic_grpc_example/migration/src/lib.rs b/examples/tonic_grpc_example/migration/src/lib.rs new file mode 100644 index 00000000..3679d81f --- /dev/null +++ b/examples/tonic_grpc_example/migration/src/lib.rs @@ -0,0 +1,12 @@ +pub use sea_schema::migration::prelude::*; + +mod m20220120_000001_create_post_table; + +pub struct Migrator; + +#[async_trait::async_trait] +impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![Box::new(m20220120_000001_create_post_table::Migration)] + } +} diff --git a/examples/tonic_grpc_example/migration/src/m20220120_000001_create_post_table.rs b/examples/tonic_grpc_example/migration/src/m20220120_000001_create_post_table.rs new file mode 100644 index 00000000..0fe872c4 --- /dev/null +++ b/examples/tonic_grpc_example/migration/src/m20220120_000001_create_post_table.rs @@ -0,0 +1,39 @@ +use entity::post::*; +use sea_schema::migration::prelude::*; + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m20220120_000001_create_post_table" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(Entity) + .if_not_exists() + .col( + ColumnDef::new(Column::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(Column::Title).string().not_null()) + .col(ColumnDef::new(Column::Text).string().not_null()) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Entity).to_owned()) + .await + } +} diff --git a/examples/tonic_grpc_example/migration/src/main.rs b/examples/tonic_grpc_example/migration/src/main.rs new file mode 100644 index 00000000..7e5e996d --- /dev/null +++ b/examples/tonic_grpc_example/migration/src/main.rs @@ -0,0 +1,7 @@ +use migration::Migrator; +use sea_schema::migration::*; + +#[async_std::main] +async fn main() { + cli::run_cli(Migrator).await; +} diff --git a/examples/tonic_grpc_example/proto/post.proto b/examples/tonic_grpc_example/proto/post.proto new file mode 100644 index 00000000..69aad41a --- /dev/null +++ b/examples/tonic_grpc_example/proto/post.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; + +package Post; + +service Blogpost { + rpc GetPosts(PostPerPage) returns (PostList) {} + rpc AddPost(Post) returns (PostId) {} + rpc UpdatePost(Post) returns (ProcessStatus) {} + rpc DeletePost(PostId) returns (ProcessStatus) {} + rpc GetPostById(PostId) returns (Post) {} +} + +message PostPerPage { uint64 per_page = 1; } + +message ProcessStatus { bool success = 1; } + +message PostId { int32 id = 1; } + +message Post { + int32 id = 1; + string title = 2; + string content = 3; +} + +message PostList { repeated Post post = 1; } \ No newline at end of file diff --git a/examples/tonic_grpc_example/src/client.rs b/examples/tonic_grpc_example/src/client.rs new file mode 100644 index 00000000..270ae86e --- /dev/null +++ b/examples/tonic_grpc_example/src/client.rs @@ -0,0 +1,28 @@ +use tonic::transport::Endpoint; +use tonic::Request; + +use tonic_grpc_example::post::{blogpost_client::BlogpostClient, PostPerPage}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let addr = Endpoint::from_static("http://0.0.0.0:50051"); + + /* + Client code is not implemented in completely + as it would just make the code base look too complicated .... + and interface requires a lot of boilerplate code to implement. + + But a basic implementation is given below .... + please refer it to implement other ways to make your code pretty + */ + + let mut client = BlogpostClient::connect(addr).await?; + let request = Request::new(PostPerPage { per_page: 10 }); + let response = client.get_posts(request).await?; + + for post in response.into_inner().post.iter() { + println!("{:?}", post); + } + + Ok(()) +} diff --git a/examples/tonic_grpc_example/src/lib.rs b/examples/tonic_grpc_example/src/lib.rs new file mode 100644 index 00000000..cd202896 --- /dev/null +++ b/examples/tonic_grpc_example/src/lib.rs @@ -0,0 +1,3 @@ +pub mod post { + tonic::include_proto!("post"); +} diff --git a/examples/tonic_grpc_example/src/server.rs b/examples/tonic_grpc_example/src/server.rs new file mode 100644 index 00000000..2ef00e02 --- /dev/null +++ b/examples/tonic_grpc_example/src/server.rs @@ -0,0 +1,132 @@ +use tonic::transport::Server; +use tonic::{Request, Response, Status}; + +use tonic_grpc_example::post::{ + blogpost_server::{Blogpost, BlogpostServer}, + Post, PostId, PostList, PostPerPage, ProcessStatus, +}; + +use entity::{ + post::{self, Entity as PostEntity}, + sea_orm::{entity::*, query::*, DatabaseConnection}, +}; +use migration::{Migrator, MigratorTrait}; + +use std::env; + +#[derive(Default)] +pub struct MyServer { + connection: DatabaseConnection, +} + +#[tonic::async_trait] +impl Blogpost for MyServer { + async fn get_posts(&self, request: Request) -> Result, Status> { + let mut response = PostList { post: Vec::new() }; + + let posts = PostEntity::find() + .order_by_asc(post::Column::Id) + .limit(request.into_inner().per_page) + .all(&self.connection) + .await + .unwrap(); + + for post in posts { + response.post.push(Post { + id: post.id, + title: post.title, + content: post.text, + }); + } + + Ok(Response::new(response)) + } + + async fn add_post(&self, request: Request) -> Result, Status> { + let input = request.into_inner(); + let insert_details = post::ActiveModel { + title: Set(input.title.clone()), + text: Set(input.content.clone()), + ..Default::default() + }; + + let response = PostId { + id: insert_details.insert(&self.connection).await.unwrap().id, + }; + + Ok(Response::new(response)) + } + + async fn update_post(&self, request: Request) -> Result, Status> { + let input = request.into_inner(); + let mut update_post: post::ActiveModel = PostEntity::find_by_id(input.id) + .one(&self.connection) + .await + .unwrap() + .unwrap() + .into(); + + update_post.title = Set(input.title.clone()); + update_post.text = Set(input.content.clone()); + + let update = update_post.update(&self.connection).await; + + match update { + Ok(_) => Ok(Response::new(ProcessStatus { success: true })), + Err(_) => Ok(Response::new(ProcessStatus { success: false })), + } + } + + async fn delete_post( + &self, + request: Request, + ) -> Result, Status> { + let delete_post: post::ActiveModel = PostEntity::find_by_id(request.into_inner().id) + .one(&self.connection) + .await + .unwrap() + .unwrap() + .into(); + + let status = delete_post.delete(&self.connection).await; + + match status { + Ok(_) => Ok(Response::new(ProcessStatus { success: true })), + Err(_) => Ok(Response::new(ProcessStatus { success: false })), + } + } + + async fn get_post_by_id(&self, request: Request) -> Result, Status> { + let post = PostEntity::find_by_id(request.into_inner().id) + .one(&self.connection) + .await + .unwrap() + .unwrap(); + + let response = Post { + id: post.id, + title: post.title, + content: post.text, + }; + Ok(Response::new(response)) + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let addr = "0.0.0.0:50051".parse()?; + + let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); + + // establish database connection + let connection = sea_orm::Database::connect(&database_url).await?; + Migrator::up(&connection, None).await?; + + let hello_server = MyServer { connection }; + Server::builder() + .add_service(BlogpostServer::new(hello_server)) + .serve(addr) + .await?; + + Ok(()) +}