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)>, }
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
31fn 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 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 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 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 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 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}