Skip to main content

heddle_api/
lib.rs

1//! Generated transport-neutral Rust types for the Heddle API.
2
3pub mod framing;
4pub mod signing;
5mod transport;
6pub mod treadle;
7
8pub use transport::{
9    ALL_METHODS, HOSTED_ALPN_V1, MethodDescriptor, MethodRoute, PROVIDER_ALPN_V1,
10    RequestMetadataError, RoutedCall, StreamingShape, human_verification_challenge,
11    human_verification_error_detail, method_descriptor,
12};
13
14include!(concat!(
15    env!("OUT_DIR"),
16    "/heddle_api_attachment_authorization.rs"
17));
18
19/// Cross-product hosted-call framing and typed-failure fixture.
20pub const HOSTED_CALL_V1_FIXTURE_JSON: &str = include_str!("../tests/fixtures/hosted-call-v1.json");
21/// Cross-product canonical unary-signing fixture.
22pub const UNARY_SIGNING_V1_FIXTURE_JSON: &str =
23    include_str!("../tests/fixtures/unary-signing-v1.json");
24/// Cross-product canonical GrantEnvelope v2 payload and rejection fixtures.
25pub const GRANT_ENVELOPE_V2_FIXTURE_JSON: &str =
26    include_str!("../tests/fixtures/grant-envelope-v2.json");
27/// Cross-product endpoint-descriptor and relay-admission signing fixture.
28pub const TRANSPORT_BOOTSTRAP_V1_FIXTURE_JSON: &str =
29    include_str!("../tests/fixtures/transport-bootstrap-v1.json");
30
31/// Page size used when callers omit or pass zero for a requested size.
32pub const DEFAULT_PAGE_SIZE: u32 = 50;
33/// Largest page the public API permits.
34pub const MAX_PAGE_SIZE: u32 = 200;
35
36/// Applies the contract-owned default and upper bound to a requested page.
37pub const fn normalize_page_size(requested: u32) -> u32 {
38    if requested == 0 {
39        DEFAULT_PAGE_SIZE
40    } else if requested > MAX_PAGE_SIZE {
41        MAX_PAGE_SIZE
42    } else {
43        requested
44    }
45}
46
47/// Heddle API protobuf packages.
48pub mod heddle {
49    /// Neutral public API contract.
50    pub mod api {
51        /// Breaking pre-1.0 API generation.
52        pub mod v1alpha1 {
53            include!(concat!(env!("OUT_DIR"), "/heddle.api.v1alpha1.rs"));
54        }
55    }
56}
57
58impl heddle::api::v1alpha1::ErrorReason {
59    /// Returns whether callers may retry without first correcting the request.
60    pub fn retryable(&self) -> bool {
61        matches!(
62            self,
63            Self::RateLimited | Self::QuotaExceeded | Self::Transient
64        )
65    }
66}
67
68/// Compiled protobuf descriptor set for reflection and contract inspection.
69#[cfg(feature = "reflection")]
70pub const FILE_DESCRIPTOR_SET: &[u8] =
71    include_bytes!(concat!(env!("OUT_DIR"), "/heddle_api_descriptor.bin"));
72
73/// Errors returned while constructing fixed-width API identifiers.
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct InvalidIdentifierLength {
76    kind: &'static str,
77    expected: usize,
78    actual: usize,
79}
80
81impl std::fmt::Display for InvalidIdentifierLength {
82    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83        write!(
84            formatter,
85            "{} requires exactly {} bytes, got {}",
86            self.kind, self.expected, self.actual
87        )
88    }
89}
90
91impl std::error::Error for InvalidIdentifierLength {}
92
93impl heddle::api::v1alpha1::StateId {
94    /// Constructs a physical state identifier from exactly 32 bytes.
95    pub fn from_bytes(value: impl AsRef<[u8]>) -> Result<Self, InvalidIdentifierLength> {
96        fixed_width("StateId", 32, value.as_ref()).map(|value| Self { value })
97    }
98}
99
100impl heddle::api::v1alpha1::ChangeId {
101    /// Constructs a rewrite-stable change identifier from exactly 16 bytes.
102    pub fn from_bytes(value: impl AsRef<[u8]>) -> Result<Self, InvalidIdentifierLength> {
103        fixed_width("ChangeId", 16, value.as_ref()).map(|value| Self { value })
104    }
105}
106
107impl heddle::api::v1alpha1::OperationId {
108    /// Constructs a durable operation identifier from exactly 16 bytes.
109    pub fn from_bytes(value: impl AsRef<[u8]>) -> Result<Self, InvalidIdentifierLength> {
110        fixed_width("OperationId", 16, value.as_ref()).map(|value| Self { value })
111    }
112}
113
114impl heddle::api::v1alpha1::OperationBatchId {
115    /// Constructs a durable operation-batch identifier from exactly 16 bytes.
116    pub fn from_bytes(value: impl AsRef<[u8]>) -> Result<Self, InvalidIdentifierLength> {
117        fixed_width("OperationBatchId", 16, value.as_ref()).map(|value| Self { value })
118    }
119}
120
121impl heddle::api::v1alpha1::GitObjectId {
122    /// Constructs and validates a Git object identifier for its hash algorithm.
123    pub fn from_digest(
124        algorithm: heddle::api::v1alpha1::GitObjectAlgorithm,
125        digest: impl AsRef<[u8]>,
126    ) -> Result<Self, InvalidIdentifierLength> {
127        let expected = match algorithm {
128            heddle::api::v1alpha1::GitObjectAlgorithm::Sha1 => 20,
129            heddle::api::v1alpha1::GitObjectAlgorithm::Sha256 => 32,
130            heddle::api::v1alpha1::GitObjectAlgorithm::Unspecified => 0,
131        };
132        fixed_width("GitObjectId", expected, digest.as_ref()).map(|digest| Self {
133            algorithm: algorithm as i32,
134            digest,
135        })
136    }
137}
138
139fn fixed_width(
140    kind: &'static str,
141    expected: usize,
142    value: &[u8],
143) -> Result<Vec<u8>, InvalidIdentifierLength> {
144    if value.len() != expected {
145        return Err(InvalidIdentifierLength {
146            kind,
147            expected,
148            actual: value.len(),
149        });
150    }
151    Ok(value.to_vec())
152}
153
154#[cfg(test)]
155mod tests {
156    use super::heddle::api::v1alpha1::{
157        ChangeId, ErrorReason, GitObjectAlgorithm, GitObjectId, OperationBatchId, OperationId,
158        StateId,
159    };
160
161    #[test]
162    fn fixed_width_identifiers_reject_ambiguous_bytes() {
163        assert!(StateId::from_bytes([0; 32]).is_ok());
164        assert!(StateId::from_bytes([0; 31]).is_err());
165        assert!(ChangeId::from_bytes([0; 16]).is_ok());
166        assert!(ChangeId::from_bytes([0; 17]).is_err());
167        assert!(OperationId::from_bytes([0; 16]).is_ok());
168        assert!(OperationId::from_bytes([0; 15]).is_err());
169        assert!(OperationBatchId::from_bytes([0; 16]).is_ok());
170        assert!(OperationBatchId::from_bytes([0; 17]).is_err());
171        assert!(GitObjectId::from_digest(GitObjectAlgorithm::Sha1, [0; 20]).is_ok());
172        assert!(GitObjectId::from_digest(GitObjectAlgorithm::Sha256, [0; 20]).is_err());
173    }
174
175    #[test]
176    fn error_reason_retryability_is_derived_from_the_taxonomy() {
177        assert!(ErrorReason::RateLimited.retryable());
178        assert!(ErrorReason::QuotaExceeded.retryable());
179        assert!(ErrorReason::Transient.retryable());
180        assert!(!ErrorReason::CursorInvalid.retryable());
181        assert!(!ErrorReason::Internal.retryable());
182    }
183}