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    let args_have_channels = shape_contains_channel(A::SHAPE);
54    assert!(
55        !(retry.persist && args_have_channels),
56        "persist methods cannot carry channels: {service_name}.{method_name}"
57    );
58
59    let id = method_id_name_only(service_name, method_name);
60
61    let arg_shapes: &[&'static Shape] = match A::SHAPE.ty {
62        Type::User(UserType::Struct(s)) => {
63            let fields: Vec<&'static Shape> = s.fields.iter().map(|f| f.shape()).collect();
64            Box::leak(fields.into_boxed_slice())
65        }
66        _ => &[],
67    };
68
69    assert_eq!(
70        arg_names.len(),
71        arg_shapes.len(),
72        "arg_names length mismatch for {service_name}.{method_name}"
73    );
74
75    let args: &'static [ArgDescriptor] = Box::leak(
76        arg_names
77            .iter()
78            .zip(arg_shapes.iter())
79            .map(|(&name, &shape)| ArgDescriptor { name, shape })
80            .collect::<Vec<_>>()
81            .into_boxed_slice(),
82    );
83
84    Box::leak(Box::new(MethodDescriptor {
85        id,
86        service_name,
87        method_name,
88        args_shape: A::SHAPE,
89        args,
90        return_shape: R::SHAPE,
91        args_have_channels,
92        retry,
93        doc,
94    }))
95}
96
97pub fn shape_contains_channel(shape: &'static Shape) -> bool {
98    fn visit(shape: &'static Shape, seen: &mut HashSet<&'static Shape>) -> bool {
99        if is_tx(shape) || is_rx(shape) {
100            return true;
101        }
102
103        if !seen.insert(shape) {
104            return false;
105        }
106
107        if let Some(inner) = shape.inner
108            && visit(inner, seen)
109        {
110            return true;
111        }
112
113        if shape.type_params.iter().any(|t| visit(t.shape, seen)) {
114            return true;
115        }
116
117        match shape.def {
118            Def::List(list_def) => visit(list_def.t(), seen),
119            Def::Array(array_def) => visit(array_def.t(), seen),
120            Def::Slice(slice_def) => visit(slice_def.t(), seen),
121            Def::Map(map_def) => visit(map_def.k(), seen) || visit(map_def.v(), seen),
122            Def::Set(set_def) => visit(set_def.t(), seen),
123            Def::Option(opt_def) => visit(opt_def.t(), seen),
124            Def::Result(result_def) => visit(result_def.t(), seen) || visit(result_def.e(), seen),
125            Def::Pointer(ptr_def) => ptr_def.pointee.is_some_and(|p| visit(p, seen)),
126            _ => match shape.ty {
127                Type::User(UserType::Struct(s)) => s.fields.iter().any(|f| visit(f.shape(), seen)),
128                Type::User(UserType::Enum(e)) => e
129                    .variants
130                    .iter()
131                    .any(|v| v.data.fields.iter().any(|f| visit(f.shape(), seen))),
132                _ => false,
133            },
134        }
135    }
136
137    let mut seen = HashSet::new();
138    visit(shape, &mut seen)
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn method_id_name_only_is_stable_across_case_variations() {
147        let a = method_id_name_only("MyService", "DoThingFast");
148        let b = method_id_name_only("my-service", "do-thing-fast");
149        let c = method_id_name_only("MY_SERVICE", "DO_THING_FAST");
150        assert_eq!(a, b);
151        assert_eq!(b, c);
152    }
153
154    #[test]
155    fn method_id_name_only_different_methods_produce_different_ids() {
156        let a = method_id_name_only("Svc", "alpha");
157        let b = method_id_name_only("Svc", "beta");
158        assert_ne!(a, b);
159    }
160}