diff options
-rw-r--r-- | lib/sellershut-core/Cargo.toml | 36 | ||||
-rw-r--r-- | lib/sellershut-core/build.rs | 81 | ||||
-rw-r--r-- | lib/sellershut-core/proto/auth/auth.proto | 35 | ||||
-rw-r--r-- | lib/sellershut-core/src/auth.rs | 3 | ||||
-rw-r--r-- | lib/sellershut-core/src/google.rs | 7 | ||||
-rw-r--r-- | lib/sellershut-core/src/google/helpers.rs | 125 | ||||
-rw-r--r-- | lib/sellershut-core/src/lib.rs | 13 |
7 files changed, 300 insertions, 0 deletions
diff --git a/lib/sellershut-core/Cargo.toml b/lib/sellershut-core/Cargo.toml new file mode 100644 index 0000000..b5e1bb9 --- /dev/null +++ b/lib/sellershut-core/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "sellershut-core" +version = "0.1.0" +edition = "2024" +license.workspace = true +homepage.workspace = true +documentation.workspace = true +description.workspace = true + +[dependencies] +prost = "0.13.5" +serde = { workspace = true, optional = true } +serde_json = { workspace = true, optional = true } +time = { workspace = true, optional = true } +tonic.workspace = true +tonic-types = "0.13.0" + +[features] +default = [] +auth = [] +serde = ["dep:serde", "serde/derive", "serde_json"] +time = [ + "dep:time", + "serde", + "time/serde", + "time/parsing", + "time/formatting", + "time/macros", +] + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[build-dependencies] +tonic-build = { version = "0.13.0", features = ["cleanup-markdown"] } diff --git a/lib/sellershut-core/build.rs b/lib/sellershut-core/build.rs new file mode 100644 index 0000000..8ff48f1 --- /dev/null +++ b/lib/sellershut-core/build.rs @@ -0,0 +1,81 @@ +#[cfg(feature = "auth")] +enum Entity { + #[cfg(feature = "auth")] + Auth, +} + +#[cfg(feature = "auth")] +impl Entity { + fn protos(&self) -> Vec<&'static str> { + let mut res: Vec<&'static str> = vec![]; + + match self { + #[cfg(feature = "auth")] + Entity::Auth => { + res.extend(vec!["proto/auth/auth.proto"]); + } + } + res + } +} + +fn main() -> Result<(), Box<dyn std::error::Error>> { + println!("cargo:rerun-if-changed=proto"); + + #[cfg(feature = "auth")] + build_proto("auth", Entity::Auth); + + Ok(()) +} + +#[cfg(feature = "auth")] +fn build_proto(package: &str, entity: Entity) { + let out_dir = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap()); + + let config = tonic_build::configure().compile_well_known_types(true) + .server_mod_attribute( + package, + format!("#[cfg(feature = \"{package}\")] #[cfg_attr(docsrs, doc(cfg(feature = \"{package}\")))]"), + ) + .client_mod_attribute( + package, + format!("#[cfg(feature = \"{package}\")] #[cfg_attr(docsrs, doc(cfg(feature = \"{package}\")))]"), + ); + + #[cfg(feature = "serde")] + let config = add_serde(config); + + #[cfg(feature = "time")] + let config = config + .type_attribute( + ".google.protobuf.Timestamp", + "#[serde(try_from = \"crate::google::helpers::DateItem\")] #[serde(into = \"String\")]", + ) + .type_attribute( + ".google.type.Date", + "#[serde(try_from = \"crate::google::helpers::DateItem\")] #[serde(into = \"String\")]", + ); + + let include_paths = &["proto"]; + + config + .file_descriptor_set_path(out_dir.join(format!("{package}_descriptor.bin"))) + .server_mod_attribute( + package, + format!("#[cfg(feature = \"{package}\")] #[cfg_attr(docsrs, doc(cfg(feature = \"{package}\")))]"), + ) + .client_mod_attribute( + package, + format!("#[cfg(feature = \"{package}\")] #[cfg_attr(docsrs, doc(cfg(feature = \"{package}\")))]"), + ) + .compile_well_known_types(true) + .compile_protos(&entity.protos(), include_paths).unwrap(); +} + +#[cfg(all(feature = "serde", feature = "auth",))] +fn add_serde(config: tonic_build::Builder) -> tonic_build::Builder { + config.type_attribute( + ".", + "#[derive(serde::Serialize, serde::Deserialize)] #[serde(rename_all = \"snake_case\")]", + ) +} diff --git a/lib/sellershut-core/proto/auth/auth.proto b/lib/sellershut-core/proto/auth/auth.proto new file mode 100644 index 0000000..5e340b3 --- /dev/null +++ b/lib/sellershut-core/proto/auth/auth.proto @@ -0,0 +1,35 @@ +syntax = "proto3"; + +package auth; + +import "google/protobuf/timestamp.proto"; + +// A message representing a user +message User { + // Unique identifier for the user + string id = 1; + // Email address of the user + string email = 2; + // Timestamp for when the user was created + google.protobuf.Timestamp created_at = 3; + // Timestamp for when the user was last updated + google.protobuf.Timestamp updated_at = 4; +} + +// Define a message for sending a token to be validated +message ValidationRequest { + // The token to validate + string token = 1; +} + +// Define a message for the result of a token validation +message ValidationResponse { + // Indicates whether the token is valid + bool valid = 1; +} + +// Define the AuthServer gRPC service +service Auth { + // Validate a token + rpc ValidateAuthToken (ValidationRequest) returns (ValidationResponse); +} diff --git a/lib/sellershut-core/src/auth.rs b/lib/sellershut-core/src/auth.rs new file mode 100644 index 0000000..d555aab --- /dev/null +++ b/lib/sellershut-core/src/auth.rs @@ -0,0 +1,3 @@ +tonic::include_proto!("auth"); +/// Auth file descriptor +pub const AUTH_FILE_DESCRIPTOR_SET: &[u8] = tonic::include_file_descriptor_set!("auth_descriptor"); diff --git a/lib/sellershut-core/src/google.rs b/lib/sellershut-core/src/google.rs new file mode 100644 index 0000000..b28aee0 --- /dev/null +++ b/lib/sellershut-core/src/google.rs @@ -0,0 +1,7 @@ +/// Well known types +pub mod protobuf { + include!(concat!(env!("OUT_DIR"), "/google.protobuf.rs")); +} + +#[cfg(feature = "time")] +mod helpers; diff --git a/lib/sellershut-core/src/google/helpers.rs b/lib/sellershut-core/src/google/helpers.rs new file mode 100644 index 0000000..2d75655 --- /dev/null +++ b/lib/sellershut-core/src/google/helpers.rs @@ -0,0 +1,125 @@ +use crate::google::protobuf::Timestamp; + +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(untagged)] +/// Date utility +#[derive(Clone, Debug)] +pub enum DateItem { + /// string + String(String), + /// ts + Timestamp { seconds: i64, nanos: i32 }, +} + +impl TryFrom<DateItem> for Timestamp { + type Error = time::Error; + + fn try_from(value: DateItem) -> Result<Self, Self::Error> { + match value { + DateItem::String(ref string) => <Timestamp as std::str::FromStr>::from_str(string), + DateItem::Timestamp { seconds, nanos } => Ok(Self { seconds, nanos }), + } + } +} + +impl From<time::OffsetDateTime> for Timestamp { + fn from(dt: time::OffsetDateTime) -> Self { + Timestamp { + seconds: dt.unix_timestamp(), + nanos: dt.nanosecond() as i32, + } + } +} + +impl From<Timestamp> for String { + fn from(value: Timestamp) -> Self { + let odt = time::OffsetDateTime::try_from(value).expect("invalid date"); + odt.format(&time::format_description::well_known::Rfc3339) + .expect("format is not rfc3339") + } +} + +impl std::str::FromStr for Timestamp { + type Err = time::Error; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + let timestamp = + time::OffsetDateTime::parse(s, &time::format_description::well_known::Rfc3339)?; + + Ok(Timestamp::from(timestamp)) + } +} + +impl TryFrom<String> for Timestamp { + type Error = time::Error; + + fn try_from(value: String) -> Result<Self, Self::Error> { + <Timestamp as std::str::FromStr>::from_str(&value) + } +} + +impl TryFrom<Timestamp> for time::OffsetDateTime { + type Error = time::Error; + + fn try_from(value: Timestamp) -> Result<Self, Self::Error> { + let dt = time::OffsetDateTime::from_unix_timestamp(value.seconds)?; + + Ok(dt.replace_nanosecond(value.nanos as u32)?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use time::{Duration, OffsetDateTime}; + + #[test] + fn test_offsetdatetime_to_timestamp() { + let now = OffsetDateTime::now_utc(); + let timestamp: Timestamp = now.into(); + + assert_eq!(timestamp.seconds, now.unix_timestamp()); + assert_eq!(timestamp.nanos, now.nanosecond() as i32); + } + + #[test] + fn test_timestamp_to_offsetdatetime() { + let now = OffsetDateTime::now_utc(); + let timestamp: Timestamp = now.into(); + let dt: OffsetDateTime = timestamp.try_into().unwrap(); + + assert_eq!(dt, now); + } + + #[test] + fn test_timestamp_to_offsetdatetime_with_nanos() { + let now = OffsetDateTime::now_utc(); + let nanos = 123456789; + let dt = now + Duration::nanoseconds(nanos); + let timestamp: Timestamp = dt.into(); + let dt_from_timestamp: OffsetDateTime = timestamp.try_into().unwrap(); + + assert_eq!(dt_from_timestamp, dt); + } + + #[test] + fn test_timestamp_to_offsetdatetime_with_negative_nanos() { + let now = OffsetDateTime::now_utc(); + let nanos = -123456789; + let dt = now + Duration::nanoseconds(nanos); + let timestamp: Timestamp = dt.into(); + let dt_from_timestamp: OffsetDateTime = timestamp.try_into().unwrap(); + + assert_eq!(dt_from_timestamp, dt); + } + + #[test] + fn test_timestamp_to_offsetdatetime_invalid_seconds() { + let timestamp = Timestamp { + seconds: i64::MIN, + nanos: 0, + }; + let result: Result<OffsetDateTime, time::Error> = timestamp.try_into(); + assert!(result.is_err()); + } +} diff --git a/lib/sellershut-core/src/lib.rs b/lib/sellershut-core/src/lib.rs new file mode 100644 index 0000000..ee2ed9b --- /dev/null +++ b/lib/sellershut-core/src/lib.rs @@ -0,0 +1,13 @@ +//! Core +#![cfg_attr(docsrs, feature(doc_cfg))] +#![warn( + missing_docs, + rustdoc::broken_intra_doc_links, + missing_debug_implementations +)] + +/// Protobuf types +#[cfg(feature = "auth")] +pub mod google; + +pub mod auth; |