1use crate::error;
2use crate::parser::findall_iter::FindallIter;
3use crate::parser::format_parser::{CompiledFields, FormatParser};
4use crate::pattern_cache::get_or_create_parser;
5use fancy_regex::Regex;
6use formatparse_core::count_capturing_groups;
7use formatparse_core::parser::validate_input_length;
8use formatparse_core::{FieldSpec, FieldType};
9use pyo3::exceptions::PyValueError;
10use pyo3::prelude::*;
11use pyo3::types::{PyAnyMethods, PyDict, PyList, PyString, PyTuple};
12use pyo3::IntoPyObjectExt;
13use std::collections::HashMap;
14
15fn merge_call_extra_types(
16 py: Python<'_>,
17 stored: &Option<HashMap<String, Py<PyAny>>>,
18 provided: Option<HashMap<String, Py<PyAny>>>,
19) -> PyResult<Option<HashMap<String, Py<PyAny>>>> {
20 let mut merged = if let Some(ref stored) = stored {
21 stored
22 .iter()
23 .map(|(k, v)| (k.clone(), v.clone_ref(py)))
24 .collect()
25 } else {
26 HashMap::new()
27 };
28 if let Some(provided) = provided {
29 for (k, v) in provided {
30 merged.insert(k.clone(), v.clone_ref(py));
31 }
32 }
33 Ok(Some(merged))
34}
35
36fn field_type_validation_tag(spec: &FieldSpec) -> String {
37 match &spec.field_type {
38 FieldType::String => "s".to_string(),
39 FieldType::Integer => "d".to_string(),
40 FieldType::Float => "f".to_string(),
41 FieldType::Boolean => "b".to_string(),
42 FieldType::Letters => "l".to_string(),
43 FieldType::Word => "w".to_string(),
44 FieldType::NonLetters => "W".to_string(),
45 FieldType::NonWhitespace => "S".to_string(),
46 FieldType::NonDigits => "D".to_string(),
47 FieldType::Custom(name) => name.clone(),
48 FieldType::Nested => "s".to_string(),
49 FieldType::Multiline | FieldType::IndentBlock => "s".to_string(),
50 FieldType::BracedContent => "s".to_string(),
51 FieldType::NumberWithThousands => "n".to_string(),
52 FieldType::Scientific => "e".to_string(),
53 FieldType::GeneralNumber => "g".to_string(),
54 FieldType::Percentage => "%".to_string(),
55 FieldType::DateTimeISO => "ti".to_string(),
56 FieldType::DateTimeRFC2822 => "te".to_string(),
57 FieldType::DateTimeGlobal => "tg".to_string(),
58 FieldType::DateTimeUS => "ta".to_string(),
59 FieldType::DateTimeCtime => "tc".to_string(),
60 FieldType::DateTimeHTTP => "th".to_string(),
61 FieldType::DateTimeTime => "tt".to_string(),
62 FieldType::DateTimeSystem => "ts".to_string(),
63 FieldType::DateTimeStrftime => "%".to_string(),
64 }
65}
66
67#[pymethods]
68impl FormatParser {
69 #[new]
70 #[pyo3(signature = (pattern=None, extra_types=None))]
71 fn new_py(
72 pattern: Option<&str>,
73 extra_types: Option<HashMap<String, Py<PyAny>>>,
74 ) -> PyResult<Self> {
75 match pattern {
76 Some(p) => Self::new_with_extra_types(p, extra_types),
77 None => {
78 let empty_regex =
81 Regex::new("^$").map_err(|e| crate::error::regex_error(&e.to_string()))?;
82 Ok(Self {
83 pattern: String::new(),
84 regex: empty_regex.clone(),
85 regex_str: String::new(),
86 regex_case_insensitive: None,
87 search_regex: empty_regex.clone(),
88 search_regex_case_insensitive: None,
89 fields: CompiledFields {
90 field_specs: Vec::new(),
91 field_names: Vec::new(),
92 normalized_names: Vec::new(),
93 custom_type_groups: Vec::new(),
94 has_nested_dict_fields: Vec::new(),
95 nested_parsers: Vec::new(),
96 field_count: 0,
97 },
98 name_mapping: HashMap::new(),
99 stored_extra_types: None,
100 allows_empty_default_string_match: false,
101 })
102 }
103 }
104 }
105
106 #[pyo3(signature = (string, case_sensitive=false, extra_types=None, evaluate_result=true))]
111 fn parse(
112 &self,
113 string: &str,
114 case_sensitive: bool,
115 extra_types: Option<HashMap<String, Py<PyAny>>>,
116 evaluate_result: bool,
117 ) -> PyResult<Option<Py<PyAny>>> {
118 validate_input_length(string).map_err(PyValueError::new_err)?;
120
121 if string.contains('\0') {
123 return Err(PyValueError::new_err("Input string contains null byte"));
124 }
125 let merged_extra_types =
126 Python::attach(|py| merge_call_extra_types(py, &self.stored_extra_types, extra_types))?;
127 let parser = Python::attach(|py| -> PyResult<std::sync::Arc<FormatParser>> {
128 let cache_et = merged_extra_types.as_ref().map(|m| {
129 m.iter()
130 .map(|(k, v)| (k.clone(), v.clone_ref(py)))
131 .collect()
132 });
133 get_or_create_parser(&self.pattern, cache_et)
134 })?;
135 parser.parse_internal(
136 string,
137 case_sensitive,
138 merged_extra_types.as_ref(),
139 evaluate_result,
140 )
141 }
142
143 #[getter]
145 fn named_fields(&self) -> Vec<String> {
146 self.fields
148 .normalized_names
149 .iter()
150 .filter_map(|n| n.clone())
151 .collect()
152 }
153
154 #[getter]
156 fn field_constraints(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
157 let list = PyList::empty(py);
158 for (i, spec) in self.fields.field_specs.iter().enumerate() {
159 let dict = PyDict::new(py);
160 match self.fields.field_names.get(i).and_then(|n| n.as_deref()) {
161 Some(name) => dict.set_item("name", name)?,
162 None => dict.set_item("name", py.None())?,
163 }
164 dict.set_item("type", field_type_validation_tag(spec))?;
165 match spec.width {
166 Some(w) => dict.set_item("width", w as i64)?,
167 None => dict.set_item("width", py.None())?,
168 }
169 match spec.precision {
170 Some(p) => dict.set_item("precision", p as i64)?,
171 None => dict.set_item("precision", py.None())?,
172 }
173 list.append(dict)?;
174 }
175 list.into_py_any(py)
176 }
177
178 #[getter]
185 fn regex_subpattern(&self) -> String {
186 self.regex_str.clone()
187 }
188
189 #[getter]
194 fn regex_capturing_group_count(&self) -> usize {
195 count_capturing_groups(&self.regex_str)
196 }
197
198 #[getter]
201 fn _expression(&self) -> String {
202 let mut result = self.regex_str.clone();
203
204 result = result.replace(r")\s+(", ") (");
207 result = result.replace(r")\s*(", ") (");
209
210 result = result.replace(
215 r"([+-]?(?:\d+\.\d+|\.\d+|\d+\.)(?:[eE][+-]?\d+)?)",
216 r"([-+ ]?\d*\.\d+)",
217 );
218
219 if result.starts_with("(") && result.ends_with(")") {
223 let inner = &result[1..result.len() - 1];
224 if inner.starts_with(" *(") && inner.ends_with(")") {
226 result = inner.to_string();
228 }
229 }
230
231 result
232 }
233
234 #[getter]
236 fn format(&self) -> Format {
237 Format {
238 pattern: self.pattern.clone(),
239 }
240 }
241
242 #[pyo3(signature = (string, case_sensitive=true, extra_types=None, evaluate_result=true))]
247 fn search(
248 &self,
249 string: &str,
250 case_sensitive: bool,
251 extra_types: Option<HashMap<String, Py<PyAny>>>,
252 evaluate_result: bool,
253 ) -> PyResult<Option<Py<PyAny>>> {
254 validate_input_length(string).map_err(PyValueError::new_err)?;
256
257 if string.contains('\0') {
259 return Err(PyValueError::new_err("Input string contains null byte"));
260 }
261
262 let merged_extra_types =
263 Python::attach(|py| merge_call_extra_types(py, &self.stored_extra_types, extra_types))?;
264 let parser = Python::attach(|py| -> PyResult<std::sync::Arc<FormatParser>> {
265 let cache_et = merged_extra_types.as_ref().map(|m| {
266 m.iter()
267 .map(|(k, v)| (k.clone(), v.clone_ref(py)))
268 .collect()
269 });
270 get_or_create_parser(&self.pattern, cache_et)
271 })?;
272 parser.search_pattern(string, case_sensitive, merged_extra_types, evaluate_result)
273 }
274
275 #[pyo3(signature = (string, case_sensitive=false, extra_types=None, evaluate_result=true, max_matches=None))]
282 fn findall_iter(
283 &self,
284 py: Python<'_>,
285 string: &str,
286 case_sensitive: bool,
287 extra_types: Option<HashMap<String, Py<PyAny>>>,
288 evaluate_result: bool,
289 max_matches: Option<usize>,
290 ) -> PyResult<Py<FindallIter>> {
291 validate_input_length(string).map_err(PyValueError::new_err)?;
292
293 if string.contains('\0') {
294 return Err(PyValueError::new_err("Input string contains null byte"));
295 }
296
297 let merged_extra_types =
298 Python::attach(|py| merge_call_extra_types(py, &self.stored_extra_types, extra_types))?;
299 let merged_map = merged_extra_types.unwrap_or_default();
300 let parser = Python::attach(|py| -> PyResult<std::sync::Arc<FormatParser>> {
301 let cache_et = if merged_map.is_empty() {
302 None
303 } else {
304 Some(
305 merged_map
306 .iter()
307 .map(|(k, v)| (k.clone(), v.clone_ref(py)))
308 .collect(),
309 )
310 };
311 get_or_create_parser(&self.pattern, cache_et)
312 })?;
313 Py::new(
314 py,
315 FindallIter::new(
316 parser,
317 string.to_string(),
318 case_sensitive,
319 evaluate_result,
320 merged_map,
321 max_matches,
322 ),
323 )
324 }
325
326 fn __reduce__(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
331 let m = py.import("_formatparse")?;
332 let compile_fn = m.getattr("compile")?;
333 let args = PyTuple::new(py, [&self.pattern])?;
334 PyTuple::new(py, [compile_fn.as_any(), args.as_any()])?.into_py_any(py)
335 }
336
337 fn __getstate__(&self, py: Python) -> PyResult<Py<PyAny>> {
339 use pyo3::types::PyDict;
340 let state = PyDict::new(py);
341 state.set_item("pattern", &self.pattern)?;
342 state.into_py_any(py)
343 }
344
345 fn __setstate__(&mut self, _py: Python, state: &Bound<'_, PyAny>) -> PyResult<()> {
347 use pyo3::types::PyDict;
348 let dict = state.cast::<PyDict>()?;
349 let pattern: String = dict
350 .get_item("pattern")?
351 .ok_or_else(|| error::missing_field_error("pattern"))?
352 .extract()?;
353
354 let reconstructed = Self::new_with_extra_types(&pattern, None)?;
356
357 self.pattern = reconstructed.pattern;
359 self.regex_str = reconstructed.regex_str;
360 self.regex = reconstructed.regex;
361 self.regex_case_insensitive = reconstructed.regex_case_insensitive;
362 self.search_regex = reconstructed.search_regex;
363 self.search_regex_case_insensitive = reconstructed.search_regex_case_insensitive;
364 self.fields = reconstructed.fields;
365 self.name_mapping = reconstructed.name_mapping;
366 self.stored_extra_types = reconstructed.stored_extra_types;
367 self.allows_empty_default_string_match = reconstructed.allows_empty_default_string_match;
368 Ok(())
369 }
370}
371
372#[pyclass]
374pub struct Format {
375 pattern: String,
376}
377
378#[pymethods]
379impl Format {
380 fn format(&self, py: Python, args: &Bound<'_, PyAny>) -> PyResult<String> {
382 let pattern_obj = PyString::new(py, &self.pattern);
384 let format_method = pattern_obj.getattr("format")?;
385
386 let result = if let Ok(tuple) = args.cast::<PyTuple>() {
388 format_method.call1(tuple)?
389 } else {
390 format_method.call1((args,))?
392 };
393 result.extract()
394 }
395}