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(structural),
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(
204            structural && !matches!(semantic_type, SemanticType::Resource(_)),
205        ),
206    }
207}
208
209const fn approved(value: bool) -> CodecClassificationV1 {
210    if value {
211        CodecClassificationV1::Approved
212    } else {
213        CodecClassificationV1::Rejected
214    }
215}
216
217fn structural_serializable(semantic_type: &SemanticType) -> bool {
218    match semantic_type {
219        SemanticType::Unknown
220        | SemanticType::Never
221        | SemanticType::Form
222        | SemanticType::SlotContent => false,
223        SemanticType::Null
224        | SemanticType::Boolean
225        | SemanticType::Number
226        | SemanticType::String
227        | SemanticType::BooleanLiteral(_)
228        | SemanticType::NumberLiteral(_)
229        | SemanticType::StringLiteral(_) => true,
230        SemanticType::Array(item) => structural_serializable(item),
231        SemanticType::Tuple(items) | SemanticType::Union(items) => {
232            items.iter().all(structural_serializable)
233        }
234        SemanticType::Object(object) => object.properties.values().all(structural_serializable),
235        SemanticType::Resource(resource) => {
236            resource.serializable
237                && structural_serializable(&resource.data)
238                && structural_serializable(&resource.error)
239        }
240    }
241}
242
243fn html_publishable(semantic_type: &SemanticType) -> bool {
244    matches!(
245        semantic_type,
246        SemanticType::Null
247            | SemanticType::Boolean
248            | SemanticType::Number
249            | SemanticType::String
250            | SemanticType::BooleanLiteral(_)
251            | SemanticType::NumberLiteral(_)
252            | SemanticType::StringLiteral(_)
253    )
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    fn declaration(id: &str, source_type: SemanticType) -> CodecDeclarationV1 {
261        CodecDeclarationV1 {
262            id: id.into(),
263            source_type,
264            representation: CodecRepresentationV1::Json,
265            environments: vec![CodecEnvironmentV1::Browser, CodecEnvironmentV1::Server],
266            encode_behavior: CodecBehaviorV1::Fallible,
267            decode_behavior: CodecBehaviorV1::Fallible,
268            version: 1,
269            failure_behavior: CodecFailureBehaviorV1::RejectPayload,
270        }
271    }
272
273    #[test]
274    fn keeps_serialization_classes_independent() {
275        let protocol = build_codec_protocol_v1(&[declaration(
276            "tuple",
277            SemanticType::Tuple(vec![SemanticType::String, SemanticType::Number]),
278        )]);
279        let classifications = &protocol.codecs[0].classifications;
280        assert_eq!(
281            classifications.network_serializable,
282            CodecClassificationV1::Approved
283        );
284        assert_eq!(
285            classifications.resume_serializable,
286            CodecClassificationV1::Rejected
287        );
288        assert_eq!(
289            classifications.html_publishable,
290            CodecClassificationV1::Rejected
291        );
292    }
293
294    #[test]
295    fn retains_codec_contract_and_diagnoses_nonserializable_types_early() {
296        let mut invalid = declaration("invalid", SemanticType::Form);
297        invalid.environments.clear();
298        invalid.version = 0;
299        let protocol = build_codec_protocol_v1(&[invalid]);
300        assert_eq!(protocol.schema_version, CODEC_PROTOCOL_SCHEMA_VERSION);
301        assert_eq!(
302            protocol.codecs[0].encode_behavior,
303            CodecBehaviorV1::Fallible
304        );
305        assert_eq!(protocol.diagnostics.len(), 3);
306        assert!(protocol.diagnostics.iter().any(|diagnostic| {
307            diagnostic.reason == CodecProtocolDiagnosticReasonV1::UnsupportedSourceType
308        }));
309    }
310}