Skip to main content

_formatparse/
match_rs.rs

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