diff --git a/build.rs b/build.rs index 4f4fd27..16762ad 100644 --- a/build.rs +++ b/build.rs @@ -1,14 +1,35 @@ use std::path::PathBuf; fn main() { + fn configure_tonic() -> tonic_build::Builder { + tonic_build::configure() + .protoc_arg("--experimental_allow_proto3_optional") + .build_server(true) + .build_client(true) + } + + // Compile news.proto let mut news = PathBuf::from(env!("CARGO_MANIFEST_DIR")); news.push("news.proto"); - tonic_build::compile_protos(news).expect("Failed to compile protos"); + // Configure and compile each proto file with descriptors let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); - tonic_build::configure() + // Compile news.proto + configure_tonic() .file_descriptor_set_path(out_dir.join("news_descriptor.bin")) - .compile(&["news.proto"], &["proto"]) - .unwrap(); + .compile(&["news.proto"], &["."]) + .expect("Failed to compile news.proto"); + + // Compile posts.proto + configure_tonic() + .file_descriptor_set_path(out_dir.join("posts_descriptor.bin")) + .compile(&["posts.proto"], &["."]) + .expect("Failed to compile posts.proto"); + + // Compile users.proto + configure_tonic() + .file_descriptor_set_path(out_dir.join("users_descriptor.bin")) + .compile(&["users.proto"], &["."]) + .expect("Failed to compile users.proto"); } diff --git a/posts.proto b/posts.proto new file mode 100644 index 0000000..7a18c39 --- /dev/null +++ b/posts.proto @@ -0,0 +1,41 @@ +syntax = "proto3"; +package posts; + +message Post { + int32 id = 1; + int32 userId = 2; + string title = 3; + string body = 4; +} + +message Filter { + repeated int32 ids = 1; + optional int32 userId = 2; + optional int32 start = 3; + optional int32 limit = 4; +} + +message PostRequest { + int32 id = 1; +} + +message PostList { + repeated Post posts = 1; +} + +message PostResponse { + Post post = 1; +} + +message DeleteResponse { + bool success = 1; + string message = 2; +} + +service PostService { + rpc ListPosts(Filter) returns (PostList); + rpc GetPost(PostRequest) returns (Post); + rpc CreatePost(Post) returns (PostResponse); + rpc UpdatePost(Post) returns (PostResponse); + rpc DeletePost(PostRequest) returns (DeleteResponse); +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 7e2aed4..a263815 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,7 +14,6 @@ use tonic_tracing_opentelemetry::middleware::server; use tower::make::Shared; use news::news_service_server::NewsService; -use news::news_service_server::NewsServiceServer; use news::{MultipleNewsId, News, NewsId, NewsList}; use shuttle_runtime::Service; use tracing_subscriber::layer::SubscriberExt; @@ -25,11 +24,33 @@ pub mod news { tonic::include_file_descriptor_set!("news_descriptor"); } +pub mod posts { + tonic::include_proto!("posts"); + pub(crate) const FILE_DESCRIPTOR_SET: &[u8] = + tonic::include_file_descriptor_set!("posts_descriptor"); +} + +pub mod users { + tonic::include_proto!("users"); + pub(crate) const FILE_DESCRIPTOR_SET: &[u8] = + tonic::include_file_descriptor_set!("users_descriptor"); +} + #[derive(Debug, Default)] pub struct MyNewsService { news: Arc>>, // Using a simple vector to store news items in memory } +#[derive(Debug, Default)] +pub struct MyPostService { + posts: Arc>>, +} + +#[derive(Debug, Default)] +pub struct MyUserService { + users: Arc>>, +} + impl MyNewsService { fn new() -> MyNewsService { let news = vec![ @@ -177,6 +198,332 @@ static RESOURCE: Lazy = Lazy::new(|| { ])) }); +impl MyPostService { + fn new() -> Self { + let posts = vec![ + posts::Post { + id: 1, + user_id: 1, + title: "First post".into(), + body: "This is the first post content".into(), + }, + posts::Post { + id: 2, + user_id: 1, + title: "Second post".into(), + body: "This is the second post content".into(), + }, + ]; + MyPostService { + posts: Arc::new(Mutex::new(posts)), + } + } +} + +#[tonic::async_trait] +impl posts::post_service_server::PostService for MyPostService { + async fn list_posts( + &self, + request: tonic::Request, + ) -> Result, Status> { + let filter = request.into_inner(); + let posts = self.posts.lock().unwrap(); + let mut filtered = posts.clone(); + + if !filter.ids.is_empty() { + filtered = filtered + .into_iter() + .filter(|p| filter.ids.contains(&p.id)) + .collect(); + } + + if let Some(user_id) = filter.user_id { + filtered = filtered + .into_iter() + .filter(|p| p.user_id == user_id) + .collect(); + } + + if let (Some(start), Some(limit)) = (filter.start, filter.limit) { + filtered = filtered + .into_iter() + .skip(start as usize) + .take(limit as usize) + .collect(); + } + + Ok(Response::new(posts::PostList { posts: filtered })) + } + + async fn get_post( + &self, + request: tonic::Request, + ) -> Result, Status> { + let id = request.into_inner().id; + let posts = self.posts.lock().unwrap(); + if let Some(post) = posts.iter().find(|p| p.id == id) { + Ok(Response::new(post.clone())) + } else { + Err(Status::not_found("Post not found")) + } + } + + async fn create_post( + &self, + request: tonic::Request, + ) -> Result, Status> { + let mut post = request.into_inner(); + let mut posts = self.posts.lock().unwrap(); + post.id = posts.iter().map(|p| p.id).max().unwrap_or(0) + 1; + posts.push(post.clone()); + Ok(Response::new(posts::PostResponse { post: Some(post) })) + } + + async fn update_post( + &self, + request: tonic::Request, + ) -> Result, Status> { + let new_post = request.into_inner(); + let mut posts = self.posts.lock().unwrap(); + if let Some(post) = posts.iter_mut().find(|p| p.id == new_post.id) { + *post = new_post.clone(); + Ok(Response::new(posts::PostResponse { + post: Some(new_post), + })) + } else { + Err(Status::not_found("Post not found")) + } + } + + async fn delete_post( + &self, + request: tonic::Request, + ) -> Result, Status> { + let id = request.into_inner().id; + let mut posts = self.posts.lock().unwrap(); + let len_before = posts.len(); + posts.retain(|p| p.id != id); + if posts.len() < len_before { + Ok(Response::new(posts::DeleteResponse { + success: true, + message: format!("Post {} successfully deleted", id), + })) + } else { + Ok(Response::new(posts::DeleteResponse { + success: false, + message: format!("Post {} not found", id), + })) + } + } +} + +impl MyUserService { + fn new() -> Self { + let users = vec![users::User { + id: 1, + name: "Leanne Graham".to_string(), + username: "Bret".to_string(), + email: "Sincere@april.biz".to_string(), + address: Some(users::Address { + street: "Kulas Light".to_string(), + suite: "Apt. 556".to_string(), + city: "Gwenborough".to_string(), + zipcode: "92998-3874".to_string(), + geo: Some(users::Geo { + lat: "-37.3159".to_string(), + lng: "81.1496".to_string(), + }), + }), + phone: "1-770-736-8031 x56442".to_string(), + website: "hildegard.org".to_string(), + company: Some(users::Company { + name: "Romaguera-Crona".to_string(), + catch_phrase: "Multi-layered client-server neural-net".to_string(), + bs: "harness real-time e-markets".to_string(), + }), + }]; + MyUserService { + users: Arc::new(Mutex::new(users)), + } + } +} + +#[tonic::async_trait] +impl users::user_service_server::UserService for MyUserService { + async fn list_users( + &self, + request: tonic::Request, + ) -> Result, Status> { + let filter = request.into_inner(); + let users = self.users.lock().unwrap(); + + let mut filtered = users.clone(); + + if !filter.ids.is_empty() { + filtered = filtered + .into_iter() + .filter(|u| filter.ids.contains(&u.id)) + .collect(); + } + + if let Some(username) = filter.username { + filtered = filtered + .into_iter() + .filter(|u| u.username == username) + .collect(); + } + + if let Some(email) = filter.email { + filtered = filtered.into_iter().filter(|u| u.email == email).collect(); + } + + if let (Some(start), Some(limit)) = (filter.start, filter.limit) { + filtered = filtered + .into_iter() + .skip(start as usize) + .take(limit as usize) + .collect(); + } + + Ok(Response::new(users::UserList { users: filtered })) + } + + async fn get_user( + &self, + request: tonic::Request, + ) -> Result, Status> { + let id = request.into_inner().id; + let users = self.users.lock().unwrap(); + + if let Some(user) = users.iter().find(|u| u.id == id) { + Ok(Response::new(user.clone())) + } else { + Err(Status::not_found("User not found")) + } + } + + async fn create_user( + &self, + request: tonic::Request, + ) -> Result, Status> { + let mut user = request.into_inner(); + let mut users = self.users.lock().unwrap(); + + user.id = users.iter().map(|u| u.id).max().unwrap_or(0) + 1; + users.push(user.clone()); + + Ok(Response::new(users::UserResponse { user: Some(user) })) + } + + async fn patch_user( + &self, + request: tonic::Request, + ) -> Result, Status> { + let patch_request = request.into_inner(); + let user_id = patch_request.id; + let new_user_data = patch_request.user.ok_or_else(|| { + Status::invalid_argument("User data must be provided for patch operation") + })?; + + let mut users = self.users.lock().unwrap(); + + if let Some(user) = users.iter_mut().find(|u| u.id == user_id) { + if !new_user_data.name.is_empty() { + user.name = new_user_data.name; + } + if !new_user_data.username.is_empty() { + user.username = new_user_data.username; + } + if !new_user_data.email.is_empty() { + user.email = new_user_data.email; + } + if !new_user_data.phone.is_empty() { + user.phone = new_user_data.phone; + } + if !new_user_data.website.is_empty() { + user.website = new_user_data.website; + } + + if let Some(new_address) = new_user_data.address { + if user.address.is_none() { + user.address = Some(new_address); + } else if let Some(ref mut address) = user.address { + if !new_address.street.is_empty() { + address.street = new_address.street; + } + if !new_address.suite.is_empty() { + address.suite = new_address.suite; + } + if !new_address.city.is_empty() { + address.city = new_address.city; + } + if !new_address.zipcode.is_empty() { + address.zipcode = new_address.zipcode; + } + // Update geo location if provided + if let Some(new_geo) = new_address.geo { + if let Some(ref mut geo) = address.geo { + if !new_geo.lat.is_empty() { + geo.lat = new_geo.lat; + } + if !new_geo.lng.is_empty() { + geo.lng = new_geo.lng; + } + } else { + address.geo = Some(new_geo); + } + } + } + } + + if let Some(new_company) = new_user_data.company { + if user.company.is_none() { + user.company = Some(new_company); + } else if let Some(ref mut company) = user.company { + if !new_company.name.is_empty() { + company.name = new_company.name; + } + if !new_company.catch_phrase.is_empty() { + company.catch_phrase = new_company.catch_phrase; + } + if !new_company.bs.is_empty() { + company.bs = new_company.bs; + } + } + } + + Ok(Response::new(users::UserResponse { + user: Some(user.clone()), + })) + } else { + Err(Status::not_found("User not found")) + } + } + + async fn delete_user( + &self, + request: tonic::Request, + ) -> Result, Status> { + let id = request.into_inner().id; + let mut users = self.users.lock().unwrap(); + let len_before = users.len(); + + users.retain(|u| u.id != id); + + if users.len() < len_before { + Ok(Response::new(users::DeleteResponse { + success: true, + message: format!("User {} successfully deleted", id), + })) + } else { + Ok(Response::new(users::DeleteResponse { + success: false, + message: format!("User {} not found", id), + })) + } + } +} + fn init_tracer() -> Result<()> { global::set_text_map_propagator(TraceContextPropagator::new()); @@ -216,32 +563,54 @@ fn init_tracer() -> Result<()> { Ok(()) } +#[derive(Debug)] +pub struct CompositeService { + news_service: MyNewsService, + post_service: MyPostService, + user_service: MyUserService, +} + #[shuttle_runtime::main] async fn shuttle_main() -> Result { if std::env::var("HONEYCOMB_API_KEY").is_ok() { init_tracer()?; } - let news_service = MyNewsService::new(); + let composite_service = CompositeService { + news_service: MyNewsService::new(), + post_service: MyPostService::new(), + user_service: MyUserService::new(), + }; - Ok(news_service) + Ok(composite_service) } #[async_trait::async_trait] -impl Service for MyNewsService { - async fn bind(mut self, addr: std::net::SocketAddr) -> Result<(), shuttle_runtime::Error> { - let service = tonic_reflection::server::Builder::configure() +impl Service for CompositeService { + async fn bind(self, addr: std::net::SocketAddr) -> Result<(), shuttle_runtime::Error> { + let reflection_service = tonic_reflection::server::Builder::configure() .register_encoded_file_descriptor_set(news::FILE_DESCRIPTOR_SET) + .register_encoded_file_descriptor_set(posts::FILE_DESCRIPTOR_SET) + .register_encoded_file_descriptor_set(users::FILE_DESCRIPTOR_SET) .build() .unwrap(); - println!("NewsService server listening on {}", addr); + println!("Server listening on {}", addr); let tonic_service = TonicServer::builder() .layer(server::OtelGrpcLayer::default()) - .add_service(NewsServiceServer::new(self)) - .add_service(service) + .add_service(news::news_service_server::NewsServiceServer::new( + self.news_service, + )) + .add_service(posts::post_service_server::PostServiceServer::new( + self.post_service, + )) + .add_service(users::user_service_server::UserServiceServer::new( + self.user_service, + )) + .add_service(reflection_service) .into_service(); + let make_svc = Shared::new(tonic_service); let server = hyper::Server::bind(&addr).serve(make_svc); diff --git a/users.proto b/users.proto new file mode 100644 index 0000000..868cd4a --- /dev/null +++ b/users.proto @@ -0,0 +1,70 @@ +syntax = "proto3"; +package users; + +message Address { + string street = 1; + string suite = 2; + string city = 3; + string zipcode = 4; + Geo geo = 5; +} + +message Geo { + string lat = 1; + string lng = 2; +} + +message Company { + string name = 1; + string catchPhrase = 2; + string bs = 3; +} + +message User { + int32 id = 1; + string name = 2; + string username = 3; + string email = 4; + optional Address address = 5; + string phone = 6; + string website = 7; + optional Company company = 8; +} + +message Filter { + repeated int32 ids = 1; + optional string username = 2; + optional string email = 3; + optional int32 start = 4; + optional int32 limit = 5; +} + +message UserRequest { + int32 id = 1; +} + +message UserList { + repeated User users = 1; +} + +message UserResponse { + User user = 1; +} + +message PatchUserRequest { + int32 id = 1; + User user = 2; +} + +message DeleteResponse { + bool success = 1; + string message = 2; +} + +service UserService { + rpc ListUsers(Filter) returns (UserList); + rpc GetUser(UserRequest) returns (User); + rpc CreateUser(User) returns (UserResponse); + rpc PatchUser(PatchUserRequest) returns (UserResponse); + rpc DeleteUser(UserRequest) returns (DeleteResponse); +} \ No newline at end of file