Skip to main content

type_bridge/
canonical_codec.rs

1//! Cancellation, deadline, and tighten-only controls for canonical records and archives.
2
3use std::time::{Duration, Instant};
4
5use type_bridge_contract::diagnostic::Diagnostic;
6use type_bridge_contract::limits::{
7    CodecLimits, MAX_CANONICAL_COLLECTION_LEN, MAX_CANONICAL_DEPTH, MAX_CANONICAL_STRING_BYTES,
8};
9use type_bridge_contract::projected_record::{
10    MAX_PROJECTED_ARCHIVE_BYTES, MAX_PROJECTED_ARCHIVE_RECORDS, MAX_PROJECTED_DECODED_WEIGHT,
11    MAX_PROJECTED_RECORD_BYTES,
12};
13use type_bridge_contract::sdk_diagnostic::SdkExecutionDiagnostic;
14
15use crate::{AnswerCancellation, Error, ModelValidationPhase};
16
17/// Tighten-only structural and byte ceilings for canonical codec work.
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub struct CanonicalCodecLimits {
20    max_input_bytes: usize,
21    max_output_bytes: usize,
22    max_depth: usize,
23    max_records: usize,
24    max_members: usize,
25}
26
27impl Default for CanonicalCodecLimits {
28    fn default() -> Self {
29        Self {
30            max_input_bytes: MAX_PROJECTED_ARCHIVE_BYTES,
31            max_output_bytes: MAX_PROJECTED_ARCHIVE_BYTES,
32            max_depth: MAX_CANONICAL_DEPTH,
33            max_records: MAX_PROJECTED_ARCHIVE_RECORDS,
34            max_members: MAX_PROJECTED_DECODED_WEIGHT.min(MAX_CANONICAL_COLLECTION_LEN),
35        }
36    }
37}
38
39impl CanonicalCodecLimits {
40    /// Return the default canonical codec ceilings.
41    #[must_use]
42    pub fn new() -> Self {
43        Self::default()
44    }
45
46    /// Tighten the canonical input byte ceiling.
47    #[must_use]
48    pub fn with_max_input_bytes(mut self, value: usize) -> Self {
49        self.max_input_bytes = self.max_input_bytes.min(value);
50        self
51    }
52
53    /// Tighten the canonical output byte ceiling.
54    #[must_use]
55    pub fn with_max_output_bytes(mut self, value: usize) -> Self {
56        self.max_output_bytes = self.max_output_bytes.min(value);
57        self
58    }
59
60    /// Tighten the canonical JSON nesting-depth ceiling.
61    #[must_use]
62    pub fn with_max_depth(mut self, value: usize) -> Self {
63        self.max_depth = self.max_depth.min(value);
64        self
65    }
66
67    /// Tighten the ordered archive record ceiling.
68    #[must_use]
69    pub fn with_max_records(mut self, value: usize) -> Self {
70        self.max_records = self.max_records.min(value);
71        self
72    }
73
74    /// Tighten the decoded member/value/reference weight ceiling.
75    #[must_use]
76    pub fn with_max_members(mut self, value: usize) -> Self {
77        self.max_members = self.max_members.min(value);
78        self
79    }
80}
81
82/// One owned cancellation/deadline/limit policy captured for a codec invocation.
83#[derive(Clone, Debug, Default)]
84pub struct CanonicalCodecOptions {
85    limits: CanonicalCodecLimits,
86    cancellation: AnswerCancellation,
87    timeout: Option<Duration>,
88}
89
90impl CanonicalCodecOptions {
91    /// Return unconstrained-by-caller codec options under the contract ceilings.
92    #[must_use]
93    pub fn new() -> Self {
94        Self::default()
95    }
96
97    /// Use exact tighten-only codec limits.
98    #[must_use]
99    pub fn with_limits(mut self, limits: CanonicalCodecLimits) -> Self {
100        self.limits = limits;
101        self
102    }
103
104    /// Use one independently owned sticky cancellation signal.
105    #[must_use]
106    pub fn with_cancellation(mut self, cancellation: AnswerCancellation) -> Self {
107        self.cancellation = cancellation;
108        self
109    }
110
111    /// Apply a relative timeout captured once as a monotonic deadline at invocation.
112    #[must_use]
113    pub fn with_timeout(mut self, timeout: Duration) -> Self {
114        self.timeout = Some(timeout);
115        self
116    }
117}
118
119pub(crate) struct CapturedCanonicalCodecControl {
120    limits: CanonicalCodecLimits,
121    cancellation: AnswerCancellation,
122    deadline: Option<Instant>,
123    archive: bool,
124}
125
126impl CapturedCanonicalCodecControl {
127    pub(crate) fn capture(options: &CanonicalCodecOptions, archive: bool) -> crate::Result<Self> {
128        let deadline = options
129            .timeout
130            .map(|timeout| {
131                Instant::now().checked_add(timeout).ok_or_else(|| {
132                    codec_error(SdkExecutionDiagnostic::projected_codec_deadline_exceeded())
133                })
134            })
135            .transpose()?;
136        Ok(Self {
137            limits: options.limits,
138            cancellation: options.cancellation.clone(),
139            deadline,
140            archive,
141        })
142    }
143
144    pub(crate) fn check(&self) -> crate::Result<()> {
145        if self.cancellation.is_cancelled() {
146            Err(codec_error(
147                SdkExecutionDiagnostic::projected_codec_cancelled(),
148            ))
149        } else if self
150            .deadline
151            .is_some_and(|deadline| Instant::now() >= deadline)
152        {
153            Err(codec_error(
154                SdkExecutionDiagnostic::projected_codec_deadline_exceeded(),
155            ))
156        } else {
157            Ok(())
158        }
159    }
160
161    pub(crate) fn input_limits(&self) -> CodecLimits {
162        let ceiling = if self.archive {
163            MAX_PROJECTED_ARCHIVE_BYTES
164        } else {
165            MAX_PROJECTED_RECORD_BYTES
166        };
167        let max_bytes = self.limits.max_input_bytes.min(ceiling);
168        CodecLimits {
169            max_bytes,
170            max_depth: self.limits.max_depth,
171            max_collection_len: self.limits.max_members,
172            max_string_bytes: MAX_CANONICAL_STRING_BYTES.min(max_bytes),
173        }
174    }
175
176    pub(crate) fn output_limits(&self) -> CodecLimits {
177        let ceiling = if self.archive {
178            MAX_PROJECTED_ARCHIVE_BYTES
179        } else {
180            MAX_PROJECTED_RECORD_BYTES
181        };
182        let max_bytes = self.limits.max_output_bytes.min(ceiling);
183        CodecLimits {
184            max_bytes,
185            max_depth: self.limits.max_depth,
186            max_collection_len: self.limits.max_members,
187            max_string_bytes: MAX_CANONICAL_STRING_BYTES.min(max_bytes),
188        }
189    }
190
191    pub(crate) fn check_record_count(&self, count: usize) -> crate::Result<()> {
192        if count > self.limits.max_records {
193            Err(codec_error(
194                SdkExecutionDiagnostic::projected_codec_member_limit(),
195            ))
196        } else {
197            Ok(())
198        }
199    }
200
201    pub(crate) fn check_decoded_weight(&self, weight: usize) -> crate::Result<()> {
202        if weight > self.limits.max_members {
203            Err(codec_error(
204                SdkExecutionDiagnostic::projected_codec_member_limit(),
205            ))
206        } else {
207            Ok(())
208        }
209    }
210
211    pub(crate) fn check_output_bytes(&self, bytes: usize) -> crate::Result<()> {
212        if bytes > self.limits.max_output_bytes {
213            Err(codec_error(
214                SdkExecutionDiagnostic::projected_codec_output_limit(),
215            ))
216        } else {
217            Ok(())
218        }
219    }
220}
221
222pub(crate) fn input_error(error: Diagnostic) -> Error {
223    limit_error(error, true)
224}
225
226pub(crate) fn output_error(error: Diagnostic) -> Error {
227    limit_error(error, false)
228}
229
230fn limit_error(error: Diagnostic, input: bool) -> Error {
231    let diagnostic = match error.code().as_str() {
232        "canonical_json_too_deep" => SdkExecutionDiagnostic::projected_codec_depth_limit(),
233        "canonical_collection_too_large" => SdkExecutionDiagnostic::projected_codec_member_limit(),
234        "canonical_json_too_large" | "canonical_string_too_large" if input => {
235            SdkExecutionDiagnostic::projected_codec_input_limit()
236        }
237        "canonical_json_too_large" | "canonical_string_too_large" => {
238            SdkExecutionDiagnostic::projected_codec_output_limit()
239        }
240        _ => return Error::from_contract_diagnostic(error),
241    };
242    codec_error(diagnostic)
243}
244
245fn codec_error(error: SdkExecutionDiagnostic) -> Error {
246    Error::from_sdk_execution(error, ModelValidationPhase::Input)
247}