From 9a6e2a8b2b6c71b40327bc0ef7fa8a9e5bb4ec0f Mon Sep 17 00:00:00 2001 From: micheal000010000-hub Date: Sun, 17 May 2026 19:07:27 +0530 Subject: [PATCH 1/3] feat: add to_json convenience API --- README.md | 7 +++++ src/lib.rs | 48 +++++++++++++++++++++++++++++++ tests/integration/test_to_json.py | 44 ++++++++++++++++++++++++++++ toons.pyi | 24 ++++++++++++++++ 4 files changed, 123 insertions(+) create mode 100644 tests/integration/test_to_json.py diff --git a/README.md b/README.md index 60f8369..ea4b355 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,13 @@ print(toons.dumps(user)) # name: Bob # age: 25 # active: true + +# Convert TOON to JSON +print(toons.to_json("name: Alice\nage: 30")) +# { +# "name": "Alice", +# "age": 30 +# } ``` ### File Operations diff --git a/src/lib.rs b/src/lib.rs index bef30e6..502f2f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,6 +45,7 @@ pyo3::create_exception!( #[pyo3::pymodule] mod toons { use pyo3::prelude::*; + use pyo3::types::PyDict; #[allow(non_upper_case_globals)] #[pymodule_export] @@ -89,6 +90,53 @@ mod toons { crate::deserialization::deserialize(py, &s, strict, expand_mode, indent) } + /// Convert a TOON formatted string to a JSON formatted string. + /// + /// Parse a string containing TOON (Token-Oriented Object Notation) data + /// and return the corresponding JSON string using Python's standard + /// library json module. + /// + /// Args: + /// s: A string containing TOON formatted data + /// strict: If True (default), enforce strict TOON v3.0 compliance. + /// If False, allow some leniency (e.g. blank lines in arrays). + /// expand_paths: Path expansion mode: None, "off", "safe", "always". + /// indent: Number of spaces per JSON indentation level, or None for + /// compact JSON (default: 2) + /// + /// Returns: + /// A string containing JSON decoded from the TOON string + /// + /// Raises: + /// ToonDecodeError: If the input is malformed. See `loads` for details. + /// + /// Example: + /// >>> import toons + /// >>> json_str = toons.to_json("name: Alice\nage: 30") + /// >>> print(json_str) + /// { + /// "name": "Alice", + /// "age": 30 + /// } + #[pyfunction] + #[pyo3(signature = (s, *, strict=true, expand_paths=None, indent=2))] + fn to_json( + py: Python, + s: String, + strict: bool, + expand_paths: Option<&str>, + indent: Option, + ) -> PyResult { + let expand_mode = expand_paths.unwrap_or("off"); + let parsed_obj = crate::deserialization::deserialize(py, &s, strict, expand_mode, None)?; + let json = py.import("json")?; + let kwargs = PyDict::new(py); + kwargs.set_item("indent", indent)?; + json.getattr("dumps")? + .call((parsed_obj,), Some(&kwargs))? + .extract() + } + /// Deserialize a TOON formatted file to a Python object. /// /// Read TOON data from a file-like object and return the corresponding diff --git a/tests/integration/test_to_json.py b/tests/integration/test_to_json.py new file mode 100644 index 0000000..bbc2cc0 --- /dev/null +++ b/tests/integration/test_to_json.py @@ -0,0 +1,44 @@ +import json + +import pytest + +import toons + + +class TestToJson: + """Integration tests for to_json() function.""" + + def test_to_json_uses_default_indent(self): + """to_json() converts TOON to pretty JSON by default.""" + toon_str = "name: Alice\nage: 30\ntags[2]: admin,user" + result = toons.to_json(toon_str) + + assert result == json.dumps( + {"name": "Alice", "age": 30, "tags": ["admin", "user"]}, + indent=2, + ) + + def test_to_json_accepts_indent_none(self): + """to_json() passes indent=None through to json.dumps().""" + toon_str = "name: Alice\nage: 30" + result = toons.to_json(toon_str, indent=None) + + assert result == json.dumps({"name": "Alice", "age": 30}, indent=None) + + def test_to_json_respects_strict_flag(self): + """to_json() forwards strict=False to the TOON parser.""" + toon_str = "[3]:\n - 1\n\n - 2\n - 3" + result = toons.to_json(toon_str, strict=False) + + assert result == json.dumps([1, 2, 3], indent=2) + + def test_to_json_raises_decode_error(self): + """to_json() raises ToonDecodeError for malformed TOON.""" + with pytest.raises(toons.ToonDecodeError): + toons.to_json("a:\n b:\n c: 1\n") + + def test_to_json_respects_expand_paths(self): + """to_json() forwards expand_paths to the TOON parser.""" + result = toons.to_json("user.name: Alice", expand_paths="safe") + + assert result == json.dumps({"user": {"name": "Alice"}}, indent=2) diff --git a/toons.pyi b/toons.pyi index 4e3f378..0d5713f 100644 --- a/toons.pyi +++ b/toons.pyi @@ -75,6 +75,30 @@ def loads( """ ... +def to_json( + s: str, + *, + strict: bool = True, + expand_paths: Optional[str] = None, + indent: Optional[int] = 2, +) -> str: + """Convert a TOON string to a JSON string. + + Args: + s: TOON-formatted string. + strict: Enforce strict TOON v3.0 compliance. + expand_paths: Path expansion mode: None, "off", "safe", "always". + indent: Spaces per JSON indentation level, or None for compact JSON. + + Returns: + JSON-formatted string. + + Raises: + ToonDecodeError: If the input is malformed. Subclass of ValueError; + carries structured ``.line`` and ``.source`` attributes. + """ + ... + def dump( obj: Any, fp: IO[str], From eb83ea121b6fe94ff7d66b96f8ef8bb06a02111a Mon Sep 17 00:00:00 2001 From: micheal000010000-hub Date: Tue, 19 May 2026 07:50:36 +0530 Subject: [PATCH 2/3] fix: align to_json default indent with json.dumps --- README.md | 2 +- src/lib.rs | 2 +- toons.pyi | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ea4b355..1795d07 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ print(toons.dumps(user)) # active: true # Convert TOON to JSON -print(toons.to_json("name: Alice\nage: 30")) +print(toons.to_json("name: Alice\nage: 30", indent=2)) # { # "name": "Alice", # "age": 30 diff --git a/src/lib.rs b/src/lib.rs index 502f2f8..64b1811 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -119,7 +119,7 @@ mod toons { /// "age": 30 /// } #[pyfunction] - #[pyo3(signature = (s, *, strict=true, expand_paths=None, indent=2))] + #[pyo3(signature = (s, *, strict=true, expand_paths=None, indent=None))] fn to_json( py: Python, s: String, diff --git a/toons.pyi b/toons.pyi index 0d5713f..c4a1355 100644 --- a/toons.pyi +++ b/toons.pyi @@ -80,7 +80,7 @@ def to_json( *, strict: bool = True, expand_paths: Optional[str] = None, - indent: Optional[int] = 2, + indent: Optional[int] = None, ) -> str: """Convert a TOON string to a JSON string. From e277b0c8496037460025b0580613296597f565c1 Mon Sep 17 00:00:00 2001 From: micheal000010000-hub Date: Wed, 20 May 2026 10:29:01 +0530 Subject: [PATCH 3/3] test: align to_json behavior with json.dumps defaults --- tests/integration/test_to_json.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/tests/integration/test_to_json.py b/tests/integration/test_to_json.py index bbc2cc0..623942a 100644 --- a/tests/integration/test_to_json.py +++ b/tests/integration/test_to_json.py @@ -8,14 +8,13 @@ class TestToJson: """Integration tests for to_json() function.""" - def test_to_json_uses_default_indent(self): - """to_json() converts TOON to pretty JSON by default.""" + def test_to_json_uses_json_default_behavior(self): + """to_json() matches json.dumps() default behavior.""" toon_str = "name: Alice\nage: 30\ntags[2]: admin,user" result = toons.to_json(toon_str) assert result == json.dumps( - {"name": "Alice", "age": 30, "tags": ["admin", "user"]}, - indent=2, + {"name": "Alice", "age": 30, "tags": ["admin", "user"]} ) def test_to_json_accepts_indent_none(self): @@ -25,12 +24,22 @@ def test_to_json_accepts_indent_none(self): assert result == json.dumps({"name": "Alice", "age": 30}, indent=None) + def test_to_json_accepts_custom_indent(self): + """to_json() passes custom indent through to json.dumps().""" + toon_str = "name: Alice\nage: 30" + result = toons.to_json(toon_str, indent=2) + + assert result == json.dumps( + {"name": "Alice", "age": 30}, + indent=2, + ) + def test_to_json_respects_strict_flag(self): """to_json() forwards strict=False to the TOON parser.""" toon_str = "[3]:\n - 1\n\n - 2\n - 3" result = toons.to_json(toon_str, strict=False) - assert result == json.dumps([1, 2, 3], indent=2) + assert result == json.dumps([1, 2, 3]) def test_to_json_raises_decode_error(self): """to_json() raises ToonDecodeError for malformed TOON.""" @@ -41,4 +50,4 @@ def test_to_json_respects_expand_paths(self): """to_json() forwards expand_paths to the TOON parser.""" result = toons.to_json("user.name: Alice", expand_paths="safe") - assert result == json.dumps({"user": {"name": "Alice"}}, indent=2) + assert result == json.dumps({"user": {"name": "Alice"}}) \ No newline at end of file