Skip to main content

_formatparse/
result.rs

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