use crate::core::{config::Config, observability::OperationTracker, SDKResult};
use std::future::Future;
pub trait Service {
fn config(&self) -> &Config;
fn service_name() -> &'static str
where
Self: Sized;
fn service_version() -> &'static str
where
Self: Sized,
{
"v1"
}
}
pub trait ServiceObservability: Service {
fn log_operation_start(&self, operation: &str, params: Option<&str>)
where
Self: Sized,
{
tracing::debug!(
service_name = <Self as Service>::service_name(),
operation = operation,
params = params,
"Starting service operation"
);
}
fn log_operation_success(&self, operation: &str, duration_ms: u64)
where
Self: Sized,
{
tracing::info!(
service_name = <Self as Service>::service_name(),
operation = operation,
duration_ms = duration_ms,
"Service operation completed successfully"
);
}
fn log_operation_error(&self, operation: &str, error: &str, duration_ms: u64)
where
Self: Sized,
{
tracing::error!(
service_name = <Self as Service>::service_name(),
operation = operation,
duration_ms = duration_ms,
error = error,
"Service operation failed"
);
}
}
pub trait AsyncServiceOperation<T, R>: Service
where
T: Send + Sync,
R: Send + Sync,
{
fn execute_with_observability<F, Fut>(
&self,
operation_name: &str,
operation: F,
) -> impl Future<Output = SDKResult<R>> + Send
where
F: FnOnce() -> Fut + Send,
Fut: Future<Output = SDKResult<R>> + Send,
Self: ServiceObservability + Sync + Sized,
{
let service_name = <Self as Service>::service_name();
async move {
let tracker = OperationTracker::start(service_name, operation_name);
match operation().await {
Ok(result) => {
tracker.success();
Ok(result)
}
Err(err) => {
tracker.error(&err.to_string());
Err(err)
}
}
}
}
}
pub trait ServiceBuilder<S: Service> {
fn build(config: Config) -> S;
fn build_default() -> S
where
Config: Default,
{
Self::build(Config::default())
}
}
pub trait ServiceHealthCheck: Service {
fn health_check(&self) -> impl Future<Output = SDKResult<ServiceHealthStatus>> + Send;
fn status_summary(&self) -> ServiceStatusSummary
where
Self: Sized,
{
ServiceStatusSummary {
service_name: <Self as Service>::service_name().to_string(),
version: <Self as Service>::service_version().to_string(),
config_valid: self.is_config_valid(),
}
}
fn is_config_valid(&self) -> bool {
let config = self.config();
!config.app_id.is_empty() && !config.app_secret.is_empty()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ServiceHealthStatus {
Healthy,
Degraded(String),
Unhealthy(String),
}
#[derive(Debug, Clone)]
pub struct ServiceStatusSummary {
pub service_name: String,
pub version: String,
pub config_valid: bool,
}
pub trait ConfigurableService: Service {
fn update_config(&mut self, new_config: Config) -> SDKResult<()>;
fn validate_config(&self, config: &Config) -> SDKResult<()> {
if config.app_id.is_empty() {
return Err(crate::core::error::LarkAPIError::IllegalParamError(
"app_id cannot be empty".to_string(),
));
}
if config.app_secret.is_empty() {
return Err(crate::core::error::LarkAPIError::IllegalParamError(
"app_secret cannot be empty".to_string(),
));
}
Ok(())
}
}
pub trait CacheableService: Service {
type CacheKey: Send + Sync + std::hash::Hash + Eq + Clone;
type CacheValue: Send + Sync + Clone;
fn get_from_cache(&self, key: &Self::CacheKey) -> Option<Self::CacheValue>;
fn put_to_cache(&self, key: Self::CacheKey, value: Self::CacheValue);
fn clear_cache(&self);
fn cache_ttl(&self) -> u64 {
300 }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::config::Config;
struct TestService {
config: Config,
}
impl Service for TestService {
fn config(&self) -> &Config {
&self.config
}
fn service_name() -> &'static str {
"test_service"
}
fn service_version() -> &'static str {
"v1.0"
}
}
impl ServiceObservability for TestService {}
impl ServiceBuilder<TestService> for TestService {
fn build(config: Config) -> TestService {
TestService { config }
}
}
impl AsyncServiceOperation<(), String> for TestService {}
#[test]
fn test_service_creation() {
let config = Config::builder()
.app_id("test_app")
.app_secret("test_secret")
.build();
let service = TestService::build(config.clone());
assert_eq!(service.config().app_id, "test_app");
assert_eq!(service.config().app_secret, "test_secret");
assert_eq!(TestService::service_name(), "test_service");
assert_eq!(TestService::service_version(), "v1.0");
}
#[test]
fn test_service_status_summary() {
let config = Config::builder()
.app_id("test_app")
.app_secret("test_secret")
.build();
let service = TestService::build(config);
struct HealthCheckableTestService {
inner: TestService,
}
impl Service for HealthCheckableTestService {
fn config(&self) -> &Config {
self.inner.config()
}
fn service_name() -> &'static str {
TestService::service_name()
}
fn service_version() -> &'static str {
TestService::service_version()
}
}
impl ServiceHealthCheck for HealthCheckableTestService {
async fn health_check(&self) -> SDKResult<ServiceHealthStatus> {
Ok(ServiceHealthStatus::Healthy)
}
}
let health_service = HealthCheckableTestService { inner: service };
let summary = health_service.status_summary();
assert_eq!(summary.service_name, "test_service");
assert_eq!(summary.version, "v1.0");
assert!(summary.config_valid);
}
#[test]
fn test_config_validation() {
let config = Config::builder()
.app_id("test_app")
.app_secret("test_secret")
.build();
let service = TestService::build(config);
struct ConfigurableTestService {
inner: TestService,
}
impl Service for ConfigurableTestService {
fn config(&self) -> &Config {
self.inner.config()
}
fn service_name() -> &'static str {
TestService::service_name()
}
}
impl ConfigurableService for ConfigurableTestService {
fn update_config(&mut self, new_config: Config) -> SDKResult<()> {
self.validate_config(&new_config)?;
self.inner.config = new_config;
Ok(())
}
}
let configurable_service = ConfigurableTestService { inner: service };
let valid_config = Config::builder()
.app_id("new_app")
.app_secret("new_secret")
.build();
assert!(configurable_service.validate_config(&valid_config).is_ok());
let invalid_config = Config::builder().app_id("").app_secret("secret").build();
assert!(configurable_service
.validate_config(&invalid_config)
.is_err());
let invalid_config = Config::builder().app_id("app").app_secret("").build();
assert!(configurable_service
.validate_config(&invalid_config)
.is_err());
}
#[test]
fn test_service_builder_default() {
let service = TestService::build_default();
assert!(service.config().app_id.is_empty());
}
#[tokio::test]
async fn test_async_service_operation() {
let config = Config::builder()
.app_id("test_app")
.app_secret("test_secret")
.build();
let service = TestService::build(config);
let result = service
.execute_with_observability("test_operation", || async { Ok("success".to_string()) })
.await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), "success");
let result = service
.execute_with_observability("test_operation_fail", || async {
Err(crate::core::error::LarkAPIError::IllegalParamError(
"test error".to_string(),
))
})
.await;
assert!(result.is_err());
}
}