diff --git a/Cargo.lock b/Cargo.lock index 21415c7..f8ebcd6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2512,6 +2512,20 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "search" +version = "0.3.13" +dependencies = [ + "anyhow", + "common", + "prost", + "serde", + "serde_json", + "tokio", + "tonic", + "tracing", +] + [[package]] name = "security-framework" version = "2.11.1" diff --git a/Cargo.toml b/Cargo.toml index 70c2d33..702f077 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["client", "server", "common"] +members = ["client", "server", "common", "search"] resolver = "2" [workspace.package] @@ -16,4 +16,24 @@ categories = ["command-line-interface"] # [workspace.metadata] # TODO: -# documentation = "https://docs.rs/accounting-client"` +# documentation = "https://docs.rs/accounting-client" + +[workspace.dependencies] +# Async and gRPC +tokio = { version = "1.44.2", features = ["full"] } +tonic = "0.13.0" +prost = "0.13.5" +async-trait = "0.1.88" + +# Data Handling & Serialization +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" +time = "0.3.41" + +# Utilities & Error Handling +anyhow = "1.0.98" +dotenvy = "0.15.7" +lazy_static = "1.5.0" +tracing = "0.1.41" + +common = { path = "./common" } diff --git a/common/build.rs b/common/build.rs index 5383226..d90d046 100644 --- a/common/build.rs +++ b/common/build.rs @@ -14,6 +14,7 @@ fn main() -> Result<(), Box> { "proto/table_definition.proto", "proto/tables_data.proto", "proto/table_script.proto", + "proto/search.proto", ], &["proto"], )?; diff --git a/common/proto/search.proto b/common/proto/search.proto new file mode 100644 index 0000000..1ca02ed --- /dev/null +++ b/common/proto/search.proto @@ -0,0 +1,20 @@ +// In common/proto/search.proto +syntax = "proto3"; +package multieko2.search; + +service Searcher { + rpc SearchTable(SearchRequest) returns (SearchResponse); +} + +message SearchRequest { + string table_name = 1; + string query = 2; +} + +message SearchResponse { + message Hit { + int64 id = 1; // The PostgreSQL row ID + float score = 2; + } + repeated Hit hits = 1; +} diff --git a/common/src/lib.rs b/common/src/lib.rs index 786771e..6ff57ee 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -25,6 +25,9 @@ pub mod proto { pub mod table_script { include!("proto/multieko2.table_script.rs"); } + pub mod search { + include!("proto/multieko2.search.rs"); + } pub const FILE_DESCRIPTOR_SET: &[u8] = include_bytes!("proto/descriptor.bin"); } diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index 60f3d32..698f17a 100644 Binary files a/common/src/proto/descriptor.bin and b/common/src/proto/descriptor.bin differ diff --git a/common/src/proto/multieko2.search.rs b/common/src/proto/multieko2.search.rs new file mode 100644 index 0000000..7cbb2c4 --- /dev/null +++ b/common/src/proto/multieko2.search.rs @@ -0,0 +1,315 @@ +// This file is @generated by prost-build. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SearchRequest { + #[prost(string, tag = "1")] + pub table_name: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub query: ::prost::alloc::string::String, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SearchResponse { + #[prost(message, repeated, tag = "1")] + pub hits: ::prost::alloc::vec::Vec, +} +/// Nested message and enum types in `SearchResponse`. +pub mod search_response { + #[derive(Clone, Copy, PartialEq, ::prost::Message)] + pub struct Hit { + /// The PostgreSQL row ID + #[prost(int64, tag = "1")] + pub id: i64, + #[prost(float, tag = "2")] + pub score: f32, + } +} +/// Generated client implementations. +pub mod searcher_client { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + #[derive(Debug, Clone)] + pub struct SearcherClient { + inner: tonic::client::Grpc, + } + impl SearcherClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl SearcherClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + std::marker::Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> SearcherClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + std::marker::Send + std::marker::Sync, + { + SearcherClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + pub async fn search_table( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/multieko2.search.Searcher/SearchTable", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("multieko2.search.Searcher", "SearchTable")); + self.inner.unary(req, path, codec).await + } + } +} +/// Generated server implementations. +pub mod searcher_server { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + /// Generated trait containing gRPC methods that should be implemented for use with SearcherServer. + #[async_trait] + pub trait Searcher: std::marker::Send + std::marker::Sync + 'static { + async fn search_table( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; + } + #[derive(Debug)] + pub struct SearcherServer { + inner: Arc, + accept_compression_encodings: EnabledCompressionEncodings, + send_compression_encodings: EnabledCompressionEncodings, + max_decoding_message_size: Option, + max_encoding_message_size: Option, + } + impl SearcherServer { + pub fn new(inner: T) -> Self { + Self::from_arc(Arc::new(inner)) + } + pub fn from_arc(inner: Arc) -> Self { + Self { + inner, + accept_compression_encodings: Default::default(), + send_compression_encodings: Default::default(), + max_decoding_message_size: None, + max_encoding_message_size: None, + } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> InterceptedService + where + F: tonic::service::Interceptor, + { + InterceptedService::new(Self::new(inner), interceptor) + } + /// Enable decompressing requests with the given encoding. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.accept_compression_encodings.enable(encoding); + self + } + /// Compress responses with the given encoding, if the client supports it. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.send_compression_encodings.enable(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.max_decoding_message_size = Some(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.max_encoding_message_size = Some(limit); + self + } + } + impl tonic::codegen::Service> for SearcherServer + where + T: Searcher, + B: Body + std::marker::Send + 'static, + B::Error: Into + std::marker::Send + 'static, + { + type Response = http::Response; + type Error = std::convert::Infallible; + type Future = BoxFuture; + fn poll_ready( + &mut self, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: http::Request) -> Self::Future { + match req.uri().path() { + "/multieko2.search.Searcher/SearchTable" => { + #[allow(non_camel_case_types)] + struct SearchTableSvc(pub Arc); + impl tonic::server::UnaryService + for SearchTableSvc { + type Response = super::SearchResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::search_table(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = SearchTableSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + _ => { + Box::pin(async move { + let mut response = http::Response::new( + tonic::body::Body::default(), + ); + let headers = response.headers_mut(); + headers + .insert( + tonic::Status::GRPC_STATUS, + (tonic::Code::Unimplemented as i32).into(), + ); + headers + .insert( + http::header::CONTENT_TYPE, + tonic::metadata::GRPC_CONTENT_TYPE, + ); + Ok(response) + }) + } + } + } + } + impl Clone for SearcherServer { + fn clone(&self) -> Self { + let inner = self.inner.clone(); + Self { + inner, + accept_compression_encodings: self.accept_compression_encodings, + send_compression_encodings: self.send_compression_encodings, + max_decoding_message_size: self.max_decoding_message_size, + max_encoding_message_size: self.max_encoding_message_size, + } + } + } + /// Generated gRPC service name + pub const SERVICE_NAME: &str = "multieko2.search.Searcher"; + impl tonic::server::NamedService for SearcherServer { + const NAME: &'static str = SERVICE_NAME; + } +} diff --git a/search/Cargo.toml b/search/Cargo.toml new file mode 100644 index 0000000..60e212e --- /dev/null +++ b/search/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "search" +version.workspace = true +edition.workspace = true +license = "AGPL-3.0-or-later" + +[dependencies] +anyhow = { workspace = true } +prost = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } +tonic = { workspace = true } +tracing = { workspace = true } + +common = { path = "../common" } diff --git a/search/src/lib.rs b/search/src/lib.rs new file mode 100644 index 0000000..b93cf3f --- /dev/null +++ b/search/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +}