1use 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#[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#[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#[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#[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}