1use std::collections::BTreeMap;
4
5use crate::{
6 serialization_compatibility, ComponentNode, FieldId, FormEntity, FormFieldEntity, FormId,
7 FormSubmissionPlan, SerializableValue, SerializationCompatibility, SerializationPlanId,
8 SourceProvenance, SubmissionPlanId,
9};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum FormSerializationFormat {
13 Json,
14 FormData,
15 UrlEncoded,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum FormFieldSerializationConversion {
20 JsonValue,
21 FormDataScalar,
22 FormDataFiles,
23 UrlEncodedScalar,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct SerializedFieldPlan {
28 pub field: FieldId,
29 pub key: String,
30 pub declaration_order: usize,
31 pub conversion: FormFieldSerializationConversion,
32 pub initial_value: SerializableValue,
33 pub provenance: SourceProvenance,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct SerializationDeclarationFact {
38 pub form: Option<FormId>,
39 pub invoked: bool,
40 pub argument_count: usize,
41 pub format: Option<String>,
42 pub provenance: SourceProvenance,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum SerializationPlanStatus {
47 Valid,
48 InvalidDecorator,
49 NonSerializableField,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct FormSerializationPlan {
54 pub id: SerializationPlanId,
55 pub form: FormId,
56 pub format: FormSerializationFormat,
57 pub fields: Vec<SerializedFieldPlan>,
58 pub linked_submission: Option<SubmissionPlanId>,
59 pub status: SerializationPlanStatus,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Default)]
63pub struct SerializationProducts {
64 pub declarations: Vec<SerializationDeclarationFact>,
65 pub plans: BTreeMap<SerializationPlanId, FormSerializationPlan>,
66}
67
68#[must_use]
69pub fn collect_serialization_products(
70 components: &[ComponentNode],
71 forms: &BTreeMap<FormId, FormEntity>,
72 fields: &BTreeMap<FieldId, FormFieldEntity>,
73 submissions: &BTreeMap<SubmissionPlanId, FormSubmissionPlan>,
74) -> SerializationProducts {
75 let declarations = components
76 .iter()
77 .flat_map(|component| component.serialization_declaration_facts.iter())
78 .map(|fact| SerializationDeclarationFact {
79 form: fact.declaration_field.as_ref().and_then(|authored| {
80 forms
81 .values()
82 .find(|form| &form.authored_field == authored)
83 .map(|form| form.id.clone())
84 }),
85 invoked: fact.invoked,
86 argument_count: fact.argument_count,
87 format: fact.format.clone(),
88 provenance: fact.decorator_provenance.clone(),
89 })
90 .collect::<Vec<_>>();
91 let mut plans = BTreeMap::new();
92 for form in forms.values() {
93 let matching = declarations
94 .iter()
95 .filter(|declaration| declaration.form.as_ref() == Some(&form.id))
96 .collect::<Vec<_>>();
97 let decorator_valid = matching.len() <= 1
98 && matching.iter().all(|declaration| {
99 declaration.invoked
100 && declaration.argument_count == 1
101 && matches!(
102 declaration.format.as_deref(),
103 Some("json" | "form-data" | "url-encoded")
104 )
105 });
106 let format = matching
107 .first()
108 .and_then(|declaration| declaration.format.as_deref())
109 .map_or(FormSerializationFormat::Json, |format| match format {
110 "form-data" => FormSerializationFormat::FormData,
111 "url-encoded" => FormSerializationFormat::UrlEncoded,
112 _ => FormSerializationFormat::Json,
113 });
114 let mut ordered = fields
115 .values()
116 .filter(|field| field.owner_form == form.id)
117 .collect::<Vec<_>>();
118 ordered.sort_by(|left, right| {
119 (left.declaration_order, &left.id).cmp(&(right.declaration_order, &right.id))
120 });
121 let serializable = ordered.iter().all(|field| {
122 serialization_compatibility(&field.semantic_type)
123 == SerializationCompatibility::Serializable
124 || (format == FormSerializationFormat::FormData
125 && is_file_array(&field.semantic_type))
126 });
127 let status = if !decorator_valid {
128 SerializationPlanStatus::InvalidDecorator
129 } else if !serializable {
130 SerializationPlanStatus::NonSerializableField
131 } else {
132 SerializationPlanStatus::Valid
133 };
134 plans.insert(
135 SerializationPlanId::for_form(&form.id),
136 FormSerializationPlan {
137 id: SerializationPlanId::for_form(&form.id),
138 form: form.id.clone(),
139 format,
140 fields: ordered
141 .into_iter()
142 .map(|field| {
143 let conversion = match format {
144 FormSerializationFormat::Json => {
145 FormFieldSerializationConversion::JsonValue
146 }
147 FormSerializationFormat::FormData
148 if is_file_array(&field.semantic_type) =>
149 {
150 FormFieldSerializationConversion::FormDataFiles
151 }
152 FormSerializationFormat::FormData => {
153 FormFieldSerializationConversion::FormDataScalar
154 }
155 FormSerializationFormat::UrlEncoded => {
156 FormFieldSerializationConversion::UrlEncodedScalar
157 }
158 };
159 SerializedFieldPlan {
160 field: field.id.clone(),
161 key: field.path.join("."),
162 declaration_order: field.declaration_order,
163 conversion,
164 initial_value: field.initial_value.clone(),
165 provenance: field.provenance.clone(),
166 }
167 })
168 .collect(),
169 linked_submission: submissions
170 .contains_key(&SubmissionPlanId::for_form(&form.id))
171 .then(|| SubmissionPlanId::for_form(&form.id)),
172 status,
173 },
174 );
175 }
176 SerializationProducts {
177 declarations,
178 plans,
179 }
180}
181
182fn is_file_array(semantic_type: &crate::SemanticType) -> bool {
183 matches!(
184 semantic_type,
185 crate::SemanticType::Array(element)
186 if element.as_ref() == &crate::SemanticType::File
187 )
188}
189
190#[cfg(test)]
191mod tests {
192 use crate::{
193 build_application_semantic_model, collect_serialization_products, FormId,
194 FormSerializationFormat, SerializationPlanStatus,
195 };
196
197 #[test]
198 fn plans_implicit_json_and_explicit_scalar_formats_in_i3_order() {
199 let parsed = presolve_parser::parse_file(
200 "src/Forms.tsx",
201 r#"
202@component("forms") class Forms {
203 @form() implicit!: Form;
204 @form() @serialize("url-encoded") encoded!: Form;
205 @field(this.implicit) object = { city: "Austin" };
206 @field(this.implicit) value = false;
207 @field(this.encoded) name = "Ada";
208 render() { return <input field={this.name} />; }
209}
210"#,
211 );
212 let model = build_application_semantic_model(&parsed);
213 let implicit = FormId::for_owner(&model.components[0].id, "implicit");
214 let encoded = FormId::for_owner(&model.components[0].id, "encoded");
215 let implicit_plan = model
216 .serialization
217 .plans
218 .get(&crate::SerializationPlanId::for_form(&implicit))
219 .expect("implicit plan");
220 let encoded_plan = model
221 .serialization
222 .plans
223 .get(&crate::SerializationPlanId::for_form(&encoded))
224 .expect("encoded plan");
225 assert_eq!(implicit_plan.format, FormSerializationFormat::Json);
226 assert_eq!(
227 implicit_plan
228 .fields
229 .iter()
230 .map(|field| field.key.as_str())
231 .collect::<Vec<_>>(),
232 vec!["object", "value"]
233 );
234 assert_eq!(encoded_plan.format, FormSerializationFormat::UrlEncoded);
235 assert_eq!(encoded_plan.status, SerializationPlanStatus::Valid);
236 }
237
238 #[test]
239 fn retains_invalid_format_as_non_executable_plan_status() {
240 let parsed = presolve_parser::parse_file(
241 "src/Bad.tsx",
242 r#"
243@component("bad") class Bad {
244 @form() @serialize("xml") value!: Form;
245 @field(this.value) name = "";
246 render() { return <input field={this.name} />; }
247}
248"#,
249 );
250 let model = build_application_semantic_model(&parsed);
251 let form = FormId::for_owner(&model.components[0].id, "value");
252 assert_eq!(
253 model.serialization.plans[&crate::SerializationPlanId::for_form(&form)].status,
254 SerializationPlanStatus::InvalidDecorator
255 );
256 }
257
258 #[test]
259 fn admits_authority_proven_files_only_for_form_data() {
260 let parsed = presolve_parser::parse_file(
261 "src/Files.tsx",
262 r#"
263@component("files") class Files {
264 @form() @serialize("form-data") upload!: Form;
265 @field(this.upload) uploadFiles = ["placeholder"];
266 @form() @serialize("json") invalid!: Form;
267 @field(this.invalid) invalidFiles = ["placeholder"];
268}
269"#,
270 );
271 let mut model = build_application_semantic_model(&parsed);
272 for field in model.form_fields.values_mut() {
273 field.semantic_type = crate::SemanticType::Array(Box::new(crate::SemanticType::File));
274 }
275 let products = collect_serialization_products(
276 &model.components,
277 &model.forms,
278 &model.form_fields,
279 &model.submissions.plans,
280 );
281 let upload = FormId::for_owner(&model.components[0].id, "upload");
282 let invalid = FormId::for_owner(&model.components[0].id, "invalid");
283 let upload = &products.plans[&crate::SerializationPlanId::for_form(&upload)];
284 let invalid = &products.plans[&crate::SerializationPlanId::for_form(&invalid)];
285 assert_eq!(upload.status, SerializationPlanStatus::Valid);
286 assert_eq!(
287 upload.fields[0].conversion,
288 super::FormFieldSerializationConversion::FormDataFiles
289 );
290 assert_eq!(
291 invalid.status,
292 SerializationPlanStatus::NonSerializableField
293 );
294 }
295}