Improve ZON parser tuple handling with configurable empty tuple parsing

- Add `empty_tuple_as_dict` parameter to control parsing of empty tuples
- Enhance tuple and object parsing logic to handle nested and complex structures
- Update test cases to cover various tuple and empty tuple scenarios
- Modify `parse_zon_file` and `zon_to_json` functions to support new parsing option
This commit is contained in:
2025-03-07 17:11:57 +08:00
parent fd29f7e3af
commit 81348b1f28
2 changed files with 156 additions and 40 deletions

View File

@ -13,11 +13,17 @@ class TestZonParser:
"""Test cases for the ZonParser class.""" """Test cases for the ZonParser class."""
def test_parse_empty_object(self): def test_parse_empty_object(self):
"""Test parsing an empty object.""" """Test parsing an empty object with empty_tuple_as_dict=True."""
parser = ZonParser(".{}") parser = ZonParser(".{}", empty_tuple_as_dict=True)
result = parser.parse() result = parser.parse()
assert result == {} assert result == {}
def test_parse_empty_tuple_as_list(self):
"""Test parsing an empty tuple as list with empty_tuple_as_dict=False."""
parser = ZonParser(".{}", empty_tuple_as_dict=False)
result = parser.parse()
assert result == []
def test_parse_simple_object(self): def test_parse_simple_object(self):
"""Test parsing a simple object with string values.""" """Test parsing a simple object with string values."""
parser = ZonParser( parser = ZonParser(
@ -42,16 +48,6 @@ class TestZonParser:
result = parser.parse() result = parser.parse()
assert result == {"metadata": {"name": "test", "version": "1.0.0"}} assert result == {"metadata": {"name": "test", "version": "1.0.0"}}
def test_parse_array(self):
"""Test parsing an array."""
parser = ZonParser(
""".{
.tags = .["tag1", "tag2", "tag3"],
}"""
)
result = parser.parse()
assert result == {"tags": ["tag1", "tag2", "tag3"]}
def test_parse_numbers(self): def test_parse_numbers(self):
"""Test parsing different number formats.""" """Test parsing different number formats."""
parser = ZonParser( parser = ZonParser(
@ -169,6 +165,65 @@ class TestZonParser:
"mixed_tuple": [1, "two", True], "mixed_tuple": [1, "two", True],
} }
def test_parse_nested_tuples(self):
"""Test parsing nested tuples."""
parser = ZonParser(
""".{
.nested_tuple = .{ .{1, 2}, .{3, 4} },
}"""
)
result = parser.parse()
assert result == {"nested_tuple": [[1, 2], [3, 4]]}
def test_parse_objects_in_tuple(self):
"""Test parsing objects within tuples."""
parser = ZonParser(
""".{
.object_in_tuple = .{ .{.x = 1, .y = 2}, .{.x = 3, .y = 4} },
}"""
)
result = parser.parse()
assert result == {"object_in_tuple": [{"x": 1, "y": 2}, {"x": 3, "y": 4}]}
def test_parse_complex_nested_structures(self):
"""Test parsing complex nested structures."""
parser = ZonParser(
""".{
.metadata = .{
.name = "example",
.version = "1.0.0",
},
.simple_tuple = .{1, 2, 3},
.nested = .{
.tuple_in_object = .{4, 5, 6},
.object_in_tuple = .{ .{.x = 1, .y = 2}, .{.x = 3, .y = 4} },
},
.nested_tuple = .{ .{1, 2}, .{3, 4} },
.empty = .{},
}"""
)
result = parser.parse()
assert result == {
"metadata": {"name": "example", "version": "1.0.0"},
"simple_tuple": [1, 2, 3],
"nested": {
"tuple_in_object": [4, 5, 6],
"object_in_tuple": [{"x": 1, "y": 2}, {"x": 3, "y": 4}],
},
"nested_tuple": [[1, 2], [3, 4]],
"empty": [],
}
def test_parse_tuple_as_array(self):
"""Test parsing a tuple as an array."""
parser = ZonParser(
""".{
.tags = .{"tag1", "tag2", "tag3"},
}"""
)
result = parser.parse()
assert result == {"tags": ["tag1", "tag2", "tag3"]}
class TestZonFileParser: class TestZonFileParser:
"""Test cases for the file parsing functions.""" """Test cases for the file parsing functions."""

View File

@ -14,17 +14,20 @@ class ZonParser:
A parser for Zig Object Notation (ZON) files. A parser for Zig Object Notation (ZON) files.
""" """
def __init__(self, content: str): def __init__(self, content: str, empty_tuple_as_dict: bool = False):
""" """
Initialize the parser with ZON content. Initialize the parser with ZON content.
Args: Args:
content: The ZON content to parse content: The ZON content to parse
empty_tuple_as_dict: If True, empty tuples (.{}) will be parsed as empty dictionaries ({})
If False, empty tuples will be parsed as empty lists ([])
""" """
self.content = content self.content = content
self.pos = 0 self.pos = 0
self.line = 1 self.line = 1
self.col = 1 self.col = 1
self.empty_tuple_as_dict = empty_tuple_as_dict
def parse(self) -> Dict[str, Any]: def parse(self) -> Dict[str, Any]:
"""Parse ZON content and return a Python dictionary.""" """Parse ZON content and return a Python dictionary."""
@ -71,6 +74,7 @@ class ZonParser:
break break
def _parse_value(self) -> Any: def _parse_value(self) -> Any:
"""Parse a ZON value."""
self._skip_whitespace_and_comments() self._skip_whitespace_and_comments()
char = self._current_char() char = self._current_char()
@ -100,33 +104,59 @@ class ZonParser:
) )
def _parse_object(self) -> Union[Dict[str, Any], List[Any]]: def _parse_object(self) -> Union[Dict[str, Any], List[Any]]:
"""Parse a ZON object (anonymous struct) or tuple.""" """Parse a ZON object or tuple."""
# Skip the opening brace # Skip the opening brace
self._next_char() self._next_char()
# Check if it's a tuple (values separated by commas without keys) # Look ahead to see if this is a tuple or an object
is_tuple = False pos_before = self.pos
pos_before_check = self.pos line_before = self.line
line_before_check = self.line col_before = self.col
col_before_check = self.col
# Look ahead to see if this is a tuple
self._skip_whitespace_and_comments() self._skip_whitespace_and_comments()
if self._current_char() != ".":
# If it doesn't start with a dot for a key, it might be a tuple
# or an empty object
if self._current_char() != "}":
is_tuple = True
# Reset position after look-ahead # Check if it's empty
self.pos = pos_before_check if self._current_char() == "}":
self.line = line_before_check # Need to determine if it should be an empty object or empty tuple
self.col = col_before_check # Use the configuration option to decide
self._next_char() # Skip the closing brace
return (
{} if self.empty_tuple_as_dict else []
) # Empty dict or list based on config
# Look at the first character to determine if it's a tuple or object
is_tuple = True
if self._current_char() == ".":
# Look ahead one more character
self._next_char()
# If the next character is an object, it could be a nested tuple
if self._current_char() == "{":
# This is potentially a nested tuple starting with .{
# Go back to the dot and let the normal parsing decide
self.pos -= 1
elif (
self._current_char() == "@"
or self._current_char().isalnum()
or self._current_char() == "_"
):
# This looks like a field name, so it's probably an object
is_tuple = False
else:
# Unexpected character after dot, could be a syntax error
is_tuple = False
# Reset position
self.pos = pos_before
self.line = line_before
self.col = col_before
if is_tuple: if is_tuple:
return self._parse_tuple() return self._parse_tuple()
else:
return self._parse_struct()
# Regular object parsing def _parse_struct(self) -> Dict[str, Any]:
"""Parse a ZON struct/object with key-value pairs."""
result = {} result = {}
while True: while True:
@ -171,10 +201,25 @@ class ZonParser:
return result return result
def _parse_tuple(self) -> List[Any]: def _parse_tuple(self) -> Union[Dict[str, Any], List[Any]]:
"""Parse a tuple in ZON format.""" """
Parse a ZON tuple as a list of values or empty dict based on configuration.
Returns:
List[Any] for non-empty tuples, or Dict[str, Any] if empty and empty_tuple_as_dict=True
"""
result = [] result = []
# Skip the opening brace (already done in _parse_object)
self._skip_whitespace_and_comments()
# Check for empty tuple
if self._current_char() == "}":
self._next_char()
return (
{} if self.empty_tuple_as_dict else []
) # Empty dict or list based on config
while True: while True:
self._skip_whitespace_and_comments() self._skip_whitespace_and_comments()
@ -183,17 +228,27 @@ class ZonParser:
self._next_char() self._next_char()
break break
# Check for nested tuple or object with dot prefix # Handle the special case of nested tuple/object with dot prefix
if self._current_char() == ".": if self._current_char() == ".":
# Save position before the dot
pos_before = self.pos
line_before = self.line
col_before = self.col
self._next_char() # Skip the dot self._next_char() # Skip the dot
# Check if it's a nested object or tuple # If we have a nested object/tuple
if self._current_char() == "{": if self._current_char() == "{":
# Parse the nested object/tuple
value = self._parse_object() value = self._parse_object()
result.append(value) result.append(value)
else: else:
# It's a field name or a special value # Not a nested tuple/object, reset position and parse normally
self.pos -= 1 # Move back to include the dot self.pos = pos_before
self.line = line_before
self.col = col_before
# Parse as normal value
value = self._parse_value() value = self._parse_value()
result.append(value) result.append(value)
else: else:
@ -346,12 +401,14 @@ class ZonParser:
) )
def parse_zon_file(file_path: str) -> Dict[str, Any]: def parse_zon_file(file_path: str, empty_tuple_as_dict: bool = False) -> Dict[str, Any]:
""" """
Parse a ZON file and return a Python dictionary. Parse a ZON file and return a Python dictionary.
Args: Args:
file_path: Path to the ZON file file_path: Path to the ZON file
empty_tuple_as_dict: If True, empty tuples (.{}) will be parsed as empty dictionaries ({})
If False, empty tuples will be parsed as empty lists ([])
Returns: Returns:
Dictionary representation of the ZON file Dictionary representation of the ZON file
@ -360,23 +417,27 @@ def parse_zon_file(file_path: str) -> Dict[str, Any]:
with open(file_path, "r") as f: with open(file_path, "r") as f:
content = f.read() content = f.read()
parser = ZonParser(content) parser = ZonParser(content, empty_tuple_as_dict=empty_tuple_as_dict)
result = parser.parse() result = parser.parse()
logger.debug(f"Successfully parsed ZON file") logger.debug(f"Successfully parsed ZON file")
return result return result
def zon_to_json(zon_content: str, indent: Optional[int] = None) -> str: def zon_to_json(
zon_content: str, indent: Optional[int] = None, empty_tuple_as_dict: bool = False
) -> str:
""" """
Convert ZON content to JSON string. Convert ZON content to JSON string.
Args: Args:
zon_content: ZON content as string zon_content: ZON content as string
indent: Number of spaces for indentation (None for compact JSON) indent: Number of spaces for indentation (None for compact JSON)
empty_tuple_as_dict: If True, empty tuples (.{}) will be parsed as empty dictionaries ({})
If False, empty tuples will be parsed as empty lists ([])
Returns: Returns:
JSON string JSON string
""" """
parser = ZonParser(zon_content) parser = ZonParser(zon_content, empty_tuple_as_dict=empty_tuple_as_dict)
result = parser.parse() result = parser.parse()
return json.dumps(result, indent=indent) return json.dumps(result, indent=indent)