#![doc = include_str!("../README.md")]
#![forbid(unsafe_op_in_unsafe_fn)]
pub mod introspection;
use facet_core::Shape;
use std::collections::HashMap;
use std::sync::LazyLock;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ServiceId(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct MethodId(pub u32);
impl MethodId {
pub const CONTROL: MethodId = MethodId(0);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum Encoding {
Postcard = 0,
Json = 1,
}
impl Encoding {
pub const ALL: &'static [Encoding] = &[Encoding::Postcard, Encoding::Json];
}
#[derive(Debug, Clone)]
pub struct ArgInfo {
pub name: &'static str,
pub type_name: &'static str,
}
#[derive(Debug)]
pub struct MethodEntry {
pub id: MethodId,
pub name: &'static str,
pub full_name: String,
pub doc: String,
pub args: Vec<ArgInfo>,
pub request_shape: &'static Shape,
pub response_shape: &'static Shape,
pub is_streaming: bool,
pub supported_encodings: Vec<Encoding>,
}
impl MethodEntry {
pub fn supports_encoding(&self, encoding: Encoding) -> bool {
self.supported_encodings.contains(&encoding)
}
}
#[derive(Debug)]
pub struct ServiceEntry {
pub id: ServiceId,
pub name: &'static str,
pub doc: String,
pub methods: HashMap<&'static str, MethodEntry>,
}
impl ServiceEntry {
pub fn method(&self, name: &str) -> Option<&MethodEntry> {
self.methods.get(name)
}
pub fn iter_methods(&self) -> impl Iterator<Item = &MethodEntry> {
self.methods.values()
}
}
#[derive(Debug, Default)]
pub struct ServiceRegistry {
services_by_name: HashMap<&'static str, ServiceEntry>,
methods_by_id: HashMap<MethodId, MethodLookup>,
next_service_id: u32,
}
#[derive(Debug, Clone)]
struct MethodLookup {
service_name: &'static str,
method_name: &'static str,
}
impl ServiceRegistry {
pub fn new() -> Self {
Self {
services_by_name: HashMap::new(),
methods_by_id: HashMap::new(),
next_service_id: 0,
}
}
pub fn register_service(
&mut self,
name: &'static str,
doc: impl Into<String>,
) -> ServiceBuilder<'_> {
let id = ServiceId(self.next_service_id);
self.next_service_id += 1;
ServiceBuilder {
registry: self,
service_name: name,
service_doc: doc.into(),
service_id: id,
methods: HashMap::new(),
}
}
pub fn service(&self, name: &str) -> Option<&ServiceEntry> {
self.services_by_name.get(name)
}
pub fn lookup_method(&self, service_name: &str, method_name: &str) -> Option<&MethodEntry> {
self.services_by_name
.get(service_name)
.and_then(|s| s.method(method_name))
}
pub fn method_by_id(&self, id: MethodId) -> Option<&MethodEntry> {
let lookup = self.methods_by_id.get(&id)?;
self.lookup_method(lookup.service_name, lookup.method_name)
}
pub fn resolve_method_id(&self, service_name: &str, method_name: &str) -> Option<MethodId> {
self.lookup_method(service_name, method_name).map(|m| m.id)
}
pub fn iter_services(&self) -> impl Iterator<Item = &ServiceEntry> {
self.services_by_name.values()
}
pub fn services(&self) -> impl Iterator<Item = &ServiceEntry> {
self.iter_services()
}
pub fn service_by_id(&self, id: ServiceId) -> Option<&ServiceEntry> {
self.services_by_name.values().find(|s| s.id == id)
}
pub fn service_count(&self) -> usize {
self.services_by_name.len()
}
pub fn method_count(&self) -> usize {
self.methods_by_id.len()
}
pub fn global() -> &'static parking_lot::RwLock<ServiceRegistry> {
&GLOBAL_REGISTRY
}
pub fn with_global<F, R>(f: F) -> R
where
F: FnOnce(&ServiceRegistry) -> R,
{
f(&GLOBAL_REGISTRY.read())
}
pub fn with_global_mut<F, R>(f: F) -> R
where
F: FnOnce(&mut ServiceRegistry) -> R,
{
f(&mut GLOBAL_REGISTRY.write())
}
}
fn compute_method_id(service_name: &str, method_name: &str) -> MethodId {
const FNV_OFFSET: u64 = 0xcbf29ce484222325;
const FNV_PRIME: u64 = 0x100000001b3;
let mut hash: u64 = FNV_OFFSET;
for byte in service_name.bytes() {
hash ^= byte as u64;
hash = hash.wrapping_mul(FNV_PRIME);
}
hash ^= b'.' as u64;
hash = hash.wrapping_mul(FNV_PRIME);
for byte in method_name.bytes() {
hash ^= byte as u64;
hash = hash.wrapping_mul(FNV_PRIME);
}
MethodId(((hash >> 32) ^ hash) as u32)
}
static GLOBAL_REGISTRY: LazyLock<parking_lot::RwLock<ServiceRegistry>> =
LazyLock::new(|| parking_lot::RwLock::new(ServiceRegistry::new()));
pub struct ServiceBuilder<'a> {
registry: &'a mut ServiceRegistry,
service_name: &'static str,
service_doc: String,
service_id: ServiceId,
methods: HashMap<&'static str, MethodEntry>,
}
impl ServiceBuilder<'_> {
pub fn add_method(
&mut self,
name: &'static str,
doc: impl Into<String>,
args: Vec<ArgInfo>,
request_shape: &'static Shape,
response_shape: &'static Shape,
) -> MethodId {
self.add_method_inner(name, doc.into(), args, request_shape, response_shape, false)
}
pub fn add_streaming_method(
&mut self,
name: &'static str,
doc: impl Into<String>,
args: Vec<ArgInfo>,
request_shape: &'static Shape,
response_shape: &'static Shape,
) -> MethodId {
self.add_method_inner(name, doc.into(), args, request_shape, response_shape, true)
}
fn add_method_inner(
&mut self,
name: &'static str,
doc: String,
args: Vec<ArgInfo>,
request_shape: &'static Shape,
response_shape: &'static Shape,
is_streaming: bool,
) -> MethodId {
let id = compute_method_id(self.service_name, name);
let full_name = format!("{}.{}", self.service_name, name);
if let Some(existing) = self.registry.methods_by_id.get(&id) {
panic!(
"method id collision: {:?} used by {}.{} and {}.{}",
id, existing.service_name, existing.method_name, self.service_name, name
);
}
let entry = MethodEntry {
id,
name,
full_name,
doc,
args,
request_shape,
response_shape,
is_streaming,
supported_encodings: vec![Encoding::Postcard], };
self.methods.insert(name, entry);
self.registry.methods_by_id.insert(
id,
MethodLookup {
service_name: self.service_name,
method_name: name,
},
);
id
}
pub fn finish(self) {
let entry = ServiceEntry {
id: self.service_id,
name: self.service_name,
doc: self.service_doc,
methods: self.methods,
};
self.registry
.services_by_name
.insert(self.service_name, entry);
}
}
#[cfg(test)]
mod tests {
use super::*;
use facet::Facet;
#[derive(Facet)]
struct AddRequest {
a: i32,
b: i32,
}
#[derive(Facet)]
struct AddResponse {
result: i32,
}
#[derive(Facet)]
struct RangeRequest {
n: u32,
}
#[derive(Facet)]
struct RangeItem {
value: u32,
}
#[test]
fn test_register_service() {
let mut registry = ServiceRegistry::new();
let mut builder = registry.register_service("Adder", "A simple adder service.");
let add_id = builder.add_method(
"add",
"Add two numbers together.",
vec![
ArgInfo {
name: "a",
type_name: "i32",
},
ArgInfo {
name: "b",
type_name: "i32",
},
],
<AddRequest as Facet>::SHAPE,
<AddResponse as Facet>::SHAPE,
);
builder.finish();
assert_eq!(registry.service_count(), 1);
assert_eq!(registry.method_count(), 1);
let service = registry.service("Adder").unwrap();
assert_eq!(service.name, "Adder");
assert_eq!(service.doc, "A simple adder service.");
assert_eq!(service.id.0, 0);
let method = service.method("add").unwrap();
assert_eq!(method.id, add_id);
assert_eq!(method.name, "add");
assert_eq!(method.full_name, "Adder.add");
assert_eq!(method.doc, "Add two numbers together.");
assert!(!method.is_streaming);
assert_eq!(method.args.len(), 2);
assert_eq!(method.args[0].name, "a");
assert_eq!(method.args[1].name, "b");
}
#[test]
fn test_register_multiple_services() {
let mut registry = ServiceRegistry::new();
let mut builder = registry.register_service("Adder", "");
let add_id = builder.add_method(
"add",
"",
vec![
ArgInfo {
name: "a",
type_name: "i32",
},
ArgInfo {
name: "b",
type_name: "i32",
},
],
<AddRequest as Facet>::SHAPE,
<AddResponse as Facet>::SHAPE,
);
builder.finish();
let mut builder = registry.register_service("RangeService", "");
let range_id = builder.add_streaming_method(
"range",
"",
vec![ArgInfo {
name: "n",
type_name: "u32",
}],
<RangeRequest as Facet>::SHAPE,
<RangeItem as Facet>::SHAPE,
);
builder.finish();
assert_eq!(registry.service_count(), 2);
assert_eq!(registry.method_count(), 2);
assert_ne!(add_id, range_id);
assert_eq!(add_id, compute_method_id("Adder", "add"));
assert_eq!(range_id, compute_method_id("RangeService", "range"));
assert_ne!(add_id, MethodId::CONTROL);
assert_ne!(range_id, MethodId::CONTROL);
let method = registry.lookup_method("RangeService", "range").unwrap();
assert!(method.is_streaming);
let method = registry.method_by_id(range_id).unwrap();
assert_eq!(method.full_name, "RangeService.range");
}
#[test]
fn test_resolve_method_id() {
let mut registry = ServiceRegistry::new();
let mut builder = registry.register_service("Adder", "");
builder.add_method(
"add",
"",
vec![
ArgInfo {
name: "a",
type_name: "i32",
},
ArgInfo {
name: "b",
type_name: "i32",
},
],
<AddRequest as Facet>::SHAPE,
<AddResponse as Facet>::SHAPE,
);
builder.finish();
let id = registry.resolve_method_id("Adder", "add").unwrap();
assert_eq!(id, compute_method_id("Adder", "add"));
assert_ne!(id, MethodId::CONTROL);
assert!(registry.resolve_method_id("Adder", "subtract").is_none());
assert!(registry.resolve_method_id("Calculator", "add").is_none());
}
#[test]
fn test_method_id_zero_reserved() {
assert_eq!(MethodId::CONTROL.0, 0);
let mut registry = ServiceRegistry::new();
let mut builder = registry.register_service("Test", "");
let first_method_id = builder.add_method(
"test",
"",
vec![],
<AddRequest as Facet>::SHAPE,
<AddResponse as Facet>::SHAPE,
);
builder.finish();
assert_eq!(first_method_id, compute_method_id("Test", "test"));
assert_ne!(first_method_id, MethodId::CONTROL);
}
#[test]
fn test_encoding_support() {
let mut registry = ServiceRegistry::new();
let mut builder = registry.register_service("Adder", "");
builder.add_method(
"add",
"",
vec![
ArgInfo {
name: "a",
type_name: "i32",
},
ArgInfo {
name: "b",
type_name: "i32",
},
],
<AddRequest as Facet>::SHAPE,
<AddResponse as Facet>::SHAPE,
);
builder.finish();
let method = registry.lookup_method("Adder", "add").unwrap();
assert!(method.supports_encoding(Encoding::Postcard));
assert!(!method.supports_encoding(Encoding::Json));
}
#[test]
fn test_shapes_are_present() {
let mut registry = ServiceRegistry::new();
let mut builder = registry.register_service("Adder", "");
builder.add_method(
"add",
"",
vec![
ArgInfo {
name: "a",
type_name: "i32",
},
ArgInfo {
name: "b",
type_name: "i32",
},
],
<AddRequest as Facet>::SHAPE,
<AddResponse as Facet>::SHAPE,
);
builder.finish();
let method = registry.lookup_method("Adder", "add").unwrap();
assert!(!method.request_shape.type_identifier.is_empty());
assert!(!method.response_shape.type_identifier.is_empty());
}
#[test]
fn test_docs_captured() {
let mut registry = ServiceRegistry::new();
let service_doc = "This is the service documentation.\nIt can span multiple lines.";
let method_doc = "This method adds two numbers.\n\n# Arguments\n* `a` - First number\n* `b` - Second number";
let mut builder = registry.register_service("Calculator", service_doc);
builder.add_method(
"add",
method_doc,
vec![
ArgInfo {
name: "a",
type_name: "i32",
},
ArgInfo {
name: "b",
type_name: "i32",
},
],
<AddRequest as Facet>::SHAPE,
<AddResponse as Facet>::SHAPE,
);
builder.finish();
let service = registry.service("Calculator").unwrap();
assert_eq!(service.doc, service_doc);
let method = service.method("add").unwrap();
assert_eq!(method.doc, method_doc);
}
#[test]
fn test_global_registry() {
ServiceRegistry::with_global_mut(|registry| {
let mut builder = registry.register_service("GlobalTestService", "Test service");
builder.add_method(
"test_method",
"Test method",
vec![],
<AddRequest as Facet>::SHAPE,
<AddResponse as Facet>::SHAPE,
);
builder.finish();
});
ServiceRegistry::with_global(|registry| {
let service = registry.service("GlobalTestService").unwrap();
assert_eq!(service.name, "GlobalTestService");
assert_eq!(service.doc, "Test service");
let method = service.method("test_method").unwrap();
assert_eq!(method.name, "test_method");
});
}
#[test]
fn test_global_registry_method_by_id() {
let method_id = ServiceRegistry::with_global_mut(|registry| {
let mut builder = registry.register_service("MethodIdTest", "");
let id = builder.add_method(
"lookup_test",
"",
vec![],
<AddRequest as Facet>::SHAPE,
<AddResponse as Facet>::SHAPE,
);
builder.finish();
id
});
ServiceRegistry::with_global(|registry| {
let method = registry.method_by_id(method_id).unwrap();
assert_eq!(method.full_name, "MethodIdTest.lookup_test");
});
}
}