use crate::error;
use crate::parser::findall_iter::FindallIter;
use crate::parser::format_parser::{CompiledFields, FormatParser};
use crate::pattern_cache::get_or_create_parser;
use fancy_regex::Regex;
use formatparse_core::count_capturing_groups;
use formatparse_core::parser::validate_input_length;
use formatparse_core::{FieldSpec, FieldType};
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3::types::{PyAnyMethods, PyDict, PyList, PyString, PyTuple};
use pyo3::IntoPyObjectExt;
use std::collections::HashMap;
fn merge_call_extra_types(
py: Python<'_>,
stored: &Option<HashMap<String, Py<PyAny>>>,
provided: Option<HashMap<String, Py<PyAny>>>,
) -> PyResult<Option<HashMap<String, Py<PyAny>>>> {
let mut merged = if let Some(ref stored) = stored {
stored
.iter()
.map(|(k, v)| (k.clone(), v.clone_ref(py)))
.collect()
} else {
HashMap::new()
};
if let Some(provided) = provided {
for (k, v) in provided {
merged.insert(k.clone(), v.clone_ref(py));
}
}
Ok(Some(merged))
}
fn field_type_validation_tag(spec: &FieldSpec) -> String {
match &spec.field_type {
FieldType::String => "s".to_string(),
FieldType::Integer => "d".to_string(),
FieldType::Float => "f".to_string(),
FieldType::Boolean => "b".to_string(),
FieldType::Letters => "l".to_string(),
FieldType::Word => "w".to_string(),
FieldType::NonLetters => "W".to_string(),
FieldType::NonWhitespace => "S".to_string(),
FieldType::NonDigits => "D".to_string(),
FieldType::Custom(name) => name.clone(),
FieldType::Nested => "s".to_string(),
FieldType::Multiline | FieldType::IndentBlock => "s".to_string(),
FieldType::BracedContent => "s".to_string(),
FieldType::NumberWithThousands => "n".to_string(),
FieldType::Scientific => "e".to_string(),
FieldType::GeneralNumber => "g".to_string(),
FieldType::Percentage => "%".to_string(),
FieldType::DateTimeISO => "ti".to_string(),
FieldType::DateTimeRFC2822 => "te".to_string(),
FieldType::DateTimeGlobal => "tg".to_string(),
FieldType::DateTimeUS => "ta".to_string(),
FieldType::DateTimeCtime => "tc".to_string(),
FieldType::DateTimeHTTP => "th".to_string(),
FieldType::DateTimeTime => "tt".to_string(),
FieldType::DateTimeSystem => "ts".to_string(),
FieldType::DateTimeStrftime => "%".to_string(),
}
}
#[pymethods]
impl FormatParser {
#[new]
#[pyo3(signature = (pattern=None, extra_types=None))]
fn new_py(
pattern: Option<&str>,
extra_types: Option<HashMap<String, Py<PyAny>>>,
) -> PyResult<Self> {
match pattern {
Some(p) => Self::new_with_extra_types(p, extra_types),
None => {
let empty_regex =
Regex::new("^$").map_err(|e| crate::error::regex_error(&e.to_string()))?;
Ok(Self {
pattern: String::new(),
regex: empty_regex.clone(),
regex_str: String::new(),
regex_case_insensitive: None,
search_regex: empty_regex.clone(),
search_regex_case_insensitive: None,
fields: CompiledFields {
field_specs: Vec::new(),
field_names: Vec::new(),
normalized_names: Vec::new(),
custom_type_groups: Vec::new(),
has_nested_dict_fields: Vec::new(),
nested_parsers: Vec::new(),
field_count: 0,
},
name_mapping: HashMap::new(),
stored_extra_types: None,
allows_empty_default_string_match: false,
})
}
}
}
#[pyo3(signature = (string, case_sensitive=false, extra_types=None, evaluate_result=true))]
fn parse(
&self,
string: &str,
case_sensitive: bool,
extra_types: Option<HashMap<String, Py<PyAny>>>,
evaluate_result: bool,
) -> PyResult<Option<Py<PyAny>>> {
validate_input_length(string).map_err(PyValueError::new_err)?;
if string.contains('\0') {
return Err(PyValueError::new_err("Input string contains null byte"));
}
let merged_extra_types = Python::attach(|py| {
merge_call_extra_types(py, &self.stored_extra_types, extra_types)
})?;
let parser = Python::attach(|py| -> PyResult<std::sync::Arc<FormatParser>> {
let cache_et = merged_extra_types.as_ref().map(|m| {
m.iter()
.map(|(k, v)| (k.clone(), v.clone_ref(py)))
.collect()
});
get_or_create_parser(&self.pattern, cache_et)
})?;
parser.parse_internal(
string,
case_sensitive,
merged_extra_types.as_ref(),
evaluate_result,
)
}
#[getter]
fn named_fields(&self) -> Vec<String> {
self.fields
.normalized_names
.iter()
.filter_map(|n| n.clone())
.collect()
}
#[getter]
fn field_constraints(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let list = PyList::empty(py);
for (i, spec) in self.fields.field_specs.iter().enumerate() {
let dict = PyDict::new(py);
match self.fields.field_names.get(i).and_then(|n| n.as_deref()) {
Some(name) => dict.set_item("name", name)?,
None => dict.set_item("name", py.None())?,
}
dict.set_item("type", field_type_validation_tag(spec))?;
match spec.width {
Some(w) => dict.set_item("width", w as i64)?,
None => dict.set_item("width", py.None())?,
}
match spec.precision {
Some(p) => dict.set_item("precision", p as i64)?,
None => dict.set_item("precision", py.None())?,
}
list.append(dict)?;
}
list.into_py_any(py)
}
#[getter]
fn regex_subpattern(&self) -> String {
self.regex_str.clone()
}
#[getter]
fn regex_capturing_group_count(&self) -> usize {
count_capturing_groups(&self.regex_str)
}
#[getter]
fn _expression(&self) -> String {
let mut result = self.regex_str.clone();
result = result.replace(r")\s+(", ") (");
result = result.replace(r")\s*(", ") (");
result = result.replace(
r"([+-]?(?:\d+\.\d+|\.\d+|\d+\.)(?:[eE][+-]?\d+)?)",
r"([-+ ]?\d*\.\d+)",
);
if result.starts_with("(") && result.ends_with(")") {
let inner = &result[1..result.len() - 1];
if inner.starts_with(" *(") && inner.ends_with(")") {
result = inner.to_string();
}
}
result
}
#[getter]
fn format(&self) -> Format {
Format {
pattern: self.pattern.clone(),
}
}
#[pyo3(signature = (string, case_sensitive=true, extra_types=None, evaluate_result=true))]
fn search(
&self,
string: &str,
case_sensitive: bool,
extra_types: Option<HashMap<String, Py<PyAny>>>,
evaluate_result: bool,
) -> PyResult<Option<Py<PyAny>>> {
validate_input_length(string).map_err(PyValueError::new_err)?;
if string.contains('\0') {
return Err(PyValueError::new_err("Input string contains null byte"));
}
let merged_extra_types = Python::attach(|py| {
merge_call_extra_types(py, &self.stored_extra_types, extra_types)
})?;
let parser = Python::attach(|py| -> PyResult<std::sync::Arc<FormatParser>> {
let cache_et = merged_extra_types.as_ref().map(|m| {
m.iter()
.map(|(k, v)| (k.clone(), v.clone_ref(py)))
.collect()
});
get_or_create_parser(&self.pattern, cache_et)
})?;
parser.search_pattern(string, case_sensitive, merged_extra_types, evaluate_result)
}
#[pyo3(signature = (string, case_sensitive=false, extra_types=None, evaluate_result=true, max_matches=None))]
fn findall_iter(
&self,
py: Python<'_>,
string: &str,
case_sensitive: bool,
extra_types: Option<HashMap<String, Py<PyAny>>>,
evaluate_result: bool,
max_matches: Option<usize>,
) -> PyResult<Py<FindallIter>> {
validate_input_length(string).map_err(PyValueError::new_err)?;
if string.contains('\0') {
return Err(PyValueError::new_err("Input string contains null byte"));
}
let merged_extra_types = Python::attach(|py| {
merge_call_extra_types(py, &self.stored_extra_types, extra_types)
})?;
let merged_map = merged_extra_types.unwrap_or_default();
let parser = Python::attach(|py| -> PyResult<std::sync::Arc<FormatParser>> {
let cache_et = if merged_map.is_empty() {
None
} else {
Some(
merged_map
.iter()
.map(|(k, v)| (k.clone(), v.clone_ref(py)))
.collect(),
)
};
get_or_create_parser(&self.pattern, cache_et)
})?;
Py::new(
py,
FindallIter::new(
parser,
string.to_string(),
case_sensitive,
evaluate_result,
merged_map,
max_matches,
),
)
}
fn __reduce__(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let m = py.import("_formatparse")?;
let compile_fn = m.getattr("compile")?;
let args = PyTuple::new(py, [&self.pattern])?;
PyTuple::new(py, [compile_fn.as_any(), args.as_any()])?.into_py_any(py)
}
fn __getstate__(&self, py: Python) -> PyResult<Py<PyAny>> {
use pyo3::types::PyDict;
let state = PyDict::new(py);
state.set_item("pattern", &self.pattern)?;
state.into_py_any(py)
}
fn __setstate__(&mut self, _py: Python, state: &Bound<'_, PyAny>) -> PyResult<()> {
use pyo3::types::PyDict;
let dict = state.cast::<PyDict>()?;
let pattern: String = dict
.get_item("pattern")?
.ok_or_else(|| error::missing_field_error("pattern"))?
.extract()?;
let reconstructed = Self::new_with_extra_types(&pattern, None)?;
self.pattern = reconstructed.pattern;
self.regex_str = reconstructed.regex_str;
self.regex = reconstructed.regex;
self.regex_case_insensitive = reconstructed.regex_case_insensitive;
self.search_regex = reconstructed.search_regex;
self.search_regex_case_insensitive = reconstructed.search_regex_case_insensitive;
self.fields = reconstructed.fields;
self.name_mapping = reconstructed.name_mapping;
self.stored_extra_types = reconstructed.stored_extra_types;
self.allows_empty_default_string_match = reconstructed.allows_empty_default_string_match;
Ok(())
}
}
#[pyclass]
pub struct Format {
pattern: String,
}
#[pymethods]
impl Format {
fn format(&self, py: Python, args: &Bound<'_, PyAny>) -> PyResult<String> {
let pattern_obj = PyString::new(py, &self.pattern);
let format_method = pattern_obj.getattr("format")?;
let result = if let Ok(tuple) = args.cast::<PyTuple>() {
format_method.call1(tuple)?
} else {
format_method.call1((args,))?
};
result.extract()
}
}