Skip to main content

engine_input_producers/
expression_domain.rs

1use std::collections::BTreeMap;
2
3use crate::{
4    ConstraintDetailCounts, ConstraintDetailInput, EngineInputV2, ExpressionDomainCandidateV0,
5    ExpressionDomainCandidatesV0, ExpressionDomainCanonicalCandidateBundleV0,
6    ExpressionDomainCanonicalProducerSignalV0, ExpressionDomainControlFlowAnalysisEntryV0,
7    ExpressionDomainControlFlowAnalysisV0, ExpressionDomainEvaluatorCandidatePayloadV0,
8    ExpressionDomainEvaluatorCandidateV0, ExpressionDomainEvaluatorCandidatesV0,
9    ExpressionDomainFlowAnalysisEntryV0, ExpressionDomainFlowAnalysisV0,
10    ExpressionDomainFlowGraphEntryV0, ExpressionDomainFragmentV0, ExpressionDomainFragmentsV0,
11    ExpressionDomainPlanSummaryV0, TypeFactEntryV2, abstract_value_facts,
12    collect_constraint_detail_counts, map_reduced_expression_value_domain_derivation,
13    map_reduced_expression_value_domain_kind,
14};
15
16struct ExpressionDomainInputRows {
17    plan_summary: ExpressionDomainPlanSummaryV0,
18    fragments: Vec<ExpressionDomainFragmentV0>,
19    candidates: Vec<ExpressionDomainCandidateV0>,
20    evaluator_candidates: Vec<ExpressionDomainEvaluatorCandidateV0>,
21}
22
23fn collect_expression_domain_input_rows(input: &EngineInputV2) -> ExpressionDomainInputRows {
24    let mut planned_expression_ids = Vec::new();
25    let mut value_domain_kinds = BTreeMap::new();
26    let mut value_constraint_kinds = BTreeMap::new();
27    let mut constraint_detail_counts = ConstraintDetailCounts::default();
28    let mut finite_value_count = 0usize;
29    let mut fragments = Vec::new();
30    let mut candidates = Vec::new();
31    let mut evaluator_candidates = Vec::new();
32
33    for entry in &input.type_facts {
34        planned_expression_ids.push(entry.expression_id.clone());
35        *value_domain_kinds
36            .entry(entry.facts.kind.clone())
37            .or_insert(0) += 1;
38
39        if let Some(values) = &entry.facts.values {
40            finite_value_count += values.len();
41        }
42
43        if let Some(constraint_kind) = &entry.facts.constraint_kind {
44            *value_constraint_kinds
45                .entry(constraint_kind.clone())
46                .or_insert(0) += 1;
47        }
48
49        collect_constraint_detail_counts(
50            &mut constraint_detail_counts,
51            ConstraintDetailInput {
52                prefix: entry.facts.prefix.as_ref(),
53                suffix: entry.facts.suffix.as_ref(),
54                min_len: entry.facts.min_len,
55                max_len: entry.facts.max_len,
56                char_must: entry.facts.char_must.as_ref(),
57                char_may: entry.facts.char_may.as_ref(),
58                may_include_other_chars: entry.facts.may_include_other_chars,
59            },
60        );
61
62        let fragment = ExpressionDomainFragmentV0 {
63            expression_id: entry.expression_id.clone(),
64            file_path: entry.file_path.clone(),
65            value_domain_kind: entry.facts.kind.clone(),
66            value_constraint_kind: entry.facts.constraint_kind.clone(),
67            value_prefix: entry.facts.prefix.clone(),
68            value_suffix: entry.facts.suffix.clone(),
69            value_min_len: entry.facts.min_len,
70            value_max_len: entry.facts.max_len,
71            value_char_must: entry.facts.char_must.clone(),
72            value_char_may: entry.facts.char_may.clone(),
73            value_may_include_other_chars: entry.facts.may_include_other_chars,
74            finite_value_count: entry.facts.values.as_ref().map_or(0, Vec::len),
75        };
76        fragments.push(fragment.clone());
77        candidates.push(ExpressionDomainCandidateV0 {
78            expression_id: fragment.expression_id,
79            file_path: fragment.file_path,
80            value_domain_kind: fragment.value_domain_kind,
81            value_constraint_kind: fragment.value_constraint_kind,
82            value_prefix: fragment.value_prefix,
83            value_suffix: fragment.value_suffix,
84            value_min_len: fragment.value_min_len,
85            value_max_len: fragment.value_max_len,
86            value_char_must: fragment.value_char_must,
87            value_char_may: fragment.value_char_may,
88            value_may_include_other_chars: fragment.value_may_include_other_chars,
89            finite_value_count: fragment.finite_value_count,
90        });
91
92        evaluator_candidates.push(ExpressionDomainEvaluatorCandidateV0 {
93            kind: "expression-domain",
94            file_path: entry.file_path.clone(),
95            query_id: entry.expression_id.clone(),
96            payload: ExpressionDomainEvaluatorCandidatePayloadV0 {
97                expression_id: entry.expression_id.clone(),
98                value_domain_kind: map_reduced_expression_value_domain_kind(&entry.facts),
99                value_constraint_kind: entry.facts.constraint_kind.clone(),
100                value_prefix: entry.facts.prefix.clone(),
101                value_suffix: entry.facts.suffix.clone(),
102                value_min_len: entry.facts.min_len,
103                value_max_len: entry.facts.max_len,
104                value_char_must: entry.facts.char_must.clone(),
105                value_char_may: entry.facts.char_may.clone(),
106                value_may_include_other_chars: entry.facts.may_include_other_chars,
107                finite_value_count: entry.facts.values.as_ref().map_or(0, Vec::len),
108                value_domain_derivation: map_reduced_expression_value_domain_derivation(
109                    &entry.facts,
110                ),
111            },
112        });
113    }
114
115    fragments.sort_by(|a, b| a.expression_id.cmp(&b.expression_id));
116    candidates.sort_by(|a, b| a.expression_id.cmp(&b.expression_id));
117    evaluator_candidates.sort_by(|a, b| a.query_id.cmp(&b.query_id));
118
119    ExpressionDomainInputRows {
120        plan_summary: ExpressionDomainPlanSummaryV0 {
121            schema_version: "0",
122            input_version: input.version.clone(),
123            planned_expression_ids,
124            value_domain_kinds,
125            value_constraint_kinds,
126            constraint_detail_counts,
127            finite_value_count,
128        },
129        fragments,
130        candidates,
131        evaluator_candidates,
132    }
133}
134
135pub fn summarize_expression_domain_plan_input(
136    input: &EngineInputV2,
137) -> ExpressionDomainPlanSummaryV0 {
138    collect_expression_domain_input_rows(input).plan_summary
139}
140
141pub fn summarize_expression_domain_fragments_input(
142    input: &EngineInputV2,
143) -> ExpressionDomainFragmentsV0 {
144    let rows = collect_expression_domain_input_rows(input);
145
146    ExpressionDomainFragmentsV0 {
147        schema_version: "0",
148        input_version: input.version.clone(),
149        fragments: rows.fragments,
150    }
151}
152
153pub fn summarize_expression_domain_candidates_input(
154    input: &EngineInputV2,
155) -> ExpressionDomainCandidatesV0 {
156    let rows = collect_expression_domain_input_rows(input);
157
158    ExpressionDomainCandidatesV0 {
159        schema_version: "0",
160        input_version: input.version.clone(),
161        candidates: rows.candidates,
162    }
163}
164
165pub fn summarize_expression_domain_canonical_candidate_bundle_input(
166    input: &EngineInputV2,
167) -> ExpressionDomainCanonicalCandidateBundleV0 {
168    let rows = collect_expression_domain_input_rows(input);
169
170    ExpressionDomainCanonicalCandidateBundleV0 {
171        schema_version: "0",
172        input_version: input.version.clone(),
173        plan_summary: rows.plan_summary,
174        fragments: rows.fragments,
175        candidates: rows.candidates,
176    }
177}
178
179pub fn summarize_expression_domain_evaluator_candidates_input(
180    input: &EngineInputV2,
181) -> ExpressionDomainEvaluatorCandidatesV0 {
182    let rows = collect_expression_domain_input_rows(input);
183
184    ExpressionDomainEvaluatorCandidatesV0 {
185        schema_version: "0",
186        input_version: input.version.clone(),
187        results: rows.evaluator_candidates,
188    }
189}
190
191pub fn summarize_expression_domain_canonical_producer_signal_input(
192    input: &EngineInputV2,
193) -> ExpressionDomainCanonicalProducerSignalV0 {
194    let rows = collect_expression_domain_input_rows(input);
195    let input_version = input.version.clone();
196
197    ExpressionDomainCanonicalProducerSignalV0 {
198        schema_version: "0",
199        input_version: input_version.clone(),
200        canonical_bundle: ExpressionDomainCanonicalCandidateBundleV0 {
201            schema_version: "0",
202            input_version: input_version.clone(),
203            plan_summary: rows.plan_summary,
204            fragments: rows.fragments,
205            candidates: rows.candidates,
206        },
207        evaluator_candidates: ExpressionDomainEvaluatorCandidatesV0 {
208            schema_version: "0",
209            input_version,
210            results: rows.evaluator_candidates,
211        },
212    }
213}
214
215pub fn summarize_expression_domain_flow_analysis_input(
216    input: &EngineInputV2,
217) -> ExpressionDomainFlowAnalysisV0 {
218    let analyses = collect_expression_domain_flow_graphs(input)
219        .into_iter()
220        .map(|entry| ExpressionDomainFlowAnalysisEntryV0 {
221            graph_id: entry.graph_id,
222            file_path: entry.file_path,
223            analysis: omena_abstract_value::analyze_class_value_flow(&entry.graph),
224        })
225        .collect();
226
227    ExpressionDomainFlowAnalysisV0 {
228        schema_version: "0",
229        product: "engine-input-producers.expression-domain-flow-analysis",
230        input_version: input.version.clone(),
231        analyses,
232    }
233}
234
235pub fn summarize_expression_domain_control_flow_analysis_input(
236    input: &EngineInputV2,
237) -> ExpressionDomainControlFlowAnalysisV0 {
238    let analyses = collect_expression_domain_flow_graphs(input)
239        .into_iter()
240        .map(|entry| {
241            let cfg = expression_domain_control_flow_graph(&entry.graph);
242            ExpressionDomainControlFlowAnalysisEntryV0 {
243                graph_id: entry.graph_id,
244                file_path: entry.file_path,
245                analysis: omena_abstract_value::analyze_class_value_control_flow_graph(&cfg),
246            }
247        })
248        .collect();
249
250    ExpressionDomainControlFlowAnalysisV0 {
251        schema_version: "0",
252        product: "engine-input-producers.expression-domain-control-flow-analysis",
253        input_version: input.version.clone(),
254        analyses,
255    }
256}
257
258pub fn collect_expression_domain_flow_graphs(
259    input: &EngineInputV2,
260) -> Vec<ExpressionDomainFlowGraphEntryV0> {
261    let mut by_file = BTreeMap::<String, Vec<&TypeFactEntryV2>>::new();
262    for entry in &input.type_facts {
263        by_file
264            .entry(entry.file_path.clone())
265            .or_default()
266            .push(entry);
267    }
268
269    by_file
270        .into_iter()
271        .map(|(file_path, mut entries)| {
272            entries.sort_by(|a, b| a.expression_id.cmp(&b.expression_id));
273            let graph_id = format!("{file_path}:expression-domain-flow");
274            let mut nodes = entries
275                .iter()
276                .map(|entry| omena_abstract_value::ClassValueFlowNodeV0 {
277                    id: entry.expression_id.clone(),
278                    predecessors: Vec::new(),
279                    transfer: omena_abstract_value::ClassValueFlowTransferV0::AssignFacts(
280                        abstract_value_facts(&entry.facts),
281                    ),
282                })
283                .collect::<Vec<_>>();
284
285            if entries.len() > 1 {
286                nodes.push(omena_abstract_value::ClassValueFlowNodeV0 {
287                    id: "file-merge".to_string(),
288                    predecessors: entries
289                        .iter()
290                        .map(|entry| entry.expression_id.clone())
291                        .collect(),
292                    transfer: omena_abstract_value::ClassValueFlowTransferV0::Join,
293                });
294            }
295
296            let graph = omena_abstract_value::ClassValueFlowGraphV0 {
297                context_key: Some(graph_id.clone()),
298                nodes,
299            };
300
301            ExpressionDomainFlowGraphEntryV0 {
302                graph_id,
303                file_path,
304                graph,
305            }
306        })
307        .collect()
308}
309
310fn expression_domain_control_flow_graph(
311    graph: &omena_abstract_value::ClassValueFlowGraphV0,
312) -> omena_abstract_value::ClassValueControlFlowGraphV0 {
313    let merge_node_id = "file-merge";
314    let has_merge = graph.nodes.iter().any(|node| node.id == merge_node_id);
315    let mut blocks = Vec::new();
316
317    if has_merge {
318        blocks.push(omena_abstract_value::ClassValueControlFlowBlockV0 {
319            id: "entry".to_string(),
320            nodes: Vec::new(),
321            successor_block_ids: graph
322                .nodes
323                .iter()
324                .filter(|node| node.id != merge_node_id)
325                .map(|node| format!("expr:{}", node.id))
326                .collect(),
327        });
328
329        for node in graph.nodes.iter().filter(|node| node.id != merge_node_id) {
330            blocks.push(omena_abstract_value::ClassValueControlFlowBlockV0 {
331                id: format!("expr:{}", node.id),
332                nodes: vec![node.clone()],
333                successor_block_ids: vec!["merge".to_string()],
334            });
335        }
336
337        if let Some(merge) = graph.nodes.iter().find(|node| node.id == merge_node_id) {
338            blocks.push(omena_abstract_value::ClassValueControlFlowBlockV0 {
339                id: "merge".to_string(),
340                nodes: vec![merge.clone()],
341                successor_block_ids: Vec::new(),
342            });
343        }
344    } else {
345        blocks.extend(graph.nodes.iter().map(|node| {
346            omena_abstract_value::ClassValueControlFlowBlockV0 {
347                id: format!("expr:{}", node.id),
348                nodes: vec![node.clone()],
349                successor_block_ids: Vec::new(),
350            }
351        }));
352    }
353
354    let entry_block_id = blocks
355        .first()
356        .map(|block| block.id.clone())
357        .unwrap_or_else(|| "entry".to_string());
358
359    omena_abstract_value::ClassValueControlFlowGraphV0 {
360        context_key: graph.context_key.clone(),
361        entry_block_id,
362        blocks,
363    }
364}
365
366#[cfg(test)]
367mod tests {
368    use super::{
369        collect_expression_domain_flow_graphs, summarize_expression_domain_candidates_input,
370        summarize_expression_domain_canonical_candidate_bundle_input,
371        summarize_expression_domain_canonical_producer_signal_input,
372        summarize_expression_domain_control_flow_analysis_input,
373        summarize_expression_domain_evaluator_candidates_input,
374        summarize_expression_domain_flow_analysis_input,
375        summarize_expression_domain_fragments_input, summarize_expression_domain_plan_input,
376    };
377    use crate::{StringTypeFactsV2, TypeFactEntryV2, test_support::sample_input};
378    use omena_abstract_value::AbstractClassValueV0;
379
380    #[test]
381    fn summarizes_expression_domain_counts() {
382        let summary = summarize_expression_domain_plan_input(&sample_input());
383
384        assert_eq!(
385            summary.planned_expression_ids,
386            vec!["expr-1".to_string(), "expr-2".to_string()]
387        );
388        assert_eq!(summary.value_domain_kinds.get("constrained"), Some(&1));
389        assert_eq!(summary.value_domain_kinds.get("finiteSet"), Some(&1));
390        assert_eq!(summary.value_constraint_kinds.get("prefixSuffix"), Some(&1));
391        assert_eq!(summary.constraint_detail_counts.prefix_count, 1);
392        assert_eq!(summary.constraint_detail_counts.suffix_count, 1);
393        assert_eq!(summary.constraint_detail_counts.min_len_count, 1);
394        assert_eq!(summary.finite_value_count, 2);
395    }
396
397    #[test]
398    fn summarizes_expression_domain_fragments() {
399        let summary = summarize_expression_domain_fragments_input(&sample_input());
400
401        assert_eq!(summary.fragments.len(), 2);
402        let first = &summary.fragments[0];
403        assert_eq!(first.expression_id, "expr-1");
404        assert_eq!(first.file_path, "/tmp/App.tsx");
405        assert_eq!(first.value_domain_kind, "constrained");
406        assert_eq!(first.value_constraint_kind.as_deref(), Some("prefixSuffix"));
407        assert_eq!(first.value_prefix.as_deref(), Some("btn-"));
408        assert_eq!(first.value_suffix.as_deref(), Some("-active"));
409        assert_eq!(first.value_min_len, Some(10));
410        assert_eq!(first.finite_value_count, 0);
411
412        let second = &summary.fragments[1];
413        assert_eq!(second.expression_id, "expr-2");
414        assert_eq!(second.value_domain_kind, "finiteSet");
415        assert_eq!(second.finite_value_count, 2);
416    }
417
418    #[test]
419    fn summarizes_expression_domain_candidates() {
420        let summary = summarize_expression_domain_candidates_input(&sample_input());
421
422        assert_eq!(summary.candidates.len(), 2);
423        assert_eq!(summary.candidates[0].expression_id, "expr-1");
424        assert_eq!(summary.candidates[0].value_domain_kind, "constrained");
425        assert_eq!(
426            summary.candidates[0].value_constraint_kind.as_deref(),
427            Some("prefixSuffix")
428        );
429        assert_eq!(summary.candidates[1].expression_id, "expr-2");
430        assert_eq!(summary.candidates[1].finite_value_count, 2);
431    }
432
433    #[test]
434    fn summarizes_expression_domain_canonical_candidate_bundle() {
435        let summary = summarize_expression_domain_canonical_candidate_bundle_input(&sample_input());
436
437        assert_eq!(summary.plan_summary.planned_expression_ids.len(), 2);
438        assert_eq!(summary.fragments.len(), 2);
439        assert_eq!(summary.candidates.len(), 2);
440    }
441
442    #[test]
443    fn summarizes_expression_domain_evaluator_candidates() {
444        let summary = summarize_expression_domain_evaluator_candidates_input(&sample_input());
445
446        assert_eq!(summary.schema_version, "0");
447        assert_eq!(summary.input_version, "2");
448        assert_eq!(summary.results.len(), 2);
449        assert_eq!(summary.results[0].kind, "expression-domain");
450        assert_eq!(summary.results[0].query_id, "expr-1");
451        assert_eq!(summary.results[0].payload.value_domain_kind, "prefixSuffix");
452        assert_eq!(
453            summary.results[0].payload.value_constraint_kind.as_deref(),
454            Some("prefixSuffix")
455        );
456        assert_eq!(summary.results[1].payload.finite_value_count, 2);
457    }
458
459    #[test]
460    fn expression_domain_evaluator_reports_reduced_value_domain_kind() {
461        let mut input = sample_input();
462        input.type_facts.push(TypeFactEntryV2 {
463            file_path: "/tmp/App.tsx".to_string(),
464            expression_id: "expr-3".to_string(),
465            facts: StringTypeFactsV2 {
466                kind: "finiteSet".to_string(),
467                constraint_kind: Some("prefix".to_string()),
468                values: Some(vec!["btn-active".to_string(), "card".to_string()]),
469                prefix: Some("btn-".to_string()),
470                suffix: None,
471                min_len: None,
472                max_len: None,
473                char_must: None,
474                char_may: None,
475                may_include_other_chars: None,
476            },
477        });
478
479        let fragments = summarize_expression_domain_fragments_input(&input);
480        let candidates = summarize_expression_domain_candidates_input(&input);
481        let evaluator_candidates = summarize_expression_domain_evaluator_candidates_input(&input);
482
483        assert_eq!(fragments.fragments[2].expression_id, "expr-3");
484        assert_eq!(fragments.fragments[2].value_domain_kind, "finiteSet");
485        assert_eq!(candidates.candidates[2].expression_id, "expr-3");
486        assert_eq!(candidates.candidates[2].value_domain_kind, "finiteSet");
487        assert_eq!(evaluator_candidates.results[2].query_id, "expr-3");
488        assert_eq!(
489            evaluator_candidates.results[2].payload.value_domain_kind,
490            "exact"
491        );
492        assert_eq!(
493            evaluator_candidates.results[2]
494                .payload
495                .value_domain_derivation
496                .reduced_kind,
497            "exact"
498        );
499        assert_eq!(
500            evaluator_candidates.results[2]
501                .payload
502                .value_domain_derivation
503                .steps[1]
504                .operation,
505            "intersectConstraint"
506        );
507    }
508
509    #[test]
510    fn summarizes_expression_domain_flow_analysis() {
511        let mut input = sample_input();
512        input.type_facts = vec![
513            exact_type_fact("expr-branch-a", "btn-primary"),
514            exact_type_fact("expr-branch-b", "btn-secondary"),
515            exact_type_fact("expr-branch-c", "card"),
516        ];
517
518        let summary = summarize_expression_domain_flow_analysis_input(&input);
519
520        assert_eq!(summary.schema_version, "0");
521        assert_eq!(
522            summary.product,
523            "engine-input-producers.expression-domain-flow-analysis"
524        );
525        assert_eq!(summary.analyses.len(), 1);
526        assert_eq!(summary.analyses[0].file_path, "/tmp/App.tsx");
527        assert_eq!(summary.analyses[0].analysis.context_sensitivity, "1-cfa");
528        assert!(summary.analyses[0].analysis.converged);
529        assert_eq!(
530            summary.analyses[0]
531                .analysis
532                .nodes
533                .iter()
534                .find(|node| node.id == "file-merge")
535                .map(|node| (node.value_kind, &node.value)),
536            Some((
537                "finiteSet",
538                &AbstractClassValueV0::FiniteSet {
539                    values: vec![
540                        "btn-primary".to_string(),
541                        "btn-secondary".to_string(),
542                        "card".to_string(),
543                    ]
544                }
545            ))
546        );
547    }
548
549    #[test]
550    fn exposes_expression_domain_flow_graphs_for_query_runtime_reuse() {
551        let mut input = sample_input();
552        input.type_facts = vec![
553            exact_type_fact("expr-branch-a", "btn-primary"),
554            exact_type_fact("expr-branch-b", "btn-secondary"),
555        ];
556
557        let graphs = collect_expression_domain_flow_graphs(&input);
558
559        assert_eq!(graphs.len(), 1);
560        assert_eq!(graphs[0].graph_id, "/tmp/App.tsx:expression-domain-flow");
561        assert_eq!(
562            graphs[0].graph.context_key.as_deref(),
563            Some(graphs[0].graph_id.as_str())
564        );
565        assert!(
566            graphs[0]
567                .graph
568                .nodes
569                .iter()
570                .any(|node| node.id == "file-merge")
571        );
572    }
573
574    #[test]
575    fn summarizes_expression_domain_control_flow_analysis() {
576        let mut input = sample_input();
577        input.type_facts = vec![
578            exact_type_fact("expr-branch-a", "btn-primary"),
579            exact_type_fact("expr-branch-b", "btn-secondary"),
580        ];
581
582        let summary = summarize_expression_domain_control_flow_analysis_input(&input);
583
584        assert_eq!(
585            summary.product,
586            "engine-input-producers.expression-domain-control-flow-analysis"
587        );
588        assert_eq!(summary.analyses.len(), 1);
589        assert_eq!(summary.analyses[0].analysis.block_count, 4);
590        assert_eq!(summary.analyses[0].analysis.edge_count, 4);
591        assert_eq!(
592            summary.analyses[0].analysis.flow_analysis.product,
593            "omena-abstract-value.flow-analysis"
594        );
595        assert!(
596            summary.analyses[0]
597                .analysis
598                .unreachable_block_ids
599                .is_empty()
600        );
601    }
602
603    #[test]
604    fn summarizes_expression_domain_canonical_producer_signal() {
605        let summary = summarize_expression_domain_canonical_producer_signal_input(&sample_input());
606
607        assert_eq!(summary.schema_version, "0");
608        assert_eq!(summary.input_version, "2");
609        assert_eq!(
610            summary
611                .canonical_bundle
612                .plan_summary
613                .planned_expression_ids
614                .len(),
615            2
616        );
617        assert_eq!(summary.canonical_bundle.fragments.len(), 2);
618        assert_eq!(summary.canonical_bundle.candidates.len(), 2);
619        assert_eq!(summary.evaluator_candidates.results.len(), 2);
620    }
621
622    fn exact_type_fact(expression_id: &str, value: &str) -> TypeFactEntryV2 {
623        TypeFactEntryV2 {
624            file_path: "/tmp/App.tsx".to_string(),
625            expression_id: expression_id.to_string(),
626            facts: StringTypeFactsV2 {
627                kind: "exact".to_string(),
628                constraint_kind: None,
629                values: Some(vec![value.to_string()]),
630                prefix: None,
631                suffix: None,
632                min_len: None,
633                max_len: None,
634                char_must: None,
635                char_may: None,
636                may_include_other_chars: None,
637            },
638        }
639    }
640}