diff --git a/README.md b/README.md index 855762a..2e20799 100644 --- a/README.md +++ b/README.md @@ -92,12 +92,14 @@ pytest --cov=zig_fetch_py --cov-report=html Zig Object Notation (ZON) is a data format used by the Zig programming language. It's similar to JSON but with some differences in syntax: -- Objects are defined with `.{ ... }` +- Objects (anonymous structs) are defined with `.{ .key = value, ... }` - Keys are prefixed with a dot: `.key = value` -- Arrays are defined with `.[ ... ]` +- Tuples are defined with `.{value1, value2, ...}` and are parsed as arrays in JSON - Special identifiers can be quoted with `@`: `.@"special-name" = value` - Comments use `//` syntax +Note: ZON doesn't have a dedicated array syntax like JSON's `[]`. Instead, tuples (`.{value1, value2, ...}`) serve a similar purpose and are converted to arrays in JSON. + Example: ```zon @@ -110,7 +112,8 @@ Example: .hash = "abcdef123456", }, }, - .tags = .["tag1", "tag2"], + .tags = .{1, 2, 3}, // Tuple (parsed as array in JSON) + .paths = .{""}, // Single-item tuple } ``` diff --git a/examples/example.zon b/examples/example.zon index 7ca93b8..d266cfa 100644 --- a/examples/example.zon +++ b/examples/example.zon @@ -7,5 +7,5 @@ .hash = "abcdef123456", }, }, - .tags = .["tag1", "tag2"], -} \ No newline at end of file + .tags = .{ "tag1", "tag2" }, +} diff --git a/examples/nested_test.zon b/examples/nested_test.zon new file mode 100644 index 0000000..72c44c0 --- /dev/null +++ b/examples/nested_test.zon @@ -0,0 +1,14 @@ +.{ + .simple_object = .{ + .x = 1, + .y = 2, + }, + .simple_tuple = .{ 1, 2, 3 }, + .nested_object = .{ + .nested = .{ + .a = 1, + .b = 2, + }, + }, + .nested_tuple = .{ .{ 1, 2 }, .{ 3, 4 } }, +} diff --git a/examples/simple.zon b/examples/simple.zon new file mode 100644 index 0000000..3a2dbdf --- /dev/null +++ b/examples/simple.zon @@ -0,0 +1,6 @@ +.{ + .name = "example", + .version = "1.0.0", + .numbers = .{ 1, 2, 3 }, + .strings = .{ "one", "two", "three" }, +} diff --git a/examples/simple_nested.zon b/examples/simple_nested.zon new file mode 100644 index 0000000..fc2a680 --- /dev/null +++ b/examples/simple_nested.zon @@ -0,0 +1,4 @@ +.{ + .simple_tuple = .{ 1, 2, 3 }, + .nested_tuple = .{ .{ 1, 2 }, .{ 3, 4 } }, +} diff --git a/examples/tuple_test.zon b/examples/tuple_test.zon new file mode 100644 index 0000000..631db74 --- /dev/null +++ b/examples/tuple_test.zon @@ -0,0 +1,6 @@ +.{ + .simple_tuple = .{ 1, 2, 3 }, + .string_tuple = .{ "one", "two", "three" }, + .empty_tuple = .{""}, + .mixed_tuple = .{ 1, "two", true }, +} diff --git a/examples/zig_tuples.zon b/examples/zig_tuples.zon new file mode 100644 index 0000000..68454ac --- /dev/null +++ b/examples/zig_tuples.zon @@ -0,0 +1,22 @@ +.{ + // Object with field values + .metadata = .{ + .name = "example", + .version = "1.0.0", + }, + + // Tuples of different types + .numbers = .{ 1, 2, 3 }, + .strings = .{ "one", "two", "three" }, + .mixed = .{ 1, "two", true, null }, + .empty_string = .{""}, + + // Nested structures + .nested = .{ + .tuple_in_object = .{ 4, 5, 6 }, + .object_in_tuple = .{ .{ .x = 1, .y = 2 }, .{ .x = 3, .y = 4 } }, + }, + + // Empty tuple + .empty = .{}, +} diff --git a/tests/test_parser.py b/tests/test_parser.py index e2be48b..e3beab9 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -151,6 +151,24 @@ class TestZonParser: ) parser.parse() + def test_parse_tuple(self): + """Test parsing tuples.""" + parser = ZonParser( + """.{ + .simple_tuple = .{1, 2, 3}, + .string_tuple = .{"one", "two", "three"}, + .empty_tuple = .{""}, + .mixed_tuple = .{1, "two", true}, + }""" + ) + result = parser.parse() + assert result == { + "simple_tuple": [1, 2, 3], + "string_tuple": ["one", "two", "three"], + "empty_tuple": [""], + "mixed_tuple": [1, "two", True], + } + class TestZonFileParser: """Test cases for the file parsing functions.""" diff --git a/zig_fetch_py/parser.py b/zig_fetch_py/parser.py index 2604322..22572b0 100644 --- a/zig_fetch_py/parser.py +++ b/zig_fetch_py/parser.py @@ -78,14 +78,10 @@ class ZonParser: if char == ".": self._next_char() # Skip the dot - # Check if it's an object + # Check if it's an object or tuple if self._current_char() == "{": return self._parse_object() - # Check if it's an array - if self._current_char() == "[": - return self._parse_array() - # It's a field name or a special value return self._parse_identifier() @@ -103,12 +99,36 @@ class ZonParser: f"Unexpected character '{char}' at line {self.line}, column {self.col}" ) - def _parse_object(self) -> Dict[str, Any]: - result = {} - + def _parse_object(self) -> Union[Dict[str, Any], List[Any]]: + """Parse a ZON object (anonymous struct) or tuple.""" # Skip the opening brace self._next_char() + # Check if it's a tuple (values separated by commas without keys) + is_tuple = False + pos_before_check = self.pos + line_before_check = self.line + col_before_check = self.col + + # Look ahead to see if this is a tuple + 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 + self.pos = pos_before_check + self.line = line_before_check + self.col = col_before_check + + if is_tuple: + return self._parse_tuple() + + # Regular object parsing + result = {} + while True: self._skip_whitespace_and_comments() @@ -151,32 +171,44 @@ class ZonParser: return result - def _parse_array(self) -> List[Any]: + def _parse_tuple(self) -> List[Any]: + """Parse a tuple in ZON format.""" result = [] - # Skip the opening bracket - self._next_char() - while True: self._skip_whitespace_and_comments() - # Check for closing bracket - if self._current_char() == "]": + # Check for closing brace + if self._current_char() == "}": self._next_char() break - # Parse value - value = self._parse_value() - result.append(value) + # Check for nested tuple or object with dot prefix + if self._current_char() == ".": + self._next_char() # Skip the dot + + # Check if it's a nested object or tuple + if self._current_char() == "{": + value = self._parse_object() + result.append(value) + else: + # It's a field name or a special value + self.pos -= 1 # Move back to include the dot + value = self._parse_value() + result.append(value) + else: + # Regular value + value = self._parse_value() + result.append(value) self._skip_whitespace_and_comments() # Check for comma if self._current_char() == ",": self._next_char() - elif self._current_char() != "]": + elif self._current_char() != "}": raise ValueError( - f"Expected ',' or ']' at line {self.line}, column {self.col}" + f"Expected ',' or '}}' at line {self.line}, column {self.col}" ) return result