Skip to main content

presolve_compiler/
codec_protocol.rs

1//! Versioned codec declarations over canonical semantic types.
2//!
3//! This product is a protocol ledger. It does not encode values, decode durable
4//! bytes, or replace the existing Form and resume codec authorities.
5
6use std::collections::BTreeSet;
7
8use serde::Serialize;
9
10use crate::{resume_value_codec, semantic_type_text, SemanticType};
11
12pub const CODEC_PROTOCOL_SCHEMA_VERSION: u32 = 1;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
15#[serde(rename_all = "snake_case")]
16pub enum CodecClassificationV1 {
17    Approved,
18    Rejected,
19}
20
21/// Independent classifications; these must never be reduced to one boolean.
22#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
23pub struct CodecClassificationsV1 {
24    pub runtime_validated: CodecClassificationV1,
25    pub form_serializable: CodecClassificationV1,
26    pub network_serializable: CodecClassificationV1,
27    pub html_publishable: CodecClassificationV1,
28    pub resume_serializable: CodecClassificationV1,
29    pub structured_cloneable: CodecClassificationV1,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
33#[serde(rename_all = "snake_case")]
34pub enum CodecRepresentationV1 {
35    Json,
36    FormDataScalar,
37    UrlEncodedScalar,
38    PlatformValue,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
42#[serde(rename_all = "snake_case")]
43pub enum CodecEnvironmentV1 {
44    Server,
45    Browser,
46    Shared,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
50#[serde(rename_all = "snake_case")]
51pub enum CodecBehaviorV1 {
52    Total,
53    Fallible,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
57#[serde(rename_all = "snake_case")]
58pub enum CodecFailureBehaviorV1 {
59    RejectValue,
60    RejectPayload,
61    SurfaceDiagnostic,
62}
63
64/// One explicit codec contract. The source type remains compiler-owned.
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct CodecDeclarationV1 {
67    pub id: String,
68    pub source_type: SemanticType,
69    pub representation: CodecRepresentationV1,
70    pub environments: Vec<CodecEnvironmentV1>,
71    pub encode_behavior: CodecBehaviorV1,
72    pub decode_behavior: CodecBehaviorV1,
73    pub version: u32,
74    pub failure_behavior: CodecFailureBehaviorV1,
75}
76
77/// Serializable inspection record that retains all codec declaration fields.
78#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
79pub struct CodecProtocolRecordV1 {
80    pub id: String,
81    pub source_type: String,
82    pub representation: CodecRepresentationV1,
83    pub environments: Vec<CodecEnvironmentV1>,
84    pub encode_behavior: CodecBehaviorV1,
85    pub decode_behavior: CodecBehaviorV1,
86    pub version: u32,
87    pub failure_behavior: CodecFailureBehaviorV1,
88    pub classifications: CodecClassificationsV1,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
92pub struct CodecProtocolDiagnosticV1 {
93    pub codec: String,
94    pub reason: CodecProtocolDiagnosticReasonV1,
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
98#[serde(rename_all = "snake_case")]
99pub enum CodecProtocolDiagnosticReasonV1 {
100    DuplicateCodecId,
101    MissingEnvironment,
102    MissingVersion,
103    UnsupportedSourceType,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
107pub struct CodecProtocolV1 {
108    pub schema_version: u32,
109    pub codecs: Vec<CodecProtocolRecordV1>,
110    pub diagnostics: Vec<CodecProtocolDiagnosticV1>,
111}
112
113/// Produces codec declaration records and diagnoses unsupported types early.
114#[must_use]
115pub fn build_codec_protocol_v1(declarations: &[CodecDeclarationV1]) -> CodecProtocolV1 {
116    let duplicate_ids = declarations
117        .iter()
118        .fold(
119            (BTreeSet::new(), BTreeSet::new()),
120            |(mut seen, mut duplicates), declaration| {
121                if !seen.insert(declaration.id.as_str()) {
122                    duplicates.insert(declaration.id.as_str());
123                }
124                (seen, duplicates)
125            },
126        )
127        .1;
128    let mut codecs = Vec::new();
129    let mut diagnostics = Vec::new();
130    for declaration in declarations {
131        let classifications = classify(&declaration.source_type);
132        if duplicate_ids.contains(declaration.id.as_str()) {
133            diagnostics.push(diagnostic(
134                declaration,
135                CodecProtocolDiagnosticReasonV1::DuplicateCodecId,
136            ));
137        }
138        if declaration.environments.is_empty() {
139            diagnostics.push(diagnostic(
140                declaration,
141                CodecProtocolDiagnosticReasonV1::MissingEnvironment,
142            ));
143        }
144        if declaration.version == 0 {
145            diagnostics.push(diagnostic(
146                declaration,
147                CodecProtocolDiagnosticReasonV1::MissingVersion,
148            ));
149        }
150        if classifications.network_serializable == CodecClassificationV1::Rejected {
151            diagnostics.push(diagnostic(
152                declaration,
153                CodecProtocolDiagnosticReasonV1::UnsupportedSourceType,
154            ));
155        }
156        let mut environments = declaration.environments.clone();
157        environments.sort();
158        environments.dedup();
159        codecs.push(CodecProtocolRecordV1 {
160            id: declaration.id.clone(),
161            source_type: semantic_type_text(&declaration.source_type),
162            representation: declaration.representation,
163            environments,
164            encode_behavior: declaration.encode_behavior,
165            decode_behavior: declaration.decode_behavior,
166            version: declaration.version,
167            failure_behavior: declaration.failure_behavior,
168            classifications,
169        });
170    }
171    codecs.sort_by(|left, right| left.id.cmp(&right.id));
172    diagnostics.sort_by(|left, right| {
173        (left.codec.as_str(), left.reason).cmp(&(right.codec.as_str(), right.reason))
174    });
175    CodecProtocolV1 {
176        schema_version: CODEC_PROTOCOL_SCHEMA_VERSION,
177        codecs,
178        diagnostics,
179    }
180}
181
182fn diagnostic(
183    declaration: &CodecDeclarationV1,
184    reason: CodecProtocolDiagnosticReasonV1,
185) -> CodecProtocolDiagnosticV1 {
186    CodecProtocolDiagnosticV1 {
187        codec: declaration.id.clone(),
188        reason,
189    }
190}
191
192fn classify(semantic_type: &SemanticType) -> CodecClassificationsV1 {
193    let structural = structural_serializable(semantic_type);
194    CodecClassificationsV1 {
195        runtime_validated: approved(!matches!(
196            semantic_type,
197            SemanticType::Unknown | SemanticType::Never
198        )),
199        form_serializable: approved(form_serializable(semantic_type)),
200        network_serializable: approved(structural),
201        html_publishable: approved(html_publishable(semantic_type)),
202        resume_serializable: approved(resume_value_codec(semantic_type).is_ok()),
203        structured_cloneable: approved(structured_cloneable(semantic_type)),
204    }
205}
206
207const fn approved(value: bool) -> CodecClassificationV1 {
208    if value {
209        CodecClassificationV1::Approved
210    } else {
211        CodecClassificationV1::Rejected
212    }
213}
214
215fn structural_serializable(semantic_type: &SemanticType) -> bool {
216    match semantic_type {
217        SemanticType::Unknown
218        | SemanticType::Never
219        | SemanticType::Form
220        | SemanticType::SlotContent => false,
221        SemanticType::File => false,
222        SemanticType::Null
223        | SemanticType::Boolean
224        | SemanticType::Number
225        | SemanticType::String
226        | SemanticType::BooleanLiteral(_)
227        | SemanticType::NumberLiteral(_)
228        | SemanticType::StringLiteral(_) => true,
229        SemanticType::Array(item) => structural_serializable(item),
230        SemanticType::Tuple(items) | SemanticType::Union(items) => {
231            items.iter().all(structural_serializable)
232        }
233        SemanticType::Object(object) => object.properties.values().all(structural_serializable),
234        SemanticType::Resource(resource) => {
235            resource.serializable
236                && structural_serializable(&resource.data)
237                && structural_serializable(&resource.error)
238        }
239    }
240}
241
242fn form_serializable(semantic_type: &SemanticType) -> bool {
243    match semantic_type {
244        SemanticType::File => true,
245        SemanticType::Array(item) => form_serializable(item),
246        SemanticType::Tuple(items) | SemanticType::Union(items) => {
247            items.iter().all(form_serializable)
248        }
249        SemanticType::Object(object) => object.properties.values().all(form_serializable),
250        _ => structural_serializable(semantic_type),
251    }
252}
253
254fn structured_cloneable(semantic_type: &SemanticType) -> bool {
255    match semantic_type {
256        SemanticType::File => true,
257        SemanticType::Array(item) => structured_cloneable(item),
258        SemanticType::Tuple(items) | SemanticType::Union(items) => {
259            items.iter().all(structured_cloneable)
260        }
261        SemanticType::Object(object) => object.properties.values().all(structured_cloneable),
262        SemanticType::Resource(_) => false,
263        _ => structural_serializable(semantic_type),
264    }
265}
266
267fn html_publishable(semantic_type: &SemanticType) -> bool {
268    matches!(
269        semantic_type,
270        SemanticType::Null
271            | SemanticType::Boolean
272            | SemanticType::Number
273            | SemanticType::String
274            | SemanticType::BooleanLiteral(_)
275            | SemanticType::NumberLiteral(_)
276            | SemanticType::StringLiteral(_)
277    )
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    fn declaration(id: &str, source_type: SemanticType) -> CodecDeclarationV1 {
285        CodecDeclarationV1 {
286            id: id.into(),
287            source_type,
288            representation: CodecRepresentationV1::Json,
289            environments: vec![CodecEnvironmentV1::Browser, CodecEnvironmentV1::Server],
290            encode_behavior: CodecBehaviorV1::Fallible,
291            decode_behavior: CodecBehaviorV1::Fallible,
292            version: 1,
293            failure_behavior: CodecFailureBehaviorV1::RejectPayload,
294        }
295    }
296
297    #[test]
298    fn keeps_serialization_classes_independent() {
299        let protocol = build_codec_protocol_v1(&[declaration(
300            "tuple",
301            SemanticType::Tuple(vec![SemanticType::String, SemanticType::Number]),
302        )]);
303        let classifications = &protocol.codecs[0].classifications;
304        assert_eq!(
305            classifications.network_serializable,
306            CodecClassificationV1::Approved
307        );
308        assert_eq!(
309            classifications.resume_serializable,
310            CodecClassificationV1::Rejected
311        );
312        assert_eq!(
313            classifications.html_publishable,
314            CodecClassificationV1::Rejected
315        );
316    }
317
318    #[test]
319    fn classifies_platform_files_without_collapsing_resume_and_form_data() {
320        let protocol = build_codec_protocol_v1(&[declaration(
321            "files",
322            SemanticType::Array(Box::new(SemanticType::File)),
323        )]);
324        let classifications = &protocol.codecs[0].classifications;
325        assert_eq!(
326            classifications.runtime_validated,
327            CodecClassificationV1::Approved
328        );
329        assert_eq!(
330            classifications.form_serializable,
331            CodecClassificationV1::Approved
332        );
333        assert_eq!(
334            classifications.network_serializable,
335            CodecClassificationV1::Rejected
336        );
337        assert_eq!(
338            classifications.resume_serializable,
339            CodecClassificationV1::Rejected
340        );
341        assert_eq!(
342            classifications.structured_cloneable,
343            CodecClassificationV1::Approved
344        );
345    }
346
347    #[test]
348    fn retains_codec_contract_and_diagnoses_nonserializable_types_early() {
349        let mut invalid = declaration("invalid", SemanticType::Form);
350        invalid.environments.clear();
351        invalid.version = 0;
352        let protocol = build_codec_protocol_v1(&[invalid]);
353        assert_eq!(protocol.schema_version, CODEC_PROTOCOL_SCHEMA_VERSION);
354        assert_eq!(
355            protocol.codecs[0].encode_behavior,
356            CodecBehaviorV1::Fallible
357        );
358        assert_eq!(protocol.diagnostics.len(), 3);
359        assert!(protocol.diagnostics.iter().any(|diagnostic| {
360            diagnostic.reason == CodecProtocolDiagnosticReasonV1::UnsupportedSourceType
361        }));
362    }
363}