diff --git a/README.md b/README.md index 60f8369..1795d07 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", indent=2)) +# { +# "name": "Alice", +# "age": 30 +# } ``` ### File Operations diff --git a/src/lib.rs b/src/lib.rs index bef30e6..64b1811 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=None))] + 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..623942a --- /dev/null +++ b/tests/integration/test_to_json.py @@ -0,0 +1,53 @@ +import json + +import pytest + +import toons + + +class TestToJson: + """Integration tests for to_json() function.""" + + 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"]} + ) + + 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_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]) + + 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"}}) \ No newline at end of file diff --git a/toons.pyi b/toons.pyi index 4e3f378..c4a1355 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] = None, +) -> 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],