Skip to main content

heddle_api/
transport.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use crate::heddle::api::v1alpha1::{
4    AuthorizationAccess, CallContext, DeploymentTarget, ErrorDetail, ErrorReason,
5    HumanVerificationChallenge, RetryBehavior, RpcEffect, ServiceMaturity, SigningTier,
6    error_detail,
7};
8
9/// Production ALPN for the first transport-neutral hosted-call protocol.
10pub const HOSTED_ALPN_V1: &[u8] = b"heddle-api/1";
11
12/// Production ALPN for an opaque-ticket provider extent transfer.
13pub const PROVIDER_ALPN_V1: &[u8] = b"heddle-provider/1";
14
15/// Build an `ErrorDetail` carrying a human-verification challenge (policy-denied
16/// with an actionable challenge), for the `ErrorDetail.human_verification` arm.
17pub fn human_verification_error_detail(challenge: HumanVerificationChallenge) -> ErrorDetail {
18    ErrorDetail {
19        reason: ErrorReason::PolicyDenied as i32,
20        resource: String::new(),
21        field: String::new(),
22        context: Some(error_detail::Context::HumanVerification(challenge)),
23    }
24}
25
26/// Extract a human-verification challenge from an `ErrorDetail`, if present.
27pub fn human_verification_challenge(detail: &ErrorDetail) -> Option<HumanVerificationChallenge> {
28    match &detail.context {
29        Some(error_detail::Context::HumanVerification(challenge)) => Some(challenge.clone()),
30        _ => None,
31    }
32}
33
34/// Message cardinality on each side of a contract method.
35#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
36pub enum StreamingShape {
37    /// One request and one response.
38    Unary,
39    /// A stream of requests and one response.
40    ClientStreaming,
41    /// One request and a stream of responses.
42    ServerStreaming,
43    /// Streams in both directions.
44    Bidirectional,
45}
46
47include!(concat!(env!("OUT_DIR"), "/heddle_api_methods.rs"));
48
49impl MethodDescriptor {
50    /// Whether the contract permits this method on a replayable 0-RTT path.
51    pub const fn allows_zero_rtt(&self) -> bool {
52        matches!(self.effect, RpcEffect::ReadOnly)
53            && matches!(self.retry_behavior, RetryBehavior::Safe)
54    }
55
56    /// Extracts the request's declared `client_operation_id`, when its input
57    /// message has that field. The generated field number keeps clients and
58    /// producers from maintaining handwritten per-route catalogs.
59    pub fn client_operation_id<'a>(
60        &self,
61        request: &'a [u8],
62    ) -> Result<Option<&'a str>, RequestMetadataError> {
63        let Some(field_number) = self.client_operation_id_field_number else {
64            return Ok(None);
65        };
66        protobuf_string_field(request, field_number)
67    }
68}
69
70/// Malformed protobuf while extracting transport-level request metadata.
71#[derive(Debug, thiserror::Error)]
72#[error("invalid request metadata protobuf: {0}")]
73pub struct RequestMetadataError(&'static str);
74
75fn protobuf_string_field(
76    mut request: &[u8],
77    target_field: u32,
78) -> Result<Option<&str>, RequestMetadataError> {
79    while !request.is_empty() {
80        let key = take_varint(&mut request)?;
81        let field = u32::try_from(key >> 3).map_err(|_| RequestMetadataError("field overflow"))?;
82        let wire = (key & 0x07) as u8;
83        if field == 0 {
84            return Err(RequestMetadataError("field zero"));
85        }
86        match wire {
87            0 => {
88                let _ = take_varint(&mut request)?;
89            }
90            1 => {
91                let _ = take_bytes(&mut request, 8)?;
92            }
93            2 => {
94                let length = usize::try_from(take_varint(&mut request)?)
95                    .map_err(|_| RequestMetadataError("length overflow"))?;
96                let value = take_bytes(&mut request, length)?;
97                if field == target_field {
98                    return std::str::from_utf8(value)
99                        .map(Some)
100                        .map_err(|_| RequestMetadataError("operation id is not UTF-8"));
101                }
102            }
103            5 => {
104                let _ = take_bytes(&mut request, 4)?;
105            }
106            _ => return Err(RequestMetadataError("unsupported wire type")),
107        }
108    }
109    Ok(None)
110}
111
112fn take_varint(input: &mut &[u8]) -> Result<u64, RequestMetadataError> {
113    let mut value = 0_u64;
114    for shift in (0..70).step_by(7) {
115        let (&byte, rest) = input
116            .split_first()
117            .ok_or(RequestMetadataError("truncated varint"))?;
118        *input = rest;
119        if shift == 63 && byte > 1 {
120            return Err(RequestMetadataError("varint overflow"));
121        }
122        value |= u64::from(byte & 0x7f) << shift;
123        if byte & 0x80 == 0 {
124            return Ok(value);
125        }
126    }
127    Err(RequestMetadataError("varint overflow"))
128}
129
130fn take_bytes<'a>(input: &mut &'a [u8], length: usize) -> Result<&'a [u8], RequestMetadataError> {
131    if input.len() < length {
132        return Err(RequestMetadataError("truncated field"));
133    }
134    let (value, rest) = input.split_at(length);
135    *input = rest;
136    Ok(value)
137}
138
139/// Transport-neutral information passed from an operation-stream decoder to a
140/// contract router before the request body is decoded.
141#[derive(Debug)]
142pub struct RoutedCall<'a> {
143    /// Generated contract descriptor selected by the fully-qualified path.
144    pub method: &'static MethodDescriptor,
145    /// Typed authentication, deadline, idempotency, and trace fields.
146    pub context: &'a CallContext,
147}
148
149impl<'a> RoutedCall<'a> {
150    /// Selects a generated route or returns `None` for an unknown method path.
151    pub fn new(path: &str, context: &'a CallContext) -> Option<Self> {
152        method_descriptor(path).map(|method| Self { method, context })
153    }
154}