diff --git a/crates/jiter-python/jiter.pyi b/crates/jiter-python/jiter.pyi index 1fe3d9bd..74e79b83 100644 --- a/crates/jiter-python/jiter.pyi +++ b/crates/jiter-python/jiter.pyi @@ -10,6 +10,7 @@ def from_json( partial_mode: Literal[True, False, "off", "on", "trailing-strings"] = False, catch_duplicate_keys: bool = False, lossless_floats: bool = False, + error_in_path: bool = False, ) -> Any: """ Parse input bytes into a JSON object. @@ -28,6 +29,7 @@ def from_json( - 'trailing-strings' - allow incomplete JSON, and include the last incomplete string in the output catch_duplicate_keys: if True, raise an exception if objects contain the same key multiple times lossless_floats: if True, preserve full detail on floats using `LosslessFloat` + error_in_path: Whether to include the JSON path to the invalid JSON in `JsonParseError` Returns: Python object built from the JSON input. @@ -63,8 +65,38 @@ class LosslessFloat: def __bytes__(self) -> bytes: """Return the JSON bytes slice as bytes""" - def __str__(self): + def __str__(self) -> str: """Return the JSON bytes slice as a string""" - def __repr__(self): + def __repr__(self) -> str: + ... + + +class JsonParseError(ValueError): + """ + Represents details of failed JSON parsing. + """ + + def kind(self) -> str: + ... + + def description(self) -> str: + ... + + def path(self) -> list[str | int]: + ... + + def index(self) -> int: + ... + + def line(self) -> int: + ... + + def column(self) -> int: + ... + + def __str__(self) -> str: + """String summary of the error, combined description and position""" + + def __repr__(self) -> str: ... diff --git a/crates/jiter-python/src/lib.rs b/crates/jiter-python/src/lib.rs index 7cd08ed6..6840e8b6 100644 --- a/crates/jiter-python/src/lib.rs +++ b/crates/jiter-python/src/lib.rs @@ -2,9 +2,10 @@ use std::sync::OnceLock; use pyo3::prelude::*; -use jiter::{map_json_error, LosslessFloat, PartialMode, PythonParse, StringCacheMode}; +use jiter::{JsonParseError, LosslessFloat, PartialMode, PythonParse, StringCacheMode}; #[allow(clippy::fn_params_excessive_bools)] +#[allow(clippy::too_many_arguments)] #[pyfunction( signature = ( json_data, @@ -15,6 +16,7 @@ use jiter::{map_json_error, LosslessFloat, PartialMode, PythonParse, StringCache partial_mode=PartialMode::Off, catch_duplicate_keys=false, lossless_floats=false, + error_in_path=false, ) )] pub fn from_json<'py>( @@ -25,6 +27,7 @@ pub fn from_json<'py>( partial_mode: PartialMode, catch_duplicate_keys: bool, lossless_floats: bool, + error_in_path: bool, ) -> PyResult> { let parse_builder = PythonParse { allow_inf_nan, @@ -32,10 +35,9 @@ pub fn from_json<'py>( partial_mode, catch_duplicate_keys, lossless_floats, + error_in_path, }; - parse_builder - .python_parse(py, json_data) - .map_err(|e| map_json_error(json_data, &e)) + parse_builder.python_parse_exc(py, json_data) } pub fn get_jiter_version() -> &'static str { @@ -70,5 +72,6 @@ fn jiter_python(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(cache_clear, m)?)?; m.add_function(wrap_pyfunction!(cache_usage, m)?)?; m.add_class::()?; + m.add_class::()?; Ok(()) } diff --git a/crates/jiter-python/tests/test_jiter.py b/crates/jiter-python/tests/test_jiter.py index 0612ba84..55b27f48 100644 --- a/crates/jiter-python/tests/test_jiter.py +++ b/crates/jiter-python/tests/test_jiter.py @@ -7,14 +7,14 @@ from dirty_equals import IsFloatNan -def test_python_parse_numeric(): +def test_parse_numeric(): parsed = jiter.from_json( b' { "int": 1, "bigint": 123456789012345678901234567890, "float": 1.2} ' ) assert parsed == {"int": 1, "bigint": 123456789012345678901234567890, "float": 1.2} -def test_python_parse_other_cached(): +def test_parse_other_cached(): parsed = jiter.from_json( b'["string", true, false, null, NaN, Infinity, -Infinity]', allow_inf_nan=True, @@ -23,7 +23,7 @@ def test_python_parse_other_cached(): assert parsed == ["string", True, False, None, IsFloatNan(), inf, -inf] -def test_python_parse_other_no_cache(): +def test_parse_other_no_cache(): parsed = jiter.from_json( b'["string", true, false, null]', cache_mode=False, @@ -31,19 +31,56 @@ def test_python_parse_other_no_cache(): assert parsed == ["string", True, False, None] -def test_python_disallow_nan(): - with pytest.raises(ValueError, match="expected value at line 1 column 2"): +def test_disallow_nan(): + with pytest.raises(jiter.JsonParseError, match="expected value at line 1 column 2"): jiter.from_json(b"[NaN]", allow_inf_nan=False) def test_error(): - with pytest.raises(ValueError, match="EOF while parsing a list at line 1 column 9"): + with pytest.raises(jiter.JsonParseError, match="EOF while parsing a list at line 1 column 9") as exc_info: jiter.from_json(b'["string"') + assert exc_info.value.kind() == 'EofWhileParsingList' + assert exc_info.value.description() == 'EOF while parsing a list' + assert exc_info.value.path() == [] + assert exc_info.value.index() == 9 + assert exc_info.value.line() == 1 + assert exc_info.value.column() == 9 + assert repr(exc_info.value) == 'JsonParseError("EOF while parsing a list at line 1 column 9")' + + +def test_error_path(): + with pytest.raises(jiter.JsonParseError, match="EOF while parsing a string at line 1 column 5") as exc_info: + jiter.from_json(b'["str', error_in_path=True) + + assert exc_info.value.kind() == 'EofWhileParsingString' + assert exc_info.value.description() == 'EOF while parsing a string' + assert exc_info.value.path() == [0] + assert exc_info.value.index() == 5 + assert exc_info.value.line() == 1 + + +def test_error_path_empty(): + with pytest.raises(jiter.JsonParseError) as exc_info: + jiter.from_json(b'"foo', error_in_path=True) + + assert exc_info.value.kind() == 'EofWhileParsingString' + assert exc_info.value.path() == [] + + +def test_error_path_object(): + with pytest.raises(jiter.JsonParseError) as exc_info: + jiter.from_json(b'{"foo":\n[1,\n2, x', error_in_path=True) + + assert exc_info.value.kind() == 'ExpectedSomeValue' + assert exc_info.value.index() == 15 + assert exc_info.value.line() == 3 + assert exc_info.value.path() == ['foo', 2] + def test_recursion_limit(): with pytest.raises( - ValueError, match="recursion limit exceeded at line 1 column 202" + jiter.JsonParseError, match="recursion limit exceeded at line 1 column 202" ): jiter.from_json(b"[" * 10_000) @@ -150,21 +187,21 @@ def test_partial_nested(): assert isinstance(parsed, dict) -def test_python_cache_usage_all(): +def test_cache_usage_all(): jiter.cache_clear() parsed = jiter.from_json(b'{"foo": "bar", "spam": 3}', cache_mode="all") assert parsed == {"foo": "bar", "spam": 3} assert jiter.cache_usage() == 3 -def test_python_cache_usage_keys(): +def test_cache_usage_keys(): jiter.cache_clear() parsed = jiter.from_json(b'{"foo": "bar", "spam": 3}', cache_mode="keys") assert parsed == {"foo": "bar", "spam": 3} assert jiter.cache_usage() == 2 -def test_python_cache_usage_none(): +def test_cache_usage_none(): jiter.cache_clear() parsed = jiter.from_json( b'{"foo": "bar", "spam": 3}', diff --git a/crates/jiter/src/errors.rs b/crates/jiter/src/errors.rs index 2068ec98..57dd4263 100644 --- a/crates/jiter/src/errors.rs +++ b/crates/jiter/src/errors.rs @@ -107,6 +107,35 @@ impl std::fmt::Display for JsonErrorType { } } +impl JsonErrorType { + pub fn kind(&self) -> &'static str { + match self { + Self::FloatExpectingInt => "FloatExpectingInt", + Self::DuplicateKey(_) => "DuplicateKey", + Self::EofWhileParsingList => "EofWhileParsingList", + Self::EofWhileParsingObject => "EofWhileParsingObject", + Self::EofWhileParsingString => "EofWhileParsingString", + Self::EofWhileParsingValue => "EofWhileParsingValue", + Self::ExpectedColon => "ExpectedColon", + Self::ExpectedListCommaOrEnd => "ExpectedListCommaOrEnd", + Self::ExpectedObjectCommaOrEnd => "ExpectedObjectCommaOrEnd", + Self::ExpectedSomeIdent => "ExpectedSomeIdent", + Self::ExpectedSomeValue => "ExpectedSomeValue", + Self::InvalidEscape => "InvalidEscape", + Self::InvalidNumber => "InvalidNumber", + Self::NumberOutOfRange => "NumberOutOfRange", + Self::InvalidUnicodeCodePoint => "InvalidUnicodeCodePoint", + Self::ControlCharacterWhileParsingString => "ControlCharacterWhileParsingString", + Self::KeyMustBeAString => "KeyMustBeAString", + Self::LoneLeadingSurrogateInHexEscape => "LoneLeadingSurrogateInHexEscape", + Self::TrailingComma => "TrailingComma", + Self::TrailingCharacters => "TrailingCharacters", + Self::UnexpectedEndOfHexEscape => "UnexpectedEndOfHexEscape", + Self::RecursionLimitExceeded => "RecursionLimitExceeded", + } + } +} + pub type JsonResult = Result; /// Represents an error from parsing JSON diff --git a/crates/jiter/src/lib.rs b/crates/jiter/src/lib.rs index b9eea501..57834633 100644 --- a/crates/jiter/src/lib.rs +++ b/crates/jiter/src/lib.rs @@ -6,6 +6,8 @@ mod lazy_index_map; mod number_decoder; mod parse; #[cfg(feature = "python")] +mod py_error; +#[cfg(feature = "python")] mod py_lossless_float; #[cfg(feature = "python")] mod py_string_cache; @@ -23,9 +25,11 @@ pub use number_decoder::{NumberAny, NumberInt}; pub use parse::Peek; pub use value::{JsonArray, JsonObject, JsonValue}; +#[cfg(feature = "python")] +pub use py_error::JsonParseError; #[cfg(feature = "python")] pub use py_lossless_float::LosslessFloat; #[cfg(feature = "python")] pub use py_string_cache::{cache_clear, cache_usage, cached_py_string, pystring_fast_new, StringCacheMode}; #[cfg(feature = "python")] -pub use python::{map_json_error, PartialMode, PythonParse}; +pub use python::{PartialMode, PythonParse}; diff --git a/crates/jiter/src/py_error.rs b/crates/jiter/src/py_error.rs new file mode 100644 index 00000000..19c3384e --- /dev/null +++ b/crates/jiter/src/py_error.rs @@ -0,0 +1,210 @@ +use pyo3::exceptions::PyValueError; +use pyo3::prelude::*; + +use crate::errors::{JsonError, LinePosition}; + +#[pyclass(extends=PyValueError, module="jiter")] +#[derive(Debug, Clone)] +pub struct JsonParseError { + json_error: JsonError, + path: Vec, + position: LinePosition, +} + +impl JsonParseError { + pub(crate) fn new_err(py: Python, py_json_error: PythonJsonError, json_data: &[u8]) -> PyErr { + let position = py_json_error.error.get_position(json_data); + let slf = Self { + json_error: py_json_error.error, + path: match py_json_error.path { + Some(mut v) => { + v.reverse(); + v + } + None => vec![], + }, + position, + }; + match Py::new(py, slf) { + Ok(err) => PyErr::from_value_bound(err.into_bound(py).into_any()), + Err(err) => err, + } + } +} + +#[pymethods] +impl JsonParseError { + fn kind(&self) -> &'static str { + self.json_error.error_type.kind() + } + + fn description(&self) -> String { + self.json_error.error_type.to_string() + } + + fn path(&self, py: Python) -> PyObject { + self.path.to_object(py) + } + + fn index(&self) -> usize { + self.json_error.index + } + + fn line(&self) -> usize { + self.position.line + } + + fn column(&self) -> usize { + self.position.column + } + + fn __str__(&self) -> String { + format!("{} at {}", self.json_error.error_type, self.position) + } + + fn __repr__(&self) -> String { + format!("JsonParseError({:?})", self.__str__()) + } +} + +pub(crate) trait MaybeBuildArrayPath: MaybeBuildPath { + fn incr_index(&mut self); + fn set_index_path(&self, err: PythonJsonError) -> PythonJsonError; +} + +pub(crate) trait MaybeBuildObjectPath: MaybeBuildPath { + fn set_key(&mut self, key: &str); + + fn set_key_path(&self, err: PythonJsonError) -> PythonJsonError; +} + +pub(crate) trait MaybeBuildPath { + type A: MaybeBuildArrayPath; + type O: MaybeBuildObjectPath; + fn new_array() -> Self::A; + fn new_object() -> Self::O; +} + +pub(crate) struct NoopBuildPath; + +impl MaybeBuildPath for NoopBuildPath { + type A = NoopBuildPath; + type O = NoopBuildPath; + fn new_array() -> Self::A { + NoopBuildPath + } + + fn new_object() -> Self::O { + NoopBuildPath + } +} + +impl MaybeBuildArrayPath for NoopBuildPath { + fn incr_index(&mut self) {} + + fn set_index_path(&self, err: PythonJsonError) -> PythonJsonError { + err + } +} + +impl MaybeBuildObjectPath for NoopBuildPath { + fn set_key(&mut self, _: &str) {} + + fn set_key_path(&self, err: PythonJsonError) -> PythonJsonError { + err + } +} + +#[derive(Default)] +pub(crate) struct ActiveBuildPath { + index: usize, +} + +impl MaybeBuildPath for ActiveBuildPath { + type A = ActiveBuildPath; + type O = ActiveObjectBuildPath; + fn new_array() -> Self::A { + ActiveBuildPath::default() + } + + fn new_object() -> Self::O { + ActiveObjectBuildPath::default() + } +} + +impl MaybeBuildArrayPath for ActiveBuildPath { + fn incr_index(&mut self) { + self.index += 1; + } + + fn set_index_path(&self, mut err: PythonJsonError) -> PythonJsonError { + err.add(PathItem::Index(self.index)); + err + } +} + +#[derive(Default)] +pub(crate) struct ActiveObjectBuildPath { + key: String, +} + +impl MaybeBuildPath for ActiveObjectBuildPath { + type A = ActiveBuildPath; + type O = ActiveObjectBuildPath; + fn new_array() -> Self::A { + ActiveBuildPath::default() + } + + fn new_object() -> Self::O { + ActiveObjectBuildPath::default() + } +} + +impl MaybeBuildObjectPath for ActiveObjectBuildPath { + fn set_key(&mut self, key: &str) { + self.key = key.to_string(); + } + + fn set_key_path(&self, mut err: PythonJsonError) -> PythonJsonError { + err.add(PathItem::Key(self.key.clone())); + err + } +} + +#[derive(Debug, Clone)] +enum PathItem { + Index(usize), + Key(String), +} + +impl ToPyObject for PathItem { + fn to_object(&self, py: Python<'_>) -> PyObject { + match self { + Self::Index(index) => index.to_object(py), + Self::Key(str) => str.to_object(py), + } + } +} + +#[derive(Debug)] +pub struct PythonJsonError { + pub error: JsonError, + path: Option>, +} + +pub(crate) type PythonJsonResult = Result; + +impl From for PythonJsonError { + fn from(error: JsonError) -> Self { + Self { error, path: None } + } +} + +impl PythonJsonError { + fn add(&mut self, path_item: PathItem) { + match self.path.as_mut() { + Some(path) => path.push(path_item), + None => self.path = Some(vec![path_item]), + } + } +} diff --git a/crates/jiter/src/python.rs b/crates/jiter/src/python.rs index 0bb49d1d..3dfd4b6f 100644 --- a/crates/jiter/src/python.rs +++ b/crates/jiter/src/python.rs @@ -9,9 +9,13 @@ use pyo3::ToPyObject; use smallvec::SmallVec; -use crate::errors::{json_err, json_error, JsonError, JsonResult, DEFAULT_RECURSION_LIMIT}; +use crate::errors::{json_error, JsonError, JsonResult, DEFAULT_RECURSION_LIMIT}; use crate::number_decoder::{AbstractNumberDecoder, NumberAny, NumberRange}; use crate::parse::{Parser, Peek}; +use crate::py_error::{ + ActiveBuildPath, JsonParseError, MaybeBuildArrayPath, MaybeBuildObjectPath, MaybeBuildPath, NoopBuildPath, + PythonJsonResult, +}; use crate::py_string_cache::{StringCacheAll, StringCacheKeys, StringCacheMode, StringMaybeCache, StringNoCache}; use crate::string_decoder::{StringDecoder, Tape}; use crate::{JsonErrorType, LosslessFloat}; @@ -29,6 +33,8 @@ pub struct PythonParse { pub catch_duplicate_keys: bool, /// Whether to preserve full detail on floats using [`LosslessFloat`] pub lossless_floats: bool, + /// Whether to include the JSON path to the invalid JSON in [`JsonParseError`] + pub error_in_path: bool, } impl PythonParse { @@ -43,10 +49,10 @@ impl PythonParse { /// # Returns /// /// A [PyObject](https://docs.rs/pyo3/latest/pyo3/type.PyObject.html) representing the parsed JSON value. - pub fn python_parse<'py>(self, py: Python<'py>, json_data: &[u8]) -> JsonResult> { + pub fn python_parse<'py>(self, py: Python<'py>, json_data: &[u8]) -> PythonJsonResult> { macro_rules! ppp { - ($string_cache:ident, $key_check:ident, $parse_number:ident) => { - PythonParser::<$string_cache, $key_check, $parse_number>::parse( + ($string_cache:ident, $key_check:ident, $parse_number:ident, $build_path:ident) => { + PythonParser::<$string_cache, $key_check, $parse_number, $build_path>::parse( py, json_data, self.allow_inf_nan, @@ -56,11 +62,34 @@ impl PythonParse { } macro_rules! ppp_group { ($string_cache:ident) => { - match (self.catch_duplicate_keys, self.lossless_floats) { - (true, true) => ppp!($string_cache, DuplicateKeyCheck, ParseNumberLossless), - (true, false) => ppp!($string_cache, DuplicateKeyCheck, ParseNumberLossy), - (false, true) => ppp!($string_cache, NoopKeyCheck, ParseNumberLossless), - (false, false) => ppp!($string_cache, NoopKeyCheck, ParseNumberLossy), + match ( + self.catch_duplicate_keys, + self.lossless_floats, + self.error_in_path, + ) { + (true, true, true) => ppp!( + $string_cache, + DuplicateKeyCheck, + ParseNumberLossless, + ActiveBuildPath + ), + (true, true, false) => ppp!( + $string_cache, + DuplicateKeyCheck, + ParseNumberLossless, + NoopBuildPath + ), + (true, false, true) => ppp!( + $string_cache, + DuplicateKeyCheck, + ParseNumberLossy, + ActiveBuildPath + ), + (true, false, false) => ppp!($string_cache, DuplicateKeyCheck, ParseNumberLossy, NoopBuildPath), + (false, true, true) => ppp!($string_cache, NoopKeyCheck, ParseNumberLossless, ActiveBuildPath), + (false, true, false) => ppp!($string_cache, NoopKeyCheck, ParseNumberLossless, NoopBuildPath), + (false, false, true) => ppp!($string_cache, NoopKeyCheck, ParseNumberLossy, ActiveBuildPath), + (false, false, false) => ppp!($string_cache, NoopKeyCheck, ParseNumberLossy, NoopBuildPath), } }; } @@ -71,17 +100,20 @@ impl PythonParse { StringCacheMode::None => ppp_group!(StringNoCache), } } -} -/// Map a `JsonError` to a `PyErr` which can be raised as an exception in Python as a `ValueError`. -pub fn map_json_error(json_data: &[u8], json_error: &JsonError) -> PyErr { - PyValueError::new_err(json_error.description(json_data)) + /// Like `python_parse`, but maps [`PythonJsonResult`] to a `PyErr` which can be raised as an exception in + /// Python as a [`JsonParseError`]. + pub fn python_parse_exc<'py>(self, py: Python<'py>, json_data: &[u8]) -> PyResult> { + self.python_parse(py, json_data) + .map_err(|e| JsonParseError::new_err(py, e, json_data)) + } } -struct PythonParser<'j, StringCache, KeyCheck, ParseNumber> { +struct PythonParser<'j, StringCache, KeyCheck, ParseNumber, BuildPath> { _string_cache: PhantomData, _key_check: PhantomData, _parse_number: PhantomData, + _build_path: PhantomData, parser: Parser<'j>, tape: Tape, recursion_limit: u8, @@ -89,19 +121,25 @@ struct PythonParser<'j, StringCache, KeyCheck, ParseNumber> { partial_mode: PartialMode, } -impl<'j, StringCache: StringMaybeCache, KeyCheck: MaybeKeyCheck, ParseNumber: MaybeParseNumber> - PythonParser<'j, StringCache, KeyCheck, ParseNumber> +impl< + 'j, + StringCache: StringMaybeCache, + KeyCheck: MaybeKeyCheck, + ParseNumber: MaybeParseNumber, + BuildPath: MaybeBuildPath, + > PythonParser<'j, StringCache, KeyCheck, ParseNumber, BuildPath> { fn parse<'py>( py: Python<'py>, json_data: &[u8], allow_inf_nan: bool, partial_mode: PartialMode, - ) -> JsonResult> { + ) -> PythonJsonResult> { let mut slf = PythonParser { _string_cache: PhantomData::, _key_check: PhantomData::, _parse_number: PhantomData::, + _build_path: PhantomData::, parser: Parser::new(json_data), tape: Tape::default(), recursion_limit: DEFAULT_RECURSION_LIMIT, @@ -117,7 +155,7 @@ impl<'j, StringCache: StringMaybeCache, KeyCheck: MaybeKeyCheck, ParseNumber: Ma Ok(v) } - fn py_take_value<'py>(&mut self, py: Python<'py>, peek: Peek) -> JsonResult> { + fn py_take_value<'py>(&mut self, py: Python<'py>, peek: Peek) -> PythonJsonResult> { match peek { Peek::Null => { self.parser.consume_null()?; @@ -138,15 +176,16 @@ impl<'j, StringCache: StringMaybeCache, KeyCheck: MaybeKeyCheck, ParseNumber: Ma Ok(StringCache::get_value(py, s.as_str(), s.ascii_only()).into_any()) } Peek::Array => { + let build_path = BuildPath::new_array(); let peek_first = match self.parser.array_first() { Ok(Some(peek)) => peek, - Err(e) if !self._allow_partial_err(&e) => return Err(e), + Err(e) if !self._allow_partial_err(&e) => return Err(e.into()), Ok(None) | Err(_) => return Ok(PyList::empty_bound(py).into_any()), }; let mut vec: SmallVec<[Bound<'_, PyAny>; 8]> = SmallVec::with_capacity(8); - if let Err(e) = self._parse_array(py, peek_first, &mut vec) { - if !self._allow_partial_err(&e) { + if let Err(e) = self._parse_array(py, peek_first, &mut vec, build_path) { + if !self._allow_partial_err(&e.error) { return Err(e); } } @@ -156,13 +195,13 @@ impl<'j, StringCache: StringMaybeCache, KeyCheck: MaybeKeyCheck, ParseNumber: Ma Peek::Object => { let dict = PyDict::new_bound(py); if let Err(e) = self._parse_object(py, &dict) { - if !self._allow_partial_err(&e) { + if !self._allow_partial_err(&e.error) { return Err(e); } } Ok(dict.into_any()) } - _ => ParseNumber::parse_number(py, &mut self.parser, peek, self.allow_inf_nan), + _ => ParseNumber::parse_number(py, &mut self.parser, peek, self.allow_inf_nan).map_err(Into::into), } } @@ -171,17 +210,27 @@ impl<'j, StringCache: StringMaybeCache, KeyCheck: MaybeKeyCheck, ParseNumber: Ma py: Python<'py>, peek_first: Peek, vec: &mut SmallVec<[Bound<'py, PyAny>; 8]>, - ) -> JsonResult<()> { - let v = self._check_take_value(py, peek_first)?; + mut build_path: impl MaybeBuildArrayPath, + ) -> PythonJsonResult<()> { + let v = self + ._check_take_value(py, peek_first) + .map_err(|e| build_path.set_index_path(e))?; vec.push(v); - while let Some(peek) = self.parser.array_step()? { - let v = self._check_take_value(py, peek)?; + while let Some(peek) = self + .parser + .array_step() + .map_err(|e| build_path.set_index_path(e.into()))? + { + build_path.incr_index(); + let v = self + ._check_take_value(py, peek) + .map_err(|e| build_path.set_index_path(e))?; vec.push(v); } Ok(()) } - fn _parse_object<'py>(&mut self, py: Python<'py>, dict: &Bound<'py, PyDict>) -> JsonResult<()> { + fn _parse_object<'py>(&mut self, py: Python<'py>, dict: &Bound<'py, PyDict>) -> PythonJsonResult<()> { let set_item = |key: Bound<'py, PyString>, value: Bound<'py, PyAny>| { let r = unsafe { ffi::PyDict_SetItem(dict.as_ptr(), key.as_ptr(), value.as_ptr()) }; // AFAIK this shouldn't happen since the key will always be a string which is hashable @@ -189,20 +238,27 @@ impl<'j, StringCache: StringMaybeCache, KeyCheck: MaybeKeyCheck, ParseNumber: Ma // presumably because there are fewer branches assert_ne!(r, -1, "PyDict_SetItem failed"); }; + let mut build_path = BuildPath::new_object(); let mut check_keys = KeyCheck::default(); if let Some(first_key) = self.parser.object_first::(&mut self.tape)? { let first_key_s = first_key.as_str(); check_keys.check(first_key_s, self.parser.index)?; + build_path.set_key(first_key_s); let first_key = StringCache::get_key(py, first_key_s, first_key.ascii_only()); - let peek = self.parser.peek()?; - let first_value = self._check_take_value(py, peek)?; + let peek = self.parser.peek().map_err(|e| build_path.set_key_path(e.into()))?; + let first_value = self + ._check_take_value(py, peek) + .map_err(|e| build_path.set_key_path(e))?; set_item(first_key, first_value); while let Some(key) = self.parser.object_step::(&mut self.tape)? { let key_s = key.as_str(); + build_path.set_key(key_s); check_keys.check(key_s, self.parser.index)?; let key = StringCache::get_key(py, key_s, key.ascii_only()); - let peek = self.parser.peek()?; - let value = self._check_take_value(py, peek)?; + let peek = self.parser.peek().map_err(|e| build_path.set_key_path(e.into()))?; + let value = self + ._check_take_value(py, peek) + .map_err(|e| build_path.set_key_path(e))?; set_item(key, value); } } @@ -225,10 +281,10 @@ impl<'j, StringCache: StringMaybeCache, KeyCheck: MaybeKeyCheck, ParseNumber: Ma } } - fn _check_take_value<'py>(&mut self, py: Python<'py>, peek: Peek) -> JsonResult> { + fn _check_take_value<'py>(&mut self, py: Python<'py>, peek: Peek) -> PythonJsonResult> { self.recursion_limit = match self.recursion_limit.checked_sub(1) { Some(limit) => limit, - None => return json_err!(RecursionLimitExceeded, self.parser.index), + None => return Err(json_error!(RecursionLimitExceeded, self.parser.index).into()), }; let r = self.py_take_value(py, peek);