1use crate::parser::matching::FieldCaptureSlices;
2use crate::result::ParseResult;
3use fancy_regex::Regex;
4use formatparse_core::count_capturing_groups;
5use formatparse_core::parser::MAX_FIELDS;
6use formatparse_core::{FieldSpec, FieldType};
7use pyo3::exceptions::PyValueError;
8use pyo3::prelude::*;
9use std::collections::HashMap;
10use std::sync::Arc;
11
12pub(crate) struct CompiledFields {
14 pub field_specs: Vec<FieldSpec>,
15 pub field_names: Vec<Option<String>>,
16 pub normalized_names: Vec<Option<String>>,
17 pub custom_type_groups: Vec<usize>,
18 pub has_nested_dict_fields: Vec<bool>,
19 pub nested_parsers: Vec<Option<Arc<FormatParser>>>,
20 pub field_count: usize,
21}
22
23impl CompiledFields {
24 pub fn capture_slices(&self) -> FieldCaptureSlices<'_> {
25 FieldCaptureSlices {
26 field_specs: &self.field_specs,
27 field_names: &self.field_names,
28 normalized_names: &self.normalized_names,
29 custom_type_groups: &self.custom_type_groups,
30 has_nested_dict_fields: &self.has_nested_dict_fields,
31 nested_parsers: &self.nested_parsers,
32 }
33 }
34}
35
36#[pyclass(module = "_formatparse", from_py_object)]
37pub struct FormatParser {
49 #[pyo3(get)]
50 pub pattern: String,
53 pub(crate) regex: Regex,
54 pub(crate) regex_str: String, pub(crate) regex_case_insensitive: Option<Regex>,
56 pub(crate) search_regex: Regex, pub(crate) search_regex_case_insensitive: Option<Regex>, pub(crate) fields: CompiledFields,
59 #[allow(dead_code)]
60 pub(crate) name_mapping: std::collections::HashMap<String, String>, pub(crate) stored_extra_types: Option<HashMap<String, Py<PyAny>>>, pub(crate) allows_empty_default_string_match: bool, }
64
65impl FormatParser {
66 pub(crate) fn matches_pattern_cache_request(
70 &self,
71 py: Python<'_>,
72 normalized_pattern: &str,
73 extra_types: &Option<HashMap<String, Py<PyAny>>>,
74 ) -> bool {
75 if self.pattern != normalized_pattern {
76 return false;
77 }
78 let requested = crate::extract_extra_types_identity(py, extra_types);
79 let stored = crate::extract_extra_types_identity(py, &self.stored_extra_types);
80 requested == stored
81 }
82
83 pub fn new(pattern: &str) -> PyResult<Self> {
84 Self::new_with_extra_types(pattern, None)
85 }
86
87 pub fn new_with_extra_types(
88 pattern: &str,
89 extra_types: Option<HashMap<String, Py<PyAny>>>,
90 ) -> PyResult<Self> {
91 let pattern_owned = crate::pattern_normalize::prepare_compiled_pattern(pattern)?;
92 let custom_patterns = Python::attach(|py| -> PyResult<HashMap<String, String>> {
93 let mut patterns = HashMap::new();
94 if let Some(ref extra_types_map) = extra_types {
95 for (name, converter_obj) in extra_types_map {
96 let converter_ref = converter_obj.bind(py);
98 if let Ok(pattern_attr) = converter_ref.getattr("pattern") {
99 if let Ok(pattern_str) = pattern_attr.extract::<String>() {
100 patterns.insert(name.clone(), pattern_str);
101 }
102 }
103 }
104 }
105 Ok(patterns)
106 })?;
107
108 let (
109 regex_str_with_anchors,
110 regex_str,
111 field_specs,
112 field_names,
113 normalized_names,
114 name_mapping,
115 allows_empty_default_string_match,
116 ) = formatparse_core::parser::pattern::parse_pattern(
117 &pattern_owned,
118 &custom_patterns,
119 true,
120 0,
121 )
122 .map_err(crate::parser::pattern::pattern_compile_error_to_py)?;
123
124 let (regex_str_search_anchored, _, _, _, _, _, _) =
127 formatparse_core::parser::pattern::parse_pattern(
128 &pattern_owned,
129 &custom_patterns,
130 false,
131 0,
132 )
133 .map_err(crate::parser::pattern::pattern_compile_error_to_py)?;
134
135 if field_specs.len() > MAX_FIELDS {
137 return Err(PyValueError::new_err(format!(
138 "Pattern contains {} fields, which exceeds the maximum allowed count of {}",
139 field_specs.len(),
140 MAX_FIELDS
141 )));
142 }
143
144 let nested_parsers: Vec<Option<Arc<FormatParser>>> = Python::attach(|py| -> PyResult<_> {
145 let mut out = Vec::with_capacity(field_specs.len());
146 for spec in &field_specs {
147 if matches!(spec.field_type, FieldType::Nested) {
148 let sub = spec.nested_subpattern.as_ref().ok_or_else(|| {
149 PyValueError::new_err("internal error: nested field missing subpattern")
150 })?;
151 let cloned_et = extra_types.as_ref().map(|m| {
152 m.iter()
153 .map(|(k, v)| (k.clone(), v.clone_ref(py)))
154 .collect::<HashMap<_, _>>()
155 });
156 out.push(Some(Arc::new(FormatParser::new_with_extra_types(
157 sub, cloned_et,
158 )?)));
159 } else {
160 out.push(None);
161 }
162 }
163 Ok(out)
164 })?;
165 let custom_type_groups = Python::attach(|py| -> PyResult<Vec<usize>> {
168 let mut groups = Vec::with_capacity(field_specs.len());
169 let empty_map = std::collections::HashMap::new();
170 let custom_converters = extra_types
171 .as_ref()
172 .map(|et| et as &HashMap<String, Py<PyAny>>)
173 .unwrap_or(&empty_map);
174
175 for spec in &field_specs {
176 let pattern_groups = if matches!(spec.field_type, FieldType::Nested) {
177 spec.nested_regex_body
178 .as_ref()
179 .map(|b| count_capturing_groups(b))
180 .unwrap_or(0)
181 } else if !custom_converters.is_empty() {
182 crate::parser::matching::validate_custom_type_pattern(
183 spec,
184 custom_converters,
185 py,
186 )?
187 } else {
188 0
189 };
190 groups.push(pattern_groups);
191 }
192 Ok(groups)
193 })?;
194
195 let has_nested_dict_fields: Vec<bool> = field_names
198 .iter()
199 .map(|name_opt| name_opt.as_ref().map(|n| n.contains('[')).unwrap_or(false))
200 .collect();
201
202 let regex = formatparse_core::build_regex(®ex_str_with_anchors)
204 .map_err(crate::error::core_error_to_py_err)?;
205
206 let regex_search_anchored = formatparse_core::build_regex(®ex_str_search_anchored)
207 .map_err(crate::error::core_error_to_py_err)?;
208
209 let regex_case_insensitive =
211 formatparse_core::build_case_insensitive_regex(®ex_str_with_anchors);
212
213 let search_regex =
215 formatparse_core::build_search_regex(regex_search_anchored.as_str(), true)
216 .map_err(crate::error::core_error_to_py_err)?;
217 let search_regex_case_insensitive =
218 formatparse_core::build_search_regex(regex_search_anchored.as_str(), false).ok();
219
220 let field_count = field_specs.len();
221 Ok(Self {
222 pattern: pattern_owned,
223 regex,
224 regex_str,
225 regex_case_insensitive,
226 search_regex,
227 search_regex_case_insensitive,
228 fields: CompiledFields {
229 field_specs,
230 field_names,
231 normalized_names,
232 custom_type_groups,
233 has_nested_dict_fields,
234 nested_parsers,
235 field_count,
236 },
237 name_mapping,
238 stored_extra_types: extra_types,
239 allows_empty_default_string_match,
240 })
241 }
242
243 pub fn search_pattern(
244 &self,
245 string: &str,
246 case_sensitive: bool,
247 extra_types: Option<HashMap<String, Py<PyAny>>>,
248 evaluate_result: bool,
249 ) -> PyResult<Option<Py<PyAny>>> {
250 let search_regex = if case_sensitive {
252 &self.search_regex
253 } else {
254 self.search_regex_case_insensitive
255 .as_ref()
256 .unwrap_or(&self.search_regex)
257 };
258
259 Python::attach(|py| {
260 if search_regex
261 .captures(string)
262 .map_err(crate::error::fancy_regex_match_error)?
263 .is_some()
264 {
265 let extra_types_ref = if let Some(ref et) = extra_types {
266 et
267 } else {
268 &HashMap::new()
269 };
270 let f = &self.fields;
271 return crate::parser::matching::match_with_regex(
272 search_regex,
273 &crate::parser::matching::RegexMatchContext {
274 string,
275 pattern: &self.pattern,
276 field_specs: &f.field_specs,
277 field_names: &f.field_names,
278 normalized_names: &f.normalized_names,
279 nested_parsers: &f.nested_parsers,
280 py,
281 custom_converters: extra_types_ref,
282 evaluate_result,
283 },
284 );
285 }
286 Ok(None)
287 })
288 }
289
290 pub(crate) fn parse_internal(
291 &self,
292 string: &str,
293 case_sensitive: bool,
294 extra_types: Option<&HashMap<String, Py<PyAny>>>,
295 evaluate_result: bool,
296 ) -> PyResult<Option<Py<PyAny>>> {
297 Python::attach(|py| {
298 let empty = HashMap::<String, Py<PyAny>>::new();
299 let extra_types_ref = extra_types.unwrap_or(&empty);
300
301 let regex = if case_sensitive {
303 &self.regex
304 } else {
305 self.regex_case_insensitive.as_ref().unwrap_or(&self.regex)
306 };
307
308 let f = &self.fields;
309 if string.is_empty()
310 && self.allows_empty_default_string_match
311 && !f.field_specs.is_empty()
312 {
313 if let Some(obj) = crate::parser::matching::match_empty_default_string_parse(
314 &self.pattern,
315 &f.field_specs,
316 &f.field_names,
317 &f.normalized_names,
318 py,
319 extra_types_ref,
320 evaluate_result,
321 )? {
322 return Ok(Some(obj));
323 }
324 }
325
326 crate::parser::matching::match_with_regex(
327 regex,
328 &crate::parser::matching::RegexMatchContext {
329 string,
330 pattern: &self.pattern,
331 field_specs: &f.field_specs,
332 field_names: &f.field_names,
333 normalized_names: &f.normalized_names,
334 nested_parsers: &f.nested_parsers,
335 py,
336 custom_converters: extra_types_ref,
337 evaluate_result,
338 },
339 )
340 })
341 }
342
343 pub(crate) fn get_search_regex(&self, case_sensitive: bool) -> &Regex {
345 if case_sensitive {
346 &self.search_regex
347 } else {
348 self.search_regex_case_insensitive
349 .as_ref()
350 .unwrap_or(&self.search_regex)
351 }
352 }
353
354 pub(crate) fn parse_nested_capture(
356 &self,
357 py: Python<'_>,
358 slice: &str,
359 custom_converters: &HashMap<String, Py<PyAny>>,
360 ) -> PyResult<Option<Py<ParseResult>>> {
361 let mut merged = HashMap::new();
362 if let Some(ref stored) = self.stored_extra_types {
363 for (k, v) in stored {
364 merged.insert(k.clone(), v.clone_ref(py));
365 }
366 }
367 for (k, v) in custom_converters {
368 merged.insert(k.clone(), v.clone_ref(py));
369 }
370 let opt = self.parse_internal(slice, true, Some(&merged), true)?;
371 let Some(obj) = opt else {
372 return Ok(None);
373 };
374 let bound = obj.bind(py);
375 let pr = bound.cast::<ParseResult>().map_err(|_| {
376 PyValueError::new_err("internal error: nested parse did not return ParseResult")
377 })?;
378 Ok(Some(pr.clone().unbind()))
379 }
380}
381
382impl Clone for FormatParser {
383 fn clone(&self) -> Self {
384 Python::attach(|py| Self {
385 pattern: self.pattern.clone(),
386 regex: self.regex.clone(),
387 regex_str: self.regex_str.clone(),
388 regex_case_insensitive: self.regex_case_insensitive.clone(),
389 search_regex: self.search_regex.clone(),
390 search_regex_case_insensitive: self.search_regex_case_insensitive.clone(),
391 fields: CompiledFields {
392 field_specs: self.fields.field_specs.clone(),
393 field_names: self.fields.field_names.clone(),
394 normalized_names: self.fields.normalized_names.clone(),
395 custom_type_groups: self.fields.custom_type_groups.clone(),
396 has_nested_dict_fields: self.fields.has_nested_dict_fields.clone(),
397 nested_parsers: self.fields.nested_parsers.clone(),
398 field_count: self.fields.field_count,
399 },
400 name_mapping: self.name_mapping.clone(),
401 stored_extra_types: self.stored_extra_types.as_ref().map(|m| {
402 m.iter()
403 .map(|(k, v)| (k.clone(), v.clone_ref(py)))
404 .collect()
405 }),
406 allows_empty_default_string_match: self.allows_empty_default_string_match,
407 })
408 }
409}