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