Skip to main content

presolve_compiler/
form_inspection.rs

1//! I17 canonical Forms inspection projection.
2//!
3//! This module consumes staged Forms products only; it never reinterprets
4//! syntax or inspects live runtime state.
5
6use std::collections::BTreeMap;
7
8use serde::Serialize;
9
10use crate::{
11    semantic_type_text, ApplicationSemanticModel, FormControlChannel, SemanticId, SerializableValue,
12};
13
14#[derive(Debug, Clone, PartialEq, Eq, Default)]
15pub struct FormInspectionRegistry {
16    pub records: BTreeMap<SemanticId, FormInspection>,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
20pub struct FormInspection {
21    pub role: &'static str,
22    pub form: String,
23    pub owner_component: String,
24    pub field_order: Vec<String>,
25    pub field_semantic_type: Option<String>,
26    pub initial_value: Option<SerializableValue>,
27    pub bindings: Vec<String>,
28    pub binding_channels: Vec<String>,
29    pub validation_rules: Vec<String>,
30    pub source_rules: Vec<String>,
31    pub dependent_rules: Vec<String>,
32    pub dirty_tracking: Option<String>,
33    pub touched_tracking: Option<String>,
34    pub submission_plan: Option<String>,
35    pub serialization_plan: Option<String>,
36    pub reset_plan: Option<String>,
37    pub instances: Vec<FormInspectionInstance>,
38    pub runtime_registry_member: bool,
39    pub runtime_artifact_member: bool,
40    pub resume_planned: bool,
41    pub blocked_reasons: Vec<String>,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
45pub struct FormInspectionInstance {
46    pub form_instance: String,
47    pub component_instance: String,
48    pub value_slots: Vec<String>,
49    pub dirty_slots: Vec<String>,
50    pub touched_slots: Vec<String>,
51    pub validation_slots: Vec<String>,
52    pub aggregate_slot: String,
53    pub submission_slot: String,
54    pub input_program_fields: Vec<String>,
55    pub blur_program_fields: Vec<String>,
56}
57
58/// Build one shared I17 projection for full/selected ASM, graph, and explain.
59///
60/// # Panics
61///
62/// Panics if a retained canonical Form has no Component owner, or if a
63/// retained binding or validation rule has no corresponding Form projection.
64#[must_use]
65#[allow(clippy::too_many_lines)]
66pub fn build_form_inspection_registry(model: &ApplicationSemanticModel) -> FormInspectionRegistry {
67    let artifact = crate::build_runtime_forms_artifact(model);
68    let resume = crate::build_resume_plan(model);
69    let mut records = BTreeMap::new();
70    for form in model.forms.values() {
71        let fields = model
72            .form_fields
73            .values()
74            .filter(|field| field.owner_form == form.id)
75            .collect::<Vec<_>>();
76        let instances = model
77            .optimized_form_ir
78            .optimized
79            .instances
80            .values()
81            .filter(|instance| instance.form == form.id)
82            .map(|instance| FormInspectionInstance {
83                form_instance: instance.id.to_string(),
84                component_instance: instance.component_instance.to_string(),
85                value_slots: instance
86                    .storage
87                    .value
88                    .values()
89                    .map(|id| id.as_str().to_string())
90                    .collect(),
91                dirty_slots: instance
92                    .storage
93                    .dirty
94                    .values()
95                    .map(|id| id.as_str().to_string())
96                    .collect(),
97                touched_slots: instance
98                    .storage
99                    .touched
100                    .values()
101                    .map(|id| id.as_str().to_string())
102                    .collect(),
103                validation_slots: instance
104                    .storage
105                    .validation
106                    .values()
107                    .map(|id| id.as_str().to_string())
108                    .collect(),
109                aggregate_slot: instance.storage.aggregate.as_str().to_string(),
110                submission_slot: instance.storage.submission.as_str().to_string(),
111                input_program_fields: instance.input.keys().map(ToString::to_string).collect(),
112                blur_program_fields: instance.blur.keys().map(ToString::to_string).collect(),
113            })
114            .collect::<Vec<_>>();
115        let base = FormInspection {
116            role: "form",
117            form: form.id.to_string(),
118            owner_component: form.owner.entity_id().expect("Form owner").to_string(),
119            field_order: fields.iter().map(|field| field.id.to_string()).collect(),
120            field_semantic_type: None,
121            initial_value: None,
122            bindings: model
123                .form_field_bindings
124                .values()
125                .filter(|binding| binding.form == form.id)
126                .map(|binding| binding.id.to_string())
127                .collect(),
128            binding_channels: model
129                .form_field_bindings
130                .values()
131                .filter(|binding| binding.form == form.id)
132                .map(|binding| form_control_channel_text(binding.channel).to_string())
133                .collect(),
134            validation_rules: model
135                .validation_rules
136                .values()
137                .filter(|rule| rule.owner_form == form.id)
138                .map(|rule| rule.id.to_string())
139                .collect(),
140            source_rules: Vec::new(),
141            dependent_rules: Vec::new(),
142            dirty_tracking: model
143                .form_tracking
144                .dirty
145                .plan(&form.id)
146                .map(|plan| plan.id.as_str().to_string()),
147            touched_tracking: model
148                .form_tracking
149                .touched
150                .plan(&form.id)
151                .map(|plan| plan.id.as_str().to_string()),
152            submission_plan: model
153                .submissions
154                .plan(&form.id)
155                .map(|plan| plan.id.as_str().to_string()),
156            serialization_plan: model
157                .serialization
158                .plans
159                .get(&crate::SerializationPlanId::for_form(&form.id))
160                .map(|plan| plan.id.as_str().to_string()),
161            reset_plan: model
162                .reset
163                .plans
164                .get(&crate::ResetPlanId::for_form(&form.id))
165                .map(|plan| plan.id.as_str().to_string()),
166            instances,
167            runtime_registry_member: model.runtime_forms.forms.contains_key(&form.id),
168            runtime_artifact_member: artifact
169                .forms
170                .iter()
171                .any(|record| record.id == form.id.to_string()),
172            resume_planned: resume
173                .form_instances
174                .iter()
175                .any(|record| record.form == form.id.to_string()),
176            blocked_reasons: Vec::new(),
177        };
178        records.insert(form.id.as_semantic_id().clone(), base.clone());
179        for field in fields {
180            let mut record = base.clone();
181            record.role = "field";
182            record.field_order = vec![field.id.to_string()];
183            record.field_semantic_type = Some(semantic_type_text(&field.semantic_type));
184            record.initial_value = Some(field.initial_value.clone());
185            record.bindings = model
186                .form_field_bindings
187                .values()
188                .filter(|binding| binding.field == field.id)
189                .map(|binding| binding.id.to_string())
190                .collect();
191            record.binding_channels = model
192                .form_field_bindings
193                .values()
194                .filter(|binding| binding.field == field.id)
195                .map(|binding| form_control_channel_text(binding.channel).to_string())
196                .collect();
197            record.validation_rules = model
198                .validation_rules
199                .values()
200                .filter(|rule| rule.target_field == field.id)
201                .map(|rule| rule.id.to_string())
202                .collect();
203            record.source_rules = model
204                .validation_dependency_plans
205                .dependencies
206                .values()
207                .filter(|dependency| dependency.source_field == field.id)
208                .map(|dependency| dependency.dependent_rule.to_string())
209                .collect();
210            record.dependent_rules = model
211                .validation_dependency_plans
212                .dependencies
213                .values()
214                .filter(|dependency| dependency.target_field == field.id)
215                .map(|dependency| dependency.dependent_rule.to_string())
216                .collect();
217            record.dirty_tracking = model
218                .form_tracking
219                .dirty
220                .tracking(&field.id)
221                .map(|tracking| tracking.id.as_str().to_string());
222            record.touched_tracking = model
223                .form_tracking
224                .touched
225                .tracking(&field.id)
226                .map(|tracking| tracking.id.as_str().to_string());
227            records.insert(field.id.as_semantic_id().clone(), record);
228        }
229    }
230    for binding in model.form_field_bindings.values() {
231        let mut record = records
232            .get(binding.form.as_semantic_id())
233            .expect("binding Form inspection")
234            .clone();
235        record.role = "binding";
236        record.bindings = vec![binding.id.to_string()];
237        record.binding_channels = vec![form_control_channel_text(binding.channel).to_string()];
238        records.insert(binding.id.as_semantic_id().clone(), record);
239    }
240    for rule in model.validation_rules.values() {
241        let mut record = records
242            .get(rule.owner_form.as_semantic_id())
243            .expect("rule Form inspection")
244            .clone();
245        record.role = "validation-rule";
246        record.validation_rules = vec![rule.id.to_string()];
247        records.insert(rule.id.as_semantic_id().clone(), record);
248    }
249    FormInspectionRegistry { records }
250}
251
252const fn form_control_channel_text(channel: FormControlChannel) -> &'static str {
253    match channel {
254        FormControlChannel::Value => "value",
255        FormControlChannel::NumericValue => "numeric-value",
256        FormControlChannel::Checked => "checked",
257        FormControlChannel::RadioValue => "radio-value",
258        FormControlChannel::SelectedValue => "selected-value",
259        FormControlChannel::SelectedValues => "selected-values",
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    #[test]
266    fn projects_canonical_form_field_binding_and_rule_records() {
267        let model = crate::build_application_semantic_model(&presolve_parser::parse_file(
268            "src/X.tsx",
269            r#"@component("x") class X { @form() @serialize("json") form!: Form; @field(this.form) value = ""; @action() @submit(this.form) save(): void {} render() { return <form form={this.form}><input field={this.value}/></form>; } }"#,
270        ));
271        let registry = super::build_form_inspection_registry(&model);
272        assert_eq!(registry.records.len(), 3);
273        assert!(registry
274            .records
275            .values()
276            .any(|record| record.role == "form" && record.runtime_artifact_member));
277    }
278}