Skip to main content

presolve_compiler/
form_serialization.rs

1//! I10 deterministic declaration-level Form serialization planning.
2
3use 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}