Skip to main content

_formatparse/
match_rs.rs

1use crate::error;
2use crate::result::ParseResult;
3use crate::types::FieldSpec;
4use pyo3::prelude::*;
5use pyo3::IntoPyObjectExt;
6use std::collections::HashMap;
7
8/// Owned components for constructing a [`Match`].
9pub struct MatchInit {
10    pub pattern: String,
11    pub field_specs: Vec<FieldSpec>,
12    pub field_names: Vec<Option<String>>,
13    pub normalized_names: Vec<Option<String>>,
14    pub captures: Vec<Option<String>>,
15    pub named_captures: HashMap<String, String>,
16    pub span: (usize, usize),
17    pub field_spans: HashMap<String, (usize, usize)>,
18}
19
20/// Match object that stores raw regex captures without type conversion
21#[pyclass]
22pub struct Match {
23    #[pyo3(get)]
24    pattern: String,
25    field_specs: Vec<FieldSpec>,
26    field_names: Vec<Option<String>>,
27    normalized_names: Vec<Option<String>>,
28    captures: Vec<Option<String>>,           // Raw captured strings
29    named_captures: HashMap<String, String>, // Raw captured strings by normalized name
30    #[pyo3(get)]
31    pub span: (usize, usize),
32    field_spans: HashMap<String, (usize, usize)>, // Spans by original field name
33}
34
35#[pymethods]
36impl Match {
37    /// Evaluate the match to convert captured values to their types
38    #[pyo3(signature = (*, extra_types=None))]
39    fn evaluate_result(
40        &self,
41        py: Python,
42        extra_types: Option<HashMap<String, PyObject>>,
43    ) -> PyResult<PyObject> {
44        let custom_converters = extra_types.unwrap_or_default();
45        let mut fixed = Vec::new();
46        let mut named: HashMap<String, PyObject> = HashMap::new();
47
48        // Apply type conversions using stored field specs
49        for (i, spec) in self.field_specs.iter().enumerate() {
50            let value_str =
51                if let Some(norm_name) = self.normalized_names.get(i).and_then(|n| n.as_ref()) {
52                    self.named_captures
53                        .get(norm_name.as_str())
54                        .map(|s| s.as_str())
55                } else {
56                    self.captures
57                        .get(i)
58                        .and_then(|s| s.as_ref())
59                        .map(|s| s.as_str())
60                };
61
62            if let Some(value_str) = value_str {
63                let converted = crate::types::conversion::convert_value(
64                    spec,
65                    value_str,
66                    py,
67                    &custom_converters,
68                )?;
69
70                if let Some(original_name) = self.field_names.get(i).and_then(|n| n.as_ref()) {
71                    // Check if this is a dict-style field name (contains [])
72                    if original_name.contains('[') {
73                        // Parse the path and insert into nested dict structure
74                        let path = crate::parser::parse_field_path(original_name);
75                        crate::parser::matching::insert_nested_dict(
76                            &mut named, &path, converted, py,
77                        )?;
78                    } else {
79                        // Regular flat field name
80                        // Check for repeated field names - values must match
81                        if let Some(existing_value) = named.get(original_name.as_str()) {
82                            let existing_obj = existing_value.clone_ref(py);
83                            let converted_obj = converted.clone_ref(py);
84                            let are_equal: bool = existing_obj
85                                .bind(py)
86                                .eq(converted_obj.bind(py))
87                                .unwrap_or(false);
88                            if !are_equal {
89                                return Err(error::repeated_name_error(original_name));
90                            }
91                        }
92                        named.insert(original_name.clone(), converted);
93                    }
94                } else {
95                    fixed.push(converted);
96                }
97            }
98        }
99
100        let parse_result =
101            ParseResult::new_with_spans(fixed, named, self.span, self.field_spans.clone());
102        // Py::new() is already optimized when GIL is held
103        Py::new(py, parse_result)?.into_py_any(py)
104    }
105}
106
107impl Match {
108    pub fn new(init: MatchInit) -> Self {
109        Self {
110            pattern: init.pattern,
111            field_specs: init.field_specs,
112            field_names: init.field_names,
113            normalized_names: init.normalized_names,
114            captures: init.captures,
115            named_captures: init.named_captures,
116            span: init.span,
117            field_spans: init.field_spans,
118        }
119    }
120}