Skip to main content

vox_types/
method_identity.rs

1use std::collections::HashSet;
2
3use facet::Facet;
4use facet_core::{Def, Shape, Type, UserType};
5use heck::ToKebabCase;
6
7use crate::{ArgDescriptor, MethodDescriptor, MethodId, RetryPolicy, is_rx, is_tx};
8
9/// Compute a method ID from service and method names.
10///
11/// Method IDs depend only on names, not on type signatures. This enables
12/// schema exchange — two peers can use different type versions and still
13/// route calls to the correct method.
14// r[impl schema.method-id]
15pub fn method_id_name_only(service_name: &str, method_name: &str) -> MethodId {
16    let mut input = Vec::new();
17    input.extend_from_slice(service_name.to_kebab_case().as_bytes());
18    input.push(b'.');
19    input.extend_from_slice(method_name.to_kebab_case().as_bytes());
20    let h = blake3::hash(&input);
21    let first8: [u8; 8] = h.as_bytes()[0..8].try_into().expect("slice len");
22    MethodId(u64::from_le_bytes(first8))
23}
24
25/// Build and leak a `MethodDescriptor` with default volatile retry policy.
26pub fn method_descriptor<'a, 'r, A: Facet<'a>, R: Facet<'r>>(
27    service_name: &'static str,
28    method_name: &'static str,
29    arg_names: &[&'static str],
30    doc: Option<&'static str>,
31) -> &'static MethodDescriptor {
32    method_descriptor_with_retry::<A, R>(
33        service_name,
34        method_name,
35        arg_names,
36        doc,
37        RetryPolicy::VOLATILE,
38    )
39}
40
41/// Build and leak a `MethodDescriptor` with an explicit retry policy.
42pub fn method_descriptor_with_retry<'a, 'r, A: Facet<'a>, R: Facet<'r>>(
43    service_name: &'static str,
44    method_name: &'static str,
45    arg_names: &[&'static str],
46    doc: Option<&'static str>,
47    retry: RetryPolicy,
48) -> &'static MethodDescriptor {
49    assert!(
50        !shape_contains_channel(R::SHAPE),
51        "channels are not allowed in return types: {service_name}.{method_name}"
52    );
53    assert!(
54        !(retry.persist && shape_contains_channel(A::SHAPE)),
55        "persist methods cannot carry channels: {service_name}.{method_name}"
56    );
57
58    let id = method_id_name_only(service_name, method_name);
59
60    let arg_shapes: &[&'static Shape] = match A::SHAPE.ty {
61        Type::User(UserType::Struct(s)) => {
62            let fields: Vec<&'static Shape> = s.fields.iter().map(|f| f.shape()).collect();
63            Box::leak(fields.into_boxed_slice())
64        }
65        _ => &[],
66    };
67
68    assert_eq!(
69        arg_names.len(),
70        arg_shapes.len(),
71        "arg_names length mismatch for {service_name}.{method_name}"
72    );
73
74    let args: &'static [ArgDescriptor] = Box::leak(
75        arg_names
76            .iter()
77            .zip(arg_shapes.iter())
78            .map(|(&name, &shape)| ArgDescriptor { name, shape })
79            .collect::<Vec<_>>()
80            .into_boxed_slice(),
81    );
82
83    Box::leak(Box::new(MethodDescriptor {
84        id,
85        service_name,
86        method_name,
87        args_shape: A::SHAPE,
88        args,
89        return_shape: R::SHAPE,
90        retry,
91        doc,
92    }))
93}
94
95pub fn shape_contains_channel(shape: &'static Shape) -> bool {
96    fn visit(shape: &'static Shape, seen: &mut HashSet<&'static Shape>) -> bool {
97        if is_tx(shape) || is_rx(shape) {
98            return true;
99        }
100
101        if !seen.insert(shape) {
102            return false;
103        }
104
105        if let Some(inner) = shape.inner
106            && visit(inner, seen)
107        {
108            return true;
109        }
110
111        if shape.type_params.iter().any(|t| visit(t.shape, seen)) {
112            return true;
113        }
114
115        match shape.def {
116            Def::List(list_def) => visit(list_def.t(), seen),
117            Def::Array(array_def) => visit(array_def.t(), seen),
118            Def::Slice(slice_def) => visit(slice_def.t(), seen),
119            Def::Map(map_def) => visit(map_def.k(), seen) || visit(map_def.v(), seen),
120            Def::Set(set_def) => visit(set_def.t(), seen),
121            Def::Option(opt_def) => visit(opt_def.t(), seen),
122            Def::Result(result_def) => visit(result_def.t(), seen) || visit(result_def.e(), seen),
123            Def::Pointer(ptr_def) => ptr_def.pointee.is_some_and(|p| visit(p, seen)),
124            _ => match shape.ty {
125                Type::User(UserType::Struct(s)) => s.fields.iter().any(|f| visit(f.shape(), seen)),
126                Type::User(UserType::Enum(e)) => e
127                    .variants
128                    .iter()
129                    .any(|v| v.data.fields.iter().any(|f| visit(f.shape(), seen))),
130                _ => false,
131            },
132        }
133    }
134
135    let mut seen = HashSet::new();
136    visit(shape, &mut seen)
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn method_id_name_only_is_stable_across_case_variations() {
145        let a = method_id_name_only("MyService", "DoThingFast");
146        let b = method_id_name_only("my-service", "do-thing-fast");
147        let c = method_id_name_only("MY_SERVICE", "DO_THING_FAST");
148        assert_eq!(a, b);
149        assert_eq!(b, c);
150    }
151
152    #[test]
153    fn method_id_name_only_different_methods_produce_different_ids() {
154        let a = method_id_name_only("Svc", "alpha");
155        let b = method_id_name_only("Svc", "beta");
156        assert_ne!(a, b);
157    }
158}