Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 48 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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))]

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a fan of the default indent set to 2, I would match the behavior of the json module, it feels more natural IMHO.

Suggested change
#[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,
strict: bool,
expand_paths: Option<&str>,
indent: Option<usize>,
) -> PyResult<String> {
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
Expand Down
44 changes: 44 additions & 0 deletions tests/integration/test_to_json.py
Original file line number Diff line number Diff line change
@@ -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)
24 changes: 24 additions & 0 deletions toons.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down