diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8760d68..2325300 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -65,7 +65,6 @@ jobs: runs-on: ubuntu-latest if: github.event_name == 'push' && github.ref == 'refs/heads/main' needs: [cargo-check, fmt-check, test-and-coverage] - # needs: [fmt-check] steps: - name: Check out from Git uses: actions/checkout@v4 @@ -89,7 +88,7 @@ jobs: - uses: arduino/setup-protoc@v3 - name: Release Build - run: cargo build --release --bin cli + run: cargo build --release --all - name: "Upload Artifact" uses: actions/upload-artifact@v4 @@ -103,7 +102,6 @@ jobs: runs-on: ubuntu-latest if: github.event_name == 'push' && github.ref == 'refs/heads/main' needs: [cargo-check, fmt-check, test-and-coverage] - # needs: [fmt-check] steps: - name: Check out from Git uses: actions/checkout@v4 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..1b3580b --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,17 @@ +name: Docker build + +on: + pull_request: + branches: + - main + +jobs: + build-docker-image: + runs-on: ubuntu-latest + steps: + - name: Check out from Git + uses: actions/checkout@v4 + + - name: Build docker image + run: | + docker build . diff --git a/Cargo.lock b/Cargo.lock index 5db657a..41b2555 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,10 +11,12 @@ dependencies = [ "deadpool-diesel", "diesel", "diesel_migrations", + "grpc_interface", "rust_core", "serde", "testcontainers-modules", "tokio", + "tonic", ] [[package]] @@ -104,9 +106,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.80" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" +checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" [[package]] name = "async-stream" @@ -147,15 +149,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" -[[package]] -name = "autotools" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8da1805e028a172334c3b680f93e71126f2327622faef2ec3d893c0a4ad77" -dependencies = [ - "cc", -] - [[package]] name = "axum" version = "0.6.20" @@ -337,7 +330,7 @@ dependencies = [ "deadpool-diesel", "diesel", "diesel_migrations", - "grpc_client", + "grpc_interface", "openssl", "opentelemetry", "rand", @@ -391,7 +384,7 @@ dependencies = [ "rust-ini", "serde", "serde_json", - "toml 0.8.10", + "toml 0.8.11", "yaml-rust", ] @@ -828,16 +821,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] -name = "grpc_client" +name = "grpc_interface" version = "0.0.1" dependencies = [ - "once_cell", + "glob", "prost", - "prost-build", - "protobuf-src", + "tonic", + "tonic-build", +] + +[[package]] +name = "grpc_server" +version = "0.0.1" +dependencies = [ + "clap", + "common", + "grpc_interface", + "opentelemetry", + "prost", + "readonly", "rust_core", + "serde", + "serde_json", + "tokio", "tonic", "tonic-build", + "tracing", ] [[package]] @@ -1584,9 +1593,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] @@ -1645,15 +1654,6 @@ dependencies = [ "prost", ] -[[package]] -name = "protobuf-src" -version = "1.1.0+21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7ac8852baeb3cc6fb83b93646fb93c0ffe5d14bf138c945ceb4b9948ee0e3c1" -dependencies = [ - "autotools", -] - [[package]] name = "quote" version = "1.0.35" @@ -2072,18 +2072,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", @@ -2246,14 +2246,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a9aad4a3066010876e8dcf5a8a06e70a558751117a145c6ce2b82c2e2054290" +checksum = "af06656561d28735e9c1cd63dfd57132c8155426aa6af24f36a00a351f88c48e" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.6", + "toml_edit 0.22.7", ] [[package]] @@ -2280,9 +2280,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.6" +version = "0.22.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c1b5fd4128cc8d3e0cb74d4ed9a9cc7c7284becd4df68f5f940e1ad123606f6" +checksum = "18769cd1cec395d70860ceb4d932812a0b4d06b1a4bb336745a4d21b9496e992" dependencies = [ "indexmap 2.2.5", "serde", diff --git a/Cargo.toml b/Cargo.toml index 49a5ae3..25646c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] -members = ["src/public"] +members = ["src/public", "src/grpc_server"] resolver = "2" diff --git a/Dockerfile b/Dockerfile index aa0966e..34c9996 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,7 @@ FROM clux/muslrust:stable AS chef USER root RUN cargo install cargo-chef + WORKDIR /app FROM clux/muslrust:stable AS bunyan @@ -16,7 +17,7 @@ FROM chef AS builder COPY --from=planner /app/recipe.json recipe.json RUN cargo chef cook --release --recipe-path recipe.json COPY . . -RUN cargo build --release --bin cli +RUN cargo build --release --all RUN mv target/${CARGO_BUILD_TARGET}/release /out FROM scratch AS prod diff --git a/src/adapter/Cargo.toml b/src/adapter/Cargo.toml index 021a25f..d04f690 100644 --- a/src/adapter/Cargo.toml +++ b/src/adapter/Cargo.toml @@ -42,4 +42,10 @@ version = "1.36.0" features = ["full"] [dependencies.anyhow] -version = "1.0.80" \ No newline at end of file +version = "1.0.80" + +[dependencies.tonic] +version = "0.11.0" + +[dependencies.grpc_interface] +path = "../grpc" diff --git a/src/adapter/src/repositories/grpc/config.rs b/src/adapter/src/repositories/grpc/config.rs new file mode 100644 index 0000000..c078b18 --- /dev/null +++ b/src/adapter/src/repositories/grpc/config.rs @@ -0,0 +1,15 @@ +use serde::Deserialize; + +/// Represents servers configuration options. +#[derive(Deserialize, Debug)] +pub struct GrpcServers { + /// Configuration for using in-memory database. + pub gpt_answer_service: Option, +} + +/// Represents service server configuration. +#[derive(Debug, Deserialize, Clone)] +pub struct ServiceServer { + /// URL for the server. + pub url: String, +} diff --git a/src/adapter/src/repositories/grpc/mod.rs b/src/adapter/src/repositories/grpc/mod.rs new file mode 100644 index 0000000..b6a44ac --- /dev/null +++ b/src/adapter/src/repositories/grpc/mod.rs @@ -0,0 +1,2 @@ +pub mod config; +pub mod models; diff --git a/src/grpc/src/grpc_client/gpt_answer.rs b/src/adapter/src/repositories/grpc/models/gpt_answer.rs similarity index 65% rename from src/grpc/src/grpc_client/gpt_answer.rs rename to src/adapter/src/repositories/grpc/models/gpt_answer.rs index b07a665..4e42922 100644 --- a/src/grpc/src/grpc_client/gpt_answer.rs +++ b/src/adapter/src/repositories/grpc/models/gpt_answer.rs @@ -1,11 +1,9 @@ use rust_core::common::errors::CoreError; use tonic::transport::Channel; -use gpt_answer::gpt_answer_service_client::GptAnswerServiceClient; - -mod gpt_answer { - tonic::include_proto!("gpt_answer"); -} +use grpc_interface::interfaces::gpt_answer::gpt_answer::{ + gpt_answer_service_client::GptAnswerServiceClient, GetAnswerPayload, +}; pub struct GptAnswerGrpcClient { client: GptAnswerServiceClient, @@ -17,10 +15,9 @@ impl GptAnswerGrpcClient { Self { client } } - pub async fn get_instance() -> Result { - let uri = "http://0.0.0.0:50051"; + pub async fn get_instance(uri: &'static str) -> Result { let channel = Channel::from_static(uri).connect().await.map_err(|err| { - println!("Error connecting to GPT: {:?}", err); + eprintln!("Error connecting to GPT: {:?}", err); CoreError::InternalError })?; @@ -29,12 +26,12 @@ impl GptAnswerGrpcClient { } pub async fn get_answer(&mut self, question: &str) -> Result { - let request = tonic::Request::new(gpt_answer::GetAnswerPayload { + let request = tonic::Request::new(GetAnswerPayload { question: question.to_string(), }); let response = self.client.get_answer(request).await.map_err(|err| { - println!("Error getting answer from GPT: {:?}", err); + eprintln!("Error getting answer from GPT: {:?}", err); CoreError::InternalError })?; diff --git a/src/grpc/src/grpc_client/mod.rs b/src/adapter/src/repositories/grpc/models/mod.rs similarity index 100% rename from src/grpc/src/grpc_client/mod.rs rename to src/adapter/src/repositories/grpc/models/mod.rs diff --git a/src/adapter/src/repositories/mod.rs b/src/adapter/src/repositories/mod.rs index 812478a..5da5a18 100644 --- a/src/adapter/src/repositories/mod.rs +++ b/src/adapter/src/repositories/mod.rs @@ -1,3 +1,4 @@ +pub mod grpc; pub mod in_memory; pub mod postgres; pub mod repository_test; diff --git a/src/adapter/src/repositories/postgres/models/question.rs b/src/adapter/src/repositories/postgres/models/question.rs index 2b3a234..93e804c 100644 --- a/src/adapter/src/repositories/postgres/models/question.rs +++ b/src/adapter/src/repositories/postgres/models/question.rs @@ -1,8 +1,10 @@ -use std::time::SystemTime; +use std::{ + io::{Error, ErrorKind}, + time::SystemTime, +}; use diesel::{AsChangeset, Identifiable, Insertable, Queryable, Selectable}; -use rust_core::common::errors::CoreError; -use rust_core::entities::question::QuestionEntity; +use rust_core::entities::question::{QuestionEntity, QuestionId}; use serde::Serialize; #[derive(Debug, Queryable, Serialize, Selectable, Insertable, AsChangeset, Identifiable)] @@ -20,23 +22,33 @@ pub struct QuestionModel { pub created_on: SystemTime, } -impl QuestionModel { - pub fn from(entity: QuestionEntity) -> Result { +impl TryFrom for QuestionModel { + type Error = Error; + + fn try_from(entity: QuestionEntity) -> Result { + let id = entity + .id + .0 + .parse() + .map_err(|_| Error::new(ErrorKind::InvalidInput, "Invalid ID"))?; + Ok(QuestionModel { - id: entity.id.to_string().parse()?, + id, title: entity.title, content: entity.content, tags: entity.tags.map(|v| v.into_iter().map(Some).collect()), created_on: SystemTime::now(), }) } +} - pub fn to_entity(self) -> Result { - Ok(QuestionEntity { - id: self.id.to_string().parse()?, +impl Into for QuestionModel { + fn into(self) -> QuestionEntity { + QuestionEntity { + id: QuestionId(self.id.to_string()), title: self.title, content: self.content, tags: self.tags.map(|v| v.into_iter().flatten().collect()), - }) + } } } diff --git a/src/adapter/src/repositories/postgres/question_db.rs b/src/adapter/src/repositories/postgres/question_db.rs index 6099855..9b3b40d 100644 --- a/src/adapter/src/repositories/postgres/question_db.rs +++ b/src/adapter/src/repositories/postgres/question_db.rs @@ -37,7 +37,8 @@ impl QuestionPort for QuestionDBRepository { .await .unwrap() .interact(move |conn| { - let question = QuestionModel::from(question).unwrap(); + let question = + QuestionModel::try_from(question).map_err(|_| CoreError::InternalError)?; let response = insert_into(questions) .values(&question) .get_result::(conn) @@ -46,7 +47,7 @@ impl QuestionPort for QuestionDBRepository { _ => CoreError::InternalError, }) .unwrap(); - Ok(response.to_entity().unwrap()) + Ok(response.into()) }) .await .unwrap() @@ -58,7 +59,8 @@ impl QuestionPort for QuestionDBRepository { .await .unwrap() .interact(move |conn| { - let question = QuestionModel::from(question)?; + let question = + QuestionModel::try_from(question).map_err(|_| CoreError::InternalError)?; let response = update(questions.filter(id.eq(question.id))) .set(&question) .get_result::(conn) @@ -66,7 +68,7 @@ impl QuestionPort for QuestionDBRepository { diesel::result::Error::NotFound => CoreError::NotFound, _ => CoreError::InternalError, })? - .to_entity()?; + .into(); Ok(response) }) @@ -109,7 +111,7 @@ impl QuestionPort for QuestionDBRepository { diesel::result::Error::NotFound => CoreError::NotFound, _ => CoreError::InternalError, })? - .to_entity()?; + .into(); Ok(response) }) @@ -136,7 +138,7 @@ impl QuestionPort for QuestionDBRepository { Ok(question_list .into_iter() - .map(|l| l.to_entity().unwrap()) + .map(|l| l.into()) .collect::>()) }) .await diff --git a/src/grpc/Cargo.toml b/src/grpc/Cargo.toml index f5aa19b..652ee3f 100644 --- a/src/grpc/Cargo.toml +++ b/src/grpc/Cargo.toml @@ -4,7 +4,7 @@ test = [] example = [] [package] -name = "grpc_client" +name = "grpc_interface" edition = "2021" version = "0.0.1" autobins = true @@ -15,28 +15,17 @@ autobenches = true [dependencies.tonic] version = "0.11.0" -[dependencies.once_cell] -version = "1.8.0" - [dependencies.prost] version = "0.12.3" -[dependencies.prost-build] -version = "0.12.3" - -[dependencies.protobuf-src] -version = "1.1.0" - -[dependencies.rust_core] -path = "../core" - [build-dependencies] tonic-build = "0.11.0" +glob = "0.3.1" [lib] path = "src/lib.rs" -name = "grpc_client" +name = "grpc_interface" test = true doctest = true bench = true diff --git a/src/grpc/build.rs b/src/grpc/build.rs index 44d03b1..729d396 100644 --- a/src/grpc/build.rs +++ b/src/grpc/build.rs @@ -1,4 +1,21 @@ -fn main() -> Result<(), Box> { - tonic_build::compile_protos("proto/gpt_answer.proto")?; +use glob::glob; +use std::error::Error; + +fn main() -> Result<(), Box> { + // Find all .proto files within the proto/ directory + let proto_files: Vec = glob("proto/*.proto")? + .filter_map(|entry| { + entry + .ok() + .and_then(|path| path.into_os_string().into_string().ok()) + }) + .collect(); + + // Compile the found .proto files + proto_files.iter().for_each(|proto_file| { + println!("cargo:rerun-if-changed={}", proto_file); + tonic_build::compile_protos(proto_file).unwrap(); + }); + Ok(()) } diff --git a/src/grpc/src/interfaces/gpt_answer.rs b/src/grpc/src/interfaces/gpt_answer.rs new file mode 100644 index 0000000..ef738af --- /dev/null +++ b/src/grpc/src/interfaces/gpt_answer.rs @@ -0,0 +1,3 @@ +pub mod gpt_answer { + tonic::include_proto!("gpt_answer"); +} diff --git a/src/grpc/src/grpc_server/mod.rs b/src/grpc/src/interfaces/mod.rs similarity index 100% rename from src/grpc/src/grpc_server/mod.rs rename to src/grpc/src/interfaces/mod.rs diff --git a/src/grpc/src/lib.rs b/src/grpc/src/lib.rs index acb46d2..43b15ec 100644 --- a/src/grpc/src/lib.rs +++ b/src/grpc/src/lib.rs @@ -1,2 +1 @@ -pub mod grpc_client; -pub mod grpc_server; +pub mod interfaces; diff --git a/src/grpc_server/Cargo.toml b/src/grpc_server/Cargo.toml new file mode 100644 index 0000000..d6a048b --- /dev/null +++ b/src/grpc_server/Cargo.toml @@ -0,0 +1,66 @@ +bin = [] +bench = [] +test = [] +example = [] + +[package] +name = "grpc_server" +edition = "2021" +version = "0.0.1" +autobins = true +autoexamples = true +autotests = true +autobenches = true + +[dependencies.tonic] +version = "0.11.0" + +[dependencies.prost] +version = "0.12.3" + +[dependencies.rust_core] +path = "../core" + +[dependencies.serde] +version = "1.0" +features = ["derive"] + +[dependencies.clap] +version = "4.4.7" +features = ["derive"] + +[dependencies.tokio] +version = "1.36.0" +features = ["full"] + +[dependencies.opentelemetry] +version = "0.22.0" + +[dependencies] +serde_json = "1.0" +readonly = "0.2.12" +tracing = "0.1" + +[dependencies.common] +path = "../common" + +[dependencies.grpc_interface] +path = "../grpc" + +[build-dependencies] +tonic-build = "0.11.0" + + +[lib] +path = "src/lib.rs" +name = "grpc_server" +test = true +doctest = true +bench = true +doc = true +plugin = false +proc-macro = false +harness = true +edition = "2021" +required-features = [] +crate-type = ["rlib"] diff --git a/src/grpc_server/build.rs b/src/grpc_server/build.rs new file mode 100644 index 0000000..3c01894 --- /dev/null +++ b/src/grpc_server/build.rs @@ -0,0 +1,57 @@ +use std::{borrow::Cow, env, process::Command}; + +/// Generate the `cargo:` key output +pub fn generate_cargo_keys() { + let output = Command::new("git") + .args(["rev-parse", "--short", "HEAD"]) + .output(); + + let commit = match output { + Ok(o) if o.status.success() => { + let sha = String::from_utf8_lossy(&o.stdout).trim().to_owned(); + Cow::from(sha) + } + Ok(o) => { + println!("cargo:warning=Git command failed with status: {}", o.status); + Cow::from("unknown") + } + Err(err) => { + println!("cargo:warning=Failed to execute git command: {}", err); + Cow::from("unknown") + } + }; + + println!("cargo:rustc-env=APP_VERSION={}", get_version(&commit)) +} + +fn get_platform() -> String { + let env_dash = if env::var("CARGO_CFG_TARGET_ENV").unwrap().is_empty() { + "" + } else { + "-" + }; + + format!( + "{}-{}{}{}", + env::var("CARGO_CFG_TARGET_ARCH").unwrap(), + env::var("CARGO_CFG_TARGET_OS").unwrap(), + env_dash, + env::var("CARGO_CFG_TARGET_ENV").unwrap_or(String::from("")), + ) +} + +fn get_version(impl_commit: &str) -> String { + let commit_dash = if impl_commit.is_empty() { "" } else { "-" }; + + format!( + "{}{}{}-{}", + std::env::var("CARGO_PKG_VERSION").unwrap_or_default(), + commit_dash, + impl_commit, + get_platform(), + ) +} + +pub fn main() { + generate_cargo_keys(); +} diff --git a/src/grpc_server/config/00-default.toml b/src/grpc_server/config/00-default.toml new file mode 100644 index 0000000..1f4048f --- /dev/null +++ b/src/grpc_server/config/00-default.toml @@ -0,0 +1,5 @@ +service_name = "rust-grpc-server" +exporter_endpoint = "http://localhost:7281" + +[servers.gpt_answer_service] +url = "0.0.0.0:50051" \ No newline at end of file diff --git a/src/grpc/src/grpc_server/gpt_answer.rs b/src/grpc_server/src/controllers/gpt_answer.rs similarity index 62% rename from src/grpc/src/grpc_server/gpt_answer.rs rename to src/grpc_server/src/controllers/gpt_answer.rs index 61f9c61..7aae3d4 100644 --- a/src/grpc/src/grpc_server/gpt_answer.rs +++ b/src/grpc_server/src/controllers/gpt_answer.rs @@ -1,10 +1,11 @@ -use gpt_answer::gpt_answer_service_server::{GptAnswerService, GptAnswerServiceServer}; -use gpt_answer::{GetAnswerPayload, GetAnswerResponse}; +use grpc_interface::interfaces::gpt_answer::gpt_answer::gpt_answer_service_server::{ + GptAnswerService, GptAnswerServiceServer, +}; +use grpc_interface::interfaces::gpt_answer::gpt_answer::{GetAnswerPayload, GetAnswerResponse}; +use rust_core::common::errors::CoreError; use tonic::{transport::Server, Request, Response, Status}; -mod gpt_answer { - tonic::include_proto!("gpt_answer"); -} +use crate::options::Options; #[derive(Debug, Default)] pub struct GptAnswerServer; @@ -24,9 +25,11 @@ impl GptAnswerService for GptAnswerServer { } } -pub async fn init_gpt_answer_server() { - let result = "0.0.0.0:50051".parse().map_err(|err| { - println!("Error: {:?}", err); +pub async fn init_gpt_answer_server(options: Options) { + let gpt_answer_config = options.servers.gpt_answer_service.clone().unwrap(); + let result = gpt_answer_config.url.parse().map_err(|err| { + eprintln!("Error: {:?}", err); + CoreError::InternalError }); if result.is_ok() { @@ -44,6 +47,6 @@ pub async fn init_gpt_answer_server() { println!("GPT Answer server started at {}", addr); } else { - println!("GPT Answer server failed to start"); + eprintln!("GPT Answer server failed to start"); } } diff --git a/src/grpc_server/src/controllers/mod.rs b/src/grpc_server/src/controllers/mod.rs new file mode 100644 index 0000000..bec2060 --- /dev/null +++ b/src/grpc_server/src/controllers/mod.rs @@ -0,0 +1 @@ +pub mod gpt_answer; diff --git a/src/grpc_server/src/lib.rs b/src/grpc_server/src/lib.rs new file mode 100644 index 0000000..aecda25 --- /dev/null +++ b/src/grpc_server/src/lib.rs @@ -0,0 +1,2 @@ +pub mod controllers; +pub mod options; diff --git a/src/grpc_server/src/main.rs b/src/grpc_server/src/main.rs new file mode 100644 index 0000000..201752b --- /dev/null +++ b/src/grpc_server/src/main.rs @@ -0,0 +1,62 @@ +use clap::{Parser, Subcommand}; +use grpc_server::{controllers, options}; +use opentelemetry::global; + +use common::loggers::telemetry::init_telemetry; +use common::options::parse_options; +use controllers::gpt_answer::init_gpt_answer_server; +use options::Options; + +#[tokio::main] +async fn main() { + let args = Args::parse(); + if args.version { + println!(env!("APP_VERSION")); + return; + } + + let options: Options = match parse_options(args.config_path) { + Ok(options) => options, + Err(err) => { + eprintln!("Failed to load config: {}", err); + return; + } + }; + + if let Some(Commands::Config) = args.command { + println!("{:#?}", options); + return; + } + + init_telemetry( + options.service_name.as_str(), + options.exporter_endpoint.as_str(), + options.log.level.as_str(), + ); + + let gpt_answer_server = tokio::spawn(init_gpt_answer_server(options)); + + tokio::try_join!(gpt_answer_server).expect("Failed to run servers"); + + global::shutdown_tracer_provider(); +} + +/// Simple REST server. +#[derive(Parser, Debug)] +#[command(about, long_about = None)] +struct Args { + #[command(subcommand)] + command: Option, + /// Config file + #[arg(short, long, default_value = "config/00-default.toml")] + config_path: Vec, + /// Print version + #[clap(short, long)] + version: bool, +} + +#[derive(Subcommand, Clone, Debug)] +enum Commands { + /// Print config + Config, +} diff --git a/src/grpc_server/src/options.rs b/src/grpc_server/src/options.rs new file mode 100644 index 0000000..f660c2c --- /dev/null +++ b/src/grpc_server/src/options.rs @@ -0,0 +1,34 @@ +use common::options::{default_log, Log}; +use serde::Deserialize; + +/// Configuration options for the application. +/// +/// This struct represents the configuration options for the application, including server settings, +/// database configuration, endpoint for the exporter, service name, and logging configuration. +#[readonly::make] +#[derive(Deserialize, Debug)] +pub struct Options { + /// Configuration for the servers. + pub servers: GrpcServers, + /// The endpoint for the exporter. + pub exporter_endpoint: String, + /// The name of the service. + pub service_name: String, + /// Configuration for logging, including log level. + #[serde(default = "default_log")] + pub log: Log, +} + +/// Represents servers configuration options. +#[derive(Deserialize, Debug)] +pub struct GrpcServers { + /// Configuration for using in-memory database. + pub gpt_answer_service: Option, +} + +/// Represents service server configuration. +#[derive(Debug, Deserialize, Clone)] +pub struct ServiceServer { + /// URL for the server. + pub url: String, +} diff --git a/src/public/Cargo.toml b/src/public/Cargo.toml index 1c4b180..74b8a55 100644 --- a/src/public/Cargo.toml +++ b/src/public/Cargo.toml @@ -103,7 +103,7 @@ version = "1.0.57" [dependencies.anyhow] version = "1.0.80" -[dependencies.grpc_client] +[dependencies.grpc_interface] path = "../grpc" [lib] diff --git a/src/public/src/controllers/question.rs b/src/public/src/controllers/question.rs index 1f83d49..6bb8797 100644 --- a/src/public/src/controllers/question.rs +++ b/src/public/src/controllers/question.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::str::FromStr; use std::sync::Arc; +use adapter::repositories::grpc::models::gpt_answer::GptAnswerGrpcClient; use tracing::instrument; use warp::http::StatusCode; use warp::reject::Rejection; @@ -11,9 +12,8 @@ use rust_core::entities::question::{QuestionEntity, QuestionId}; use rust_core::entities::question_filter::QuestionFilter; use rust_core::ports::question::QuestionPort; -use grpc_client::grpc_client::gpt_answer::GptAnswerGrpcClient; - use crate::errors::WarpError; +use crate::options::GrpcClients; /// Handler for retrieving questions based on query parameters. /// @@ -23,14 +23,15 @@ use crate::errors::WarpError; #[instrument(level = "info", skip(question_port))] pub async fn get_questions( question_port: Arc, + server_config: Arc, query: HashMap, ) -> Result { - let question_filter = QuestionFilter::from_query(&query).map_err(|err| WarpError::from(err))?; + let question_filter = QuestionFilter::from_query(&query).map_err(WarpError::from)?; let questions = question_port .list(&question_filter) .await - .map_err(|err| WarpError::from(err))?; + .map_err(WarpError::from)?; Ok(warp::reply::json(&questions)) } @@ -43,14 +44,15 @@ pub async fn get_questions( #[instrument(level = "info", skip(question_port))] pub async fn get_question( question_port: Arc, + server_config: Arc, id: String, ) -> Result { - let question_id = QuestionId::from_str(id.as_str()).map_err(|err| WarpError::from(err))?; + let question_id = QuestionId::from_str(id.as_str()).map_err(WarpError::from)?; let question = question_port .get(&question_id) .await - .map_err(|err| WarpError::from(err))?; + .map_err(WarpError::from)?; Ok(warp::reply::json(&question)) } @@ -63,12 +65,10 @@ pub async fn get_question( #[instrument(level = "info", skip(question_port))] pub async fn add_question( question_port: Arc, + server_config: Arc, question: QuestionEntity, ) -> Result { - question_port - .add(question) - .await - .map_err(|err| WarpError::from(err))?; + question_port.add(question).await.map_err(WarpError::from)?; Ok(warp::reply::with_status("Question added", StatusCode::OK)) } @@ -81,14 +81,15 @@ pub async fn add_question( #[instrument(level = "info", skip(question_port))] pub async fn delete_question( question_port: Arc, + server_config: Arc, id: String, ) -> Result { - let question_id = QuestionId::from_str(id.as_str()).map_err(|err| WarpError::from(err))?; + let question_id = QuestionId::from_str(id.as_str()).map_err(WarpError::from)?; question_port .delete(&question_id) .await - .map_err(|err| WarpError::from(err))?; + .map_err(WarpError::from)?; Ok(warp::reply::with_status("Question deleted", StatusCode::OK)) } @@ -102,16 +103,17 @@ pub async fn delete_question( #[instrument(level = "info", skip(question_port))] pub async fn update_question( question_port: Arc, + server_config: Arc, id: String, mut question: QuestionEntity, ) -> Result { - let question_id = QuestionId::from_str(id.as_str()).map_err(|err| WarpError::from(err))?; + let question_id = QuestionId::from_str(id.as_str()).map_err(WarpError::from)?; question.id = question_id; question_port .update(question) .await - .map_err(|err| WarpError::from(err))?; + .map_err(WarpError::from)?; Ok(warp::reply::with_status("Question updated", StatusCode::OK)) } @@ -136,20 +138,25 @@ pub async fn update_question( #[instrument(level = "info", skip(question_port))] pub async fn get_question_answer_controller( question_port: Arc, + server_config: Arc, id: String, ) -> Result { let question = question_port .get(&QuestionId::from_str(id.as_str()).unwrap()) .await - .map_err(|err| WarpError::from(err))?; + .map_err(WarpError::from)?; + + // Clone the gpt_client string to have a static lifetime + let gpt_client = server_config.gpt_answer_service.clone().unwrap().url; - let client = GptAnswerGrpcClient::get_instance().await; + // Use the cloned gpt_client string + let client = GptAnswerGrpcClient::get_instance(gpt_client.leak()).await; let answer = client .unwrap() .get_answer(&question.content) .await - .map_err(|err| WarpError::from(err))?; + .map_err(WarpError::from)?; Ok(warp::reply::with_status(answer, StatusCode::OK)) } diff --git a/src/public/src/main.rs b/src/public/src/main.rs index 50487ee..a5c0302 100644 --- a/src/public/src/main.rs +++ b/src/public/src/main.rs @@ -1,4 +1,3 @@ -#[rustfmt::skip] #[cfg_attr(debug_assertions, allow(dead_code, unused_imports))] use openssl; #[rustfmt::skip] @@ -23,8 +22,6 @@ use common::loggers::telemetry::init_telemetry; use common::options::parse_options; use rust_core::ports::question::QuestionPort; -use grpc_client::grpc_server::gpt_answer::init_gpt_answer_server; - #[tokio::main] async fn main() { let args = Args::parse(); @@ -52,9 +49,8 @@ async fn main() { options.log.level.as_str(), ); - let grpc_server = tokio::spawn(init_gpt_answer_server()); let warp_server = tokio::spawn(run_warp_server(options)); - tokio::try_join!(grpc_server, warp_server).expect("Failed to run servers"); + tokio::try_join!(warp_server).expect("Failed to run servers"); global::shutdown_tracer_provider(); } @@ -97,7 +93,8 @@ pub async fn run_warp_server(options: Options) { Arc::new(QuestionInMemoryRepository::new()) }; - let router = Router::new(question_port); + let grpc_clients = options.grpc_clients.clone(); + let router = Router::new(question_port, Arc::new(grpc_clients)); let address = SocketAddrV4::new( Ipv4Addr::from_str(options.server.url.as_str()).unwrap(), diff --git a/src/public/src/options.rs b/src/public/src/options.rs index dda7300..ad1c30a 100644 --- a/src/public/src/options.rs +++ b/src/public/src/options.rs @@ -13,6 +13,8 @@ pub struct Options { /// Configuration for the server. pub server: Server, /// Specifies the backend database will be used. + pub grpc_clients: GrpcClients, + /// Specifies the backend database will be used. pub db: Database, /// The endpoint for the exporter. pub exporter_endpoint: String, @@ -44,3 +46,16 @@ pub struct Server { /// URL for the server. pub url: String, } + +/// Represents server configuration. +#[derive(Debug, Deserialize, Clone)] +pub struct GrpcClients { + pub gpt_answer_service: Option, +} + +/// Represents service server configuration. +#[derive(Debug, Deserialize, Clone)] +pub struct ServiceServer { + /// URL for the server. + pub url: String, +} diff --git a/src/public/src/router.rs b/src/public/src/router.rs index 9e37332..98c33a6 100644 --- a/src/public/src/router.rs +++ b/src/public/src/router.rs @@ -10,23 +10,30 @@ use crate::controllers::question::{ update_question, }; use crate::errors::return_error; +use crate::options::GrpcClients; /// Router for handling HTTP requests related to questions. pub struct Router { question_port: Arc, + server_config: Arc, } impl Router { /// Creates a new Router instance with the specified QuestionPort. - pub fn new(question_port: Arc) -> Self { + pub fn new( + question_port: Arc, + server_config: Arc, + ) -> Self { Router { question_port: question_port.clone(), + server_config: server_config.clone(), } } /// Configures and returns the Warp filter for handling HTTP requests. pub fn routes(self) -> impl Filter + Clone { let store_filter = warp::any().map(move || self.question_port.clone()); + let server_config = warp::any().map(move || self.server_config.clone()); let cors = warp::cors() .allow_any_origin() .allow_header("content-type") @@ -35,12 +42,14 @@ impl Router { .and(warp::path("questions")) .and(warp::path::end()) .and(store_filter.clone()) + .and(server_config.clone()) .and(warp::query()) .and_then(get_questions); let get_question = warp::get() .and(warp::path("questions")) .and(store_filter.clone()) + .and(server_config.clone()) .and(warp::path::param::()) .and(warp::path::end()) .and_then(get_question); @@ -49,12 +58,14 @@ impl Router { .and(warp::path("questions")) .and(warp::path::end()) .and(store_filter.clone()) + .and(server_config.clone()) .and(warp::body::json()) .and_then(add_question); let update_question = warp::put() .and(warp::path("questions")) .and(store_filter.clone()) + .and(server_config.clone()) .and(warp::path::param::()) .and(warp::path::end()) .and(warp::body::json()) @@ -63,6 +74,7 @@ impl Router { let delete_question = warp::delete() .and(warp::path("questions")) .and(store_filter.clone()) + .and(server_config.clone()) .and(warp::path::param::()) .and(warp::path::end()) .and_then(delete_question); @@ -70,6 +82,7 @@ impl Router { let get_question_answer = warp::get() .and(warp::path("questions")) .and(store_filter.clone()) + .and(server_config.clone()) .and(warp::path::param::()) .and(warp::path("answer")) .and_then(get_question_answer_controller); diff --git a/src/public/tests/questions_router_test.rs b/src/public/tests/questions_router_test.rs index 5ee3abf..0672db1 100644 --- a/src/public/tests/questions_router_test.rs +++ b/src/public/tests/questions_router_test.rs @@ -15,7 +15,10 @@ mod tests { in_memory::question::QuestionInMemoryRepository, postgres::question_db::{QuestionDBRepository, MIGRATIONS}, }; - use cli::router::Router; + use cli::{ + options::{GrpcClients, ServiceServer}, + router::Router, + }; use rust_core::{ entities::question::{QuestionEntity, QuestionId}, ports::question::QuestionPort, @@ -30,7 +33,13 @@ mod tests { where T: QuestionPort + Send + Sync + 'static, { - let router = Router::new(question_port); + let server_config: GrpcClients = GrpcClients { + gpt_answer_service: Some(ServiceServer { + url: "http://localhost:50051".to_string(), + }), + }; + + let router = Router::new(question_port, Arc::new(server_config)); let routers = router.routes(); let raw_question_id: String = rand::thread_rng().gen_range(1..=1000).to_string();