Skip to main content

_formatparse/
result.rs

1use pyo3::prelude::*;
2use pyo3::types::{PySlice, PyString, PyTuple};
3use pyo3::IntoPyObjectExt;
4use std::collections::HashMap;
5
6#[pyclass]
7pub struct ParseResult {
8    fixed: Vec<PyObject>,
9    #[pyo3(get)]
10    pub named: HashMap<String, PyObject>,
11    pub span: (usize, usize),
12    pub field_spans: HashMap<String, (usize, usize)>, // Maps field index/name to (start, end)
13}
14
15impl Clone for ParseResult {
16    fn clone(&self) -> Self {
17        Python::with_gil(|py| Self {
18            fixed: self.fixed.iter().map(|obj| obj.clone_ref(py)).collect(),
19            named: self
20                .named
21                .iter()
22                .map(|(k, v)| (k.clone(), v.clone_ref(py)))
23                .collect(),
24            span: self.span,
25            field_spans: self.field_spans.clone(),
26        })
27    }
28}
29
30/// Truncate by Unicode scalar values so we never split inside a codepoint.
31fn repr_trunc(s: &str, max_chars: usize) -> String {
32    if max_chars < 3 {
33        return "...".to_string();
34    }
35    let n = s.chars().count();
36    if n <= max_chars {
37        return s.to_string();
38    }
39    let take = max_chars.saturating_sub(3);
40    s.chars().take(take).collect::<String>() + "..."
41}
42
43impl ParseResult {
44    pub fn new(
45        fixed: Vec<PyObject>,
46        named: HashMap<String, PyObject>,
47        span: (usize, usize),
48    ) -> Self {
49        Self {
50            fixed,
51            named,
52            span,
53            field_spans: HashMap::new(),
54        }
55    }
56
57    pub fn new_with_spans(
58        fixed: Vec<PyObject>,
59        named: HashMap<String, PyObject>,
60        span: (usize, usize),
61        field_spans: HashMap<String, (usize, usize)>,
62    ) -> Self {
63        Self {
64            fixed,
65            named,
66            span,
67            field_spans,
68        }
69    }
70
71    pub fn with_offset(mut self, offset: usize) -> Self {
72        self.span = (self.span.0 + offset, self.span.1 + offset);
73        // Adjust all field spans by offset
74        self.field_spans = self
75            .field_spans
76            .into_iter()
77            .map(|(k, (start, end))| (k, (start + offset, end + offset)))
78            .collect();
79        self
80    }
81
82    /// Rich but bounded string for `__repr__` / `__str__` (sorted `named` keys for stability).
83    fn format_display(&self, py: Python<'_>) -> PyResult<String> {
84        const MAX_KEYS: usize = 12;
85        const MAX_VAL_CHARS: usize = 120;
86        const MAX_FIXED: usize = 8;
87
88        let mut keys: Vec<_> = self.named.keys().cloned().collect();
89        keys.sort();
90
91        let mut named_parts = Vec::new();
92        for k in keys.iter().take(MAX_KEYS) {
93            let Some(v) = self.named.get(k) else {
94                continue;
95            };
96            // Use Python `repr(key)` so dict-style output matches CPython (single-quoted keys),
97            // not Rust `Debug` / `{:?}` which uses double quotes.
98            let key_repr: String = PyString::new(py, k.as_str()).repr()?.extract()?;
99            let r: String = v.bind(py).repr()?.extract()?;
100            named_parts.push(format!("{}: {}", key_repr, repr_trunc(&r, MAX_VAL_CHARS)));
101        }
102        let mut named_body = named_parts.join(", ");
103        if keys.len() > MAX_KEYS {
104            named_body.push_str(&format!(", ... (+{} more)", keys.len() - MAX_KEYS));
105        }
106        let named_display = format!("{{{}}}", named_body);
107
108        let fixed_display = if self.fixed.is_empty() {
109            "()".to_string()
110        } else {
111            let mut fp = Vec::new();
112            for obj in self.fixed.iter().take(MAX_FIXED) {
113                let r: String = obj.bind(py).repr()?.extract()?;
114                fp.push(repr_trunc(&r, MAX_VAL_CHARS));
115            }
116            if self.fixed.len() > MAX_FIXED {
117                format!(
118                    "({}, ... (+{} more))",
119                    fp.join(", "),
120                    self.fixed.len() - MAX_FIXED
121                )
122            } else {
123                format!("({})", fp.join(", "))
124            }
125        };
126
127        Ok(format!(
128            "<ParseResult span={:?} named={} fixed={}>",
129            self.span, named_display, fixed_display
130        ))
131    }
132}
133
134#[pymethods]
135impl ParseResult {
136    #[new]
137    #[pyo3(signature = (fixed, named, span=None))]
138    fn new_py(
139        fixed: Vec<PyObject>,
140        named: HashMap<String, PyObject>,
141        span: Option<(usize, usize)>,
142    ) -> Self {
143        Self::new(fixed, named, span.unwrap_or((0, 0)))
144    }
145
146    #[getter]
147    fn fixed(&self) -> PyResult<PyObject> {
148        Python::with_gil(|py| {
149            let items: Vec<_> = self.fixed.iter().map(|obj| obj.bind(py)).collect();
150            let tuple = PyTuple::new(py, items)?;
151            Ok(tuple.into())
152        })
153    }
154
155    #[getter]
156    fn span(&self) -> (usize, usize) {
157        self.span
158    }
159
160    #[getter]
161    fn start(&self) -> usize {
162        self.span.0
163    }
164
165    #[getter]
166    fn end(&self) -> usize {
167        self.span.1
168    }
169
170    fn __repr__(&self, py: Python<'_>) -> PyResult<String> {
171        self.format_display(py)
172    }
173
174    fn __str__(&self, py: Python<'_>) -> PyResult<String> {
175        self.format_display(py)
176    }
177
178    fn __getitem__(&self, key: &Bound<'_, PyAny>) -> PyResult<PyObject> {
179        Python::with_gil(|py| {
180            // Try to extract as slice first
181            if let Ok(slice) = key.downcast::<PySlice>() {
182                let len = self.fixed.len() as isize;
183                let indices = slice.indices(len)?;
184
185                let mut result = Vec::new();
186                let mut idx = indices.start;
187                for _ in 0..indices.slicelength {
188                    if idx >= 0 && (idx as usize) < self.fixed.len() {
189                        result.push(self.fixed[idx as usize].bind(py));
190                    }
191                    idx += indices.step;
192                }
193
194                let tuple = PyTuple::new(py, result)?;
195                Ok(tuple.into())
196            } else if let Ok(idx) = key.extract::<usize>() {
197                self.fixed
198                    .get(idx)
199                    .map(|obj| obj.clone_ref(py))
200                    .ok_or_else(|| {
201                        PyErr::new::<pyo3::exceptions::PyIndexError, _>("Index out of range")
202                    })
203            } else if let Ok(name) = key.extract::<String>() {
204                self.named
205                    .get(&name)
206                    .map(|obj| obj.clone_ref(py))
207                    .ok_or_else(|| {
208                        PyErr::new::<pyo3::exceptions::PyKeyError, _>(format!(
209                            "Key '{}' not found",
210                            name
211                        ))
212                    })
213            } else {
214                Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(
215                    "Key must be int, str, or slice",
216                ))
217            }
218        })
219    }
220
221    fn __contains__(&self, key: &Bound<'_, PyAny>) -> PyResult<bool> {
222        Python::with_gil(|_py| {
223            if let Ok(idx) = key.extract::<usize>() {
224                Ok(idx < self.fixed.len())
225            } else if let Ok(name) = key.extract::<String>() {
226                Ok(self.named.contains_key(&name))
227            } else {
228                Ok(false)
229            }
230        })
231    }
232
233    #[getter]
234    fn spans(&self) -> PyResult<PyObject> {
235        Python::with_gil(|py| {
236            let dict = pyo3::types::PyDict::new(py);
237            for (key, value) in &self.field_spans {
238                let py_key: PyObject = if let Ok(idx) = key.parse::<usize>() {
239                    idx.into_py_any(py)?
240                } else {
241                    key.clone().into_py_any(py)?
242                };
243                let py_value = PyTuple::new(py, [value.0, value.1])?;
244                dict.set_item(py_key.bind(py), py_value)?;
245            }
246            dict.into_py_any(py)
247        })
248    }
249}