Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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", indent=2))
# {
# "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=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
53 changes: 53 additions & 0 deletions tests/integration/test_to_json.py
Original file line number Diff line number Diff line change
@@ -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"}})
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] = 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],
Expand Down
Loading