Skip to main content

presolve_compiler/
form_validation_definition_lowering.rs

1//! TypeScript-authoritative V2 Form validation recognition.
2
3use std::collections::{BTreeMap, BTreeSet};
4
5use presolve_parser::{ParsedFile, SourceSpan};
6
7use crate::{
8    normalize_authored_semantics_v1, AuthoredSemanticCandidateKindV1,
9    AuthoredSemanticNormalizationErrorV1, AuthoredSourceRangeV1,
10    CanonicalAuthoredDeclarationKindV1, CanonicalAuthoredSemanticModelV1, CanonicalIntrinsicKindV1,
11    ResolvedAuthoredSemanticCandidateV1, ResolvedIntrinsicIdentityV1,
12};
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct FormValidationDefinitionSiteV1 {
16    pub subject: String,
17    pub owner_field_subject: String,
18    pub declaration_source: AuthoredSourceRangeV1,
19    pub callee_source: AuthoredSourceRangeV1,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct ResolvedFormValidationDefinitionV1 {
24    pub callee_source: AuthoredSourceRangeV1,
25    pub kind: ResolvedFormValidationDefinitionKindV1,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum ResolvedFormValidationDefinitionKindV1 {
30    PresolveRule {
31        validation_identity: ResolvedIntrinsicIdentityV1,
32    },
33    StandardSchema {
34        module_specifier: String,
35        export_name: String,
36        declaration_modules: Vec<String>,
37        input_type: Option<String>,
38        output_type: Option<String>,
39    },
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum FormValidationDefinitionLoweringErrorV1 {
44    SourcePathMismatch,
45    DuplicateResolution { start: usize, end: usize },
46    UnknownResolution { start: usize, end: usize },
47    ValidationOutsideCanonicalField { subject: String },
48    InvalidAuthoredSemantics(AuthoredSemanticNormalizationErrorV1),
49}
50
51impl std::fmt::Display for FormValidationDefinitionLoweringErrorV1 {
52    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        match self {
54            Self::SourcePathMismatch => write!(
55                formatter,
56                "Form Field and validation products must describe the same source file"
57            ),
58            Self::DuplicateResolution { start, end } => {
59                write!(
60                    formatter,
61                    "duplicate Form validation resolution at {start}..{end}"
62                )
63            }
64            Self::UnknownResolution { start, end } => {
65                write!(
66                    formatter,
67                    "unknown Form validation resolution at {start}..{end}"
68                )
69            }
70            Self::ValidationOutsideCanonicalField { subject } => write!(
71                formatter,
72                "resolved Form validation `{subject}` is not owned by a canonical Form Field"
73            ),
74            Self::InvalidAuthoredSemantics(error) => error.fmt(formatter),
75        }
76    }
77}
78
79impl std::error::Error for FormValidationDefinitionLoweringErrorV1 {}
80
81pub fn form_validation_definition_sites_v1(
82    parsed: &ParsedFile,
83    component_model: &CanonicalAuthoredSemanticModelV1,
84) -> Result<Vec<FormValidationDefinitionSiteV1>, FormValidationDefinitionLoweringErrorV1> {
85    if component_model.source_path != parsed.path {
86        return Err(FormValidationDefinitionLoweringErrorV1::SourcePathMismatch);
87    }
88    let components = component_model
89        .declarations
90        .iter()
91        .filter(|declaration| declaration.kind == CanonicalAuthoredDeclarationKindV1::Component)
92        .map(|declaration| declaration.subject.as_str())
93        .collect::<BTreeSet<_>>();
94    let mut sites = Vec::new();
95    for class in parsed
96        .classes
97        .iter()
98        .filter(|class| components.contains(class.name.as_str()))
99    {
100        for property in &class.properties {
101            let Some(shape) = property.form_definition_shape.as_ref() else {
102                continue;
103            };
104            for field in &shape.fields {
105                let owner = format!("{}.{}.{}", class.name, property.name, field.path.join("."));
106                for (ordinal, validation) in field.validations.iter().enumerate() {
107                    let Some(callee_span) = validation.callee_span else {
108                        continue;
109                    };
110                    sites.push(FormValidationDefinitionSiteV1 {
111                        subject: format!("{owner}.validation.{ordinal}"),
112                        owner_field_subject: owner.clone(),
113                        declaration_source: range(validation.span),
114                        callee_source: range(callee_span),
115                    });
116                }
117            }
118        }
119    }
120    sites.sort_by_key(|site| (site.callee_source.start, site.subject.clone()));
121    Ok(sites)
122}
123
124pub fn lower_form_validation_definitions_v1(
125    parsed: &ParsedFile,
126    component_model: &CanonicalAuthoredSemanticModelV1,
127    field_model: &CanonicalAuthoredSemanticModelV1,
128    resolutions: impl IntoIterator<Item = ResolvedFormValidationDefinitionV1>,
129) -> Result<CanonicalAuthoredSemanticModelV1, FormValidationDefinitionLoweringErrorV1> {
130    if field_model.source_path != parsed.path {
131        return Err(FormValidationDefinitionLoweringErrorV1::SourcePathMismatch);
132    }
133    let sites = form_validation_definition_sites_v1(parsed, component_model)?;
134    let known = sites
135        .iter()
136        .map(|site| range_key(site.callee_source))
137        .collect::<BTreeSet<_>>();
138    let fields = field_model
139        .declarations
140        .iter()
141        .filter(|declaration| declaration.kind == CanonicalAuthoredDeclarationKindV1::FormField)
142        .map(|declaration| declaration.subject.as_str())
143        .collect::<BTreeSet<_>>();
144    let mut by_site = BTreeMap::new();
145    for resolution in resolutions {
146        let key = range_key(resolution.callee_source);
147        if !known.contains(&key) {
148            return Err(FormValidationDefinitionLoweringErrorV1::UnknownResolution {
149                start: key.0,
150                end: key.1,
151            });
152        }
153        if by_site.insert(key, resolution).is_some() {
154            return Err(
155                FormValidationDefinitionLoweringErrorV1::DuplicateResolution {
156                    start: key.0,
157                    end: key.1,
158                },
159            );
160        }
161    }
162    let candidates = sites
163        .iter()
164        .filter_map(|site| {
165            by_site
166                .get(&range_key(site.callee_source))
167                .map(|proof| (site, proof))
168        })
169        .map(|(site, proof)| {
170            if !fields.contains(site.owner_field_subject.as_str()) {
171                return Err(
172                    FormValidationDefinitionLoweringErrorV1::ValidationOutsideCanonicalField {
173                        subject: site.subject.clone(),
174                    },
175                );
176            }
177            let kind = match &proof.kind {
178                ResolvedFormValidationDefinitionKindV1::PresolveRule {
179                    validation_identity,
180                } => AuthoredSemanticCandidateKindV1::ResolvedIntrinsic {
181                    intrinsic_kind: CanonicalIntrinsicKindV1::Validate,
182                    intrinsic_identity: validation_identity.clone(),
183                },
184                ResolvedFormValidationDefinitionKindV1::StandardSchema {
185                    module_specifier,
186                    export_name,
187                    declaration_modules,
188                    input_type,
189                    output_type,
190                } => AuthoredSemanticCandidateKindV1::DerivedStandardSchemaValidation {
191                    module_specifier: module_specifier.clone(),
192                    export_name: export_name.clone(),
193                    declaration_modules: declaration_modules.clone(),
194                    input_type: input_type.clone(),
195                    output_type: output_type.clone(),
196                },
197            };
198            Ok(ResolvedAuthoredSemanticCandidateV1 {
199                subject: site.subject.clone(),
200                source: site.declaration_source,
201                kind,
202            })
203        })
204        .collect::<Result<Vec<_>, _>>()?;
205    normalize_authored_semantics_v1(parsed, candidates)
206        .map_err(FormValidationDefinitionLoweringErrorV1::InvalidAuthoredSemantics)
207}
208
209fn range(span: SourceSpan) -> AuthoredSourceRangeV1 {
210    AuthoredSourceRangeV1 {
211        start: span.start,
212        end: span.end,
213        line: span.line,
214        column: span.column,
215    }
216}
217
218fn range_key(range: AuthoredSourceRangeV1) -> (usize, usize) {
219    (range.start, range.end)
220}