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(skip_from_py_object)]
22#[derive(Clone)]
23pub struct Match {
24    #[pyo3(get)]
25    pattern: String,
26    field_specs: Vec<FieldSpec>,
27    field_names: Vec<Option<String>>,
28    normalized_names: Vec<Option<String>>,
29    captures: Vec<Option<String>>,           // Raw captured strings
30    named_captures: HashMap<String, String>, // Raw captured strings by normalized name
31    #[pyo3(get)]
32    pub span: (usize, usize),
33    field_spans: HashMap<String, (usize, usize)>, // Spans by original field name
34}
35
36#[pymethods]
37impl Match {
38    /// Evaluate the match to convert captured values to their types
39    #[pyo3(signature = (*, extra_types=None))]
40    fn evaluate_result(
41        &self,
42        py: Python,
43        extra_types: Option<HashMap<String, Py<PyAny>>>,
44    ) -> PyResult<Py<PyAny>> {
45        let custom_converters = extra_types.unwrap_or_default();
46        let mut fixed = Vec::new();
47        let mut named: HashMap<String, Py<PyAny>> = HashMap::new();
48
49        // Apply type conversions using stored field specs
50        for (i, spec) in self.field_specs.iter().enumerate() {
51            let value_str =
52                if let Some(norm_name) = self.normalized_names.get(i).and_then(|n| n.as_ref()) {
53                    self.named_captures
54                        .get(norm_name.as_str())
55                        .map(|s| s.as_str())
56                } else {
57                    self.captures
58                        .get(i)
59                        .and_then(|s| s.as_ref())
60                        .map(|s| s.as_str())
61                };
62
63            if let Some(value_str) = value_str {
64                if !crate::types::conversion::validate_alignment_precision_for_capture(
65                    spec, value_str,
66                ) {
67                    return Err(pyo3::exceptions::PyValueError::new_err(
68                        "capture failed alignment/precision validation",
69                    ));
70                }
71                let converted = crate::types::conversion::convert_value(
72                    spec,
73                    value_str,
74                    py,
75                    &custom_converters,
76                )?;
77
78                if let Some(original_name) = self.field_names.get(i).and_then(|n| n.as_ref()) {
79                    // Check if this is a dict-style field name (contains [])
80                    if original_name.contains('[') {
81                        // Parse the path and insert into nested dict structure
82                        let path = crate::parser::parse_field_path(original_name);
83                        crate::parser::matching::insert_nested_dict(
84                            &mut named, &path, converted, py,
85                        )?;
86                    } else {
87                        // Regular flat field name
88                        // Check for repeated field names - values must match
89                        if let Some(existing_value) = named.get(original_name.as_str()) {
90                            let existing_obj = existing_value.clone_ref(py);
91                            let converted_obj = converted.clone_ref(py);
92                            let are_equal: bool =
93                                existing_obj.bind(py).eq(converted_obj.bind(py))?;
94                            if !are_equal {
95                                return Err(error::repeated_name_error(original_name));
96                            }
97                        }
98                        named.insert(original_name.clone(), converted);
99                    }
100                } else {
101                    fixed.push(converted);
102                }
103            }
104        }
105
106        let parse_result =
107            ParseResult::new_with_spans(fixed, named, self.span, self.field_spans.clone());
108        // Py::new() is already optimized when GIL is held
109        Py::new(py, parse_result)?.into_py_any(py)
110    }
111}
112
113impl Match {
114    pub fn new(init: MatchInit) -> Self {
115        Self {
116            pattern: init.pattern,
117            field_specs: init.field_specs,
118            field_names: init.field_names,
119            normalized_names: init.normalized_names,
120            captures: init.captures,
121            named_captures: init.named_captures,
122            span: init.span,
123            field_spans: init.field_spans,
124        }
125    }
126
127    pub fn with_offset(mut self, offset: usize) -> Self {
128        self.span = (self.span.0 + offset, self.span.1 + offset);
129        self.field_spans = self
130            .field_spans
131            .into_iter()
132            .map(|(k, (start, end))| (k, (start + offset, end + offset)))
133            .collect();
134        self
135    }
136}