use std::{
any::{Any, TypeId},
collections::HashSet,
sync::Arc,
};
use saddle_core::{CallContext, ErrorKind, ModuleId, OperationId, Result, SaddleError, ServiceId};
use saddle_observability::{CallKind, Observer};
use crate::{Service, ServiceHandler};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ServiceDescriptor {
module: ModuleId,
service: ServiceId,
operation: OperationId,
}
impl ServiceDescriptor {
pub fn new(
module: impl Into<ModuleId>,
service: impl Into<ServiceId>,
operation: impl Into<OperationId>,
) -> Self {
Self {
module: module.into(),
service: service.into(),
operation: operation.into(),
}
}
pub fn module(&self) -> &ModuleId {
&self.module
}
pub fn service(&self) -> &ServiceId {
&self.service
}
pub fn operation(&self) -> &OperationId {
&self.operation
}
fn identity(&self) -> ServiceIdentity {
ServiceIdentity {
module: self.module.to_string(),
service: self.service.to_string(),
operation: self.operation.to_string(),
}
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
struct ServiceIdentity {
module: String,
service: String,
operation: String,
}
struct Entry {
contract: TypeId,
descriptor: ServiceDescriptor,
implementation: Arc<dyn Any + Send + Sync>,
}
struct TypedImplementation<S: Service> {
handler: Arc<dyn ServiceHandler<S>>,
}
type Factory = Box<dyn FnOnce(&ServiceResolver<'_>) -> Result<Arc<dyn Any + Send + Sync>> + Send>;
struct Definition {
contract: TypeId,
descriptor: ServiceDescriptor,
factory: Factory,
}
pub struct ServiceRegistryBuilder {
definitions: Vec<Definition>,
contracts: HashSet<TypeId>,
identities: HashSet<ServiceIdentity>,
}
impl ServiceRegistryBuilder {
pub fn new() -> Self {
Self {
definitions: Vec::new(),
contracts: HashSet::new(),
identities: HashSet::new(),
}
}
pub fn register<S, H>(&mut self, descriptor: ServiceDescriptor, handler: H) -> Result<()>
where
S: Service,
H: ServiceHandler<S>,
{
self.register_with::<S, H, _>(descriptor, move |_| Ok(handler))
}
pub fn register_with<S, H, F>(
&mut self,
descriptor: ServiceDescriptor,
factory: F,
) -> Result<()>
where
S: Service,
H: ServiceHandler<S>,
F: FnOnce(&ServiceResolver<'_>) -> Result<H> + Send + 'static,
{
let contract = TypeId::of::<S>();
if !self.contracts.insert(contract) {
return Err(registration_error(
"service.duplicate_contract",
"the Service contract is already registered",
));
}
if !self.identities.insert(descriptor.identity()) {
self.contracts.remove(&contract);
return Err(registration_error(
"service.duplicate_identity",
"the Service identity is already registered",
));
}
self.definitions.push(Definition {
contract,
descriptor,
factory: Box::new(move |resolver| {
let handler = factory(resolver)?;
Ok(Arc::new(TypedImplementation::<S> {
handler: Arc::new(handler),
}))
}),
});
Ok(())
}
pub fn build(self, observer: Observer) -> Result<ServiceRegistry> {
let mut entries = Vec::with_capacity(self.definitions.len());
for definition in self.definitions {
let implementation = {
let resolver = ServiceResolver {
entries: &entries,
observer: &observer,
};
(definition.factory)(&resolver)?
};
entries.push(Entry {
contract: definition.contract,
descriptor: definition.descriptor,
implementation,
});
}
Ok(ServiceRegistry {
inner: Arc::new(RegistryInner { entries }),
observer,
})
}
}
impl Default for ServiceRegistryBuilder {
fn default() -> Self {
Self::new()
}
}
pub struct ServiceResolver<'a> {
entries: &'a [Entry],
observer: &'a Observer,
}
impl ServiceResolver<'_> {
pub fn client<S>(&self) -> Result<ServiceClient<S>>
where
S: Service,
{
client_from_entries(self.entries, self.observer.clone())
}
}
struct RegistryInner {
entries: Vec<Entry>,
}
#[derive(Clone)]
pub struct ServiceRegistry {
inner: Arc<RegistryInner>,
observer: Observer,
}
impl ServiceRegistry {
pub fn client<S>(&self) -> Result<ServiceClient<S>>
where
S: Service,
{
client_from_entries(&self.inner.entries, self.observer.clone())
}
pub(crate) fn observer(&self) -> Observer {
self.observer.clone()
}
}
fn client_from_entries<S>(entries: &[Entry], observer: Observer) -> Result<ServiceClient<S>>
where
S: Service,
{
let entry = entries
.iter()
.find(|entry| entry.contract == TypeId::of::<S>())
.ok_or_else(missing_dependency)?;
let implementation = Arc::clone(&entry.implementation)
.downcast::<TypedImplementation<S>>()
.map_err(|_| {
SaddleError::new(
ErrorKind::Internal,
"service.registry_corrupt",
"the Service registry contains an invalid implementation",
)
})?;
Ok(ServiceClient {
descriptor: entry.descriptor.clone(),
implementation: Arc::clone(&implementation.handler),
observer,
})
}
pub struct ServiceClient<S: Service> {
descriptor: ServiceDescriptor,
implementation: Arc<dyn ServiceHandler<S>>,
observer: Observer,
}
impl<S: Service> Clone for ServiceClient<S> {
fn clone(&self) -> Self {
Self {
descriptor: self.descriptor.clone(),
implementation: Arc::clone(&self.implementation),
observer: self.observer.clone(),
}
}
}
impl<S: Service> ServiceClient<S> {
pub fn descriptor(&self) -> &ServiceDescriptor {
&self.descriptor
}
pub async fn call(&self, parent: &CallContext, request: S::Request) -> Result<S::Response> {
let call = self.observer.start_child_call(
parent,
CallKind::Service,
self.descriptor.module().clone(),
self.descriptor.service().clone(),
self.descriptor.operation().clone(),
);
let result = self.implementation.call(call.context(), request).await;
match &result {
Ok(_) => call.succeed(),
Err(error) => call.fail(error),
}
result
}
}
fn missing_dependency() -> SaddleError {
SaddleError::new(
ErrorKind::InvalidArgument,
"service.missing_dependency",
"a Service factory dependency is not registered before its dependent Service",
)
}
fn registration_error(code: &'static str, message: &'static str) -> SaddleError {
SaddleError::new(ErrorKind::Conflict, code, message)
}
#[cfg(test)]
pub(crate) mod tests {
use std::io;
use saddle_core::{ApplicationId, SpanId, TraceId};
use saddle_observability::ObserverConfig;
use super::*;
pub(crate) struct Echo;
impl Service for Echo {
type Request = String;
type Response = String;
}
pub(crate) struct EchoImpl;
impl ServiceHandler<Echo> for EchoImpl {
fn call<'a>(
&'a self,
_context: &'a CallContext,
request: String,
) -> crate::ServiceFuture<'a, String> {
Box::pin(async move { Ok(request) })
}
}
struct Prefix;
impl Service for Prefix {
type Request = String;
type Response = String;
}
struct PrefixImpl {
echo: ServiceClient<Echo>,
}
impl ServiceHandler<Prefix> for PrefixImpl {
fn call<'a>(
&'a self,
context: &'a CallContext,
request: String,
) -> crate::ServiceFuture<'a, String> {
Box::pin(async move {
let value = self.echo.call(context, request).await?;
Ok(format!("prefix:{value}"))
})
}
}
pub(crate) fn observer() -> Observer {
Observer::with_writer(ObserverConfig::default(), io::sink()).unwrap()
}
pub(crate) fn context() -> CallContext {
CallContext::new(
ApplicationId::from("app"),
ModuleId::from("caller"),
ServiceId::from("caller"),
OperationId::from("call"),
TraceId::from_u128(1),
SpanId::from_u64(1),
)
}
#[test]
fn rejects_contract_and_identity_conflicts() {
let mut builder = ServiceRegistryBuilder::new();
let descriptor = ServiceDescriptor::new("test", "echo", "call");
builder
.register::<Echo, _>(descriptor.clone(), EchoImpl)
.unwrap();
assert_eq!(
builder
.register::<Echo, _>(descriptor, EchoImpl)
.unwrap_err()
.code(),
"service.duplicate_contract"
);
}
#[test]
fn actual_factory_resolution_rejects_missing_dependency() {
let mut builder = ServiceRegistryBuilder::new();
builder
.register_with::<Prefix, PrefixImpl, _>(
ServiceDescriptor::new("test", "prefix", "call"),
|resolver| {
Ok(PrefixImpl {
echo: resolver.client::<Echo>()?,
})
},
)
.unwrap();
assert_eq!(
builder.build(observer()).err().unwrap().code(),
"service.missing_dependency"
);
}
#[tokio::test]
async fn factory_injects_typed_client_for_positive_service_chain() {
let observer = observer();
let mut builder = ServiceRegistryBuilder::new();
builder
.register::<Echo, _>(ServiceDescriptor::new("test", "echo", "call"), EchoImpl)
.unwrap();
builder
.register_with::<Prefix, PrefixImpl, _>(
ServiceDescriptor::new("test", "prefix", "call"),
|resolver| {
Ok(PrefixImpl {
echo: resolver.client::<Echo>()?,
})
},
)
.unwrap();
let registry = builder.build(observer).unwrap();
let client = registry.client::<Prefix>().unwrap();
assert_eq!(
client.call(&context(), "hello".to_owned()).await.unwrap(),
"prefix:hello"
);
}
}