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(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}