Skip to content

Commit 4a9a5e8

Browse files
authored
Support complex option parsing (#54)
This patch adds proper parsing for complex options like: ```protobuf syntax = "proto3"; message Foo { string bar = 4 [ (oompa.loompa) = { example: "\\"mini@mouse.com\\""; } ]; } ``` This is a backwards-incompatible change because old string values now show up as `Identifier`s. Fixes #52
1 parent 34ab42a commit 4a9a5e8

File tree

2 files changed

+159
-6
lines changed

2 files changed

+159
-6
lines changed

proto_schema_parser/parser.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,8 @@ def visitFieldDeclWithCardinality(
128128
)
129129

130130
def visitCompactOption(self, ctx: ProtobufParser.CompactOptionContext):
131-
name = self._getText(ctx.optionName())
132-
value = self._stringToType(self._getText(ctx.optionValue()))
131+
name = _ASTConstructor.normalize_option_name(self._getText(ctx.optionName()))
132+
value = self.visit(ctx.optionValue())
133133
return ast.Option(name=name, value=value)
134134

135135
def visitCompactOptions(self, ctx: ProtobufParser.CompactOptionsContext):
@@ -347,7 +347,7 @@ def visitListOfMessagesLiteral(
347347

348348
def visitAlwaysIdent(self, ctx: ProtobufParser.AlwaysIdentContext):
349349
if ctx.IDENTIFIER():
350-
# Unlike string/int/float, bools are just reated as identiifers in
350+
# Unlike string/int/float, bools are just treated as identifiers in
351351
# the lexer, so we need to handle them here
352352
identifier_text = self._getText(ctx)
353353
if identifier_text in ["true", "false"]:
@@ -369,7 +369,12 @@ def _getText(self, ctx: Any, strip_quotes: bool = True):
369369
ctx.stop.stop,
370370
) # pyright: ignore [reportGeneralTypeIssues]
371371
text = input_stream.getText(start, stop)
372-
text = text.strip('"') if strip_quotes else text
372+
if (
373+
strip_quotes
374+
and (text.startswith('"') and text.endswith('"'))
375+
or (text.startswith("'") and text.endswith("'"))
376+
):
377+
text = text[1:-1]
373378
return text
374379

375380
def _stringToType(self, value: str):

tests/test_parser.py

Lines changed: 150 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,11 @@ def test_parse_person():
9898
number=2,
9999
cardinality=ast.FieldCardinality.OPTIONAL,
100100
type="PhoneType",
101-
options=[ast.Option(name="default", value="HOME")],
101+
options=[
102+
ast.Option(
103+
name="default", value=ast.Identifier("HOME")
104+
)
105+
],
102106
),
103107
],
104108
),
@@ -142,7 +146,12 @@ def test_parse_search_request():
142146
options=[
143147
ast.Option(
144148
name="(validate.rules).double",
145-
value="{gte: -90, lte: 90}",
149+
value=ast.MessageLiteral(
150+
fields=[
151+
ast.MessageLiteralField(name="gte", value=-90),
152+
ast.MessageLiteralField(name="lte", value=90),
153+
]
154+
),
146155
)
147156
],
148157
),
@@ -1691,3 +1700,142 @@ def _setup_parser(parser: psp_antlr.ProtobufParser) -> None:
16911700
exc_info.value.args[0]
16921701
== r"Failed to parse: line 6:4: mismatched input '}' expecting {';', '['}"
16931702
)
1703+
1704+
1705+
def test_parse_complex_compact_option():
1706+
text_with_email = """
1707+
syntax = "proto3";
1708+
1709+
message Foo {
1710+
string bar = 4 [
1711+
(oompa.loompa) = {
1712+
example: "mini@mouse.com";
1713+
}
1714+
];
1715+
}
1716+
"""
1717+
1718+
result = Parser().parse(text_with_email)
1719+
expected = ast.File(
1720+
syntax="proto3",
1721+
file_elements=[
1722+
ast.Message(
1723+
name="Foo",
1724+
elements=[
1725+
ast.Field(
1726+
name="bar",
1727+
number=4,
1728+
type="string",
1729+
options=[
1730+
ast.Option(
1731+
name="(oompa.loompa)",
1732+
value=ast.MessageLiteral(
1733+
fields=[
1734+
ast.MessageLiteralField(
1735+
name="example",
1736+
value="mini@mouse.com",
1737+
)
1738+
]
1739+
),
1740+
)
1741+
],
1742+
),
1743+
],
1744+
),
1745+
],
1746+
)
1747+
assert result == expected
1748+
1749+
1750+
def test_parse_complex_compact_option_with_escaped_string():
1751+
text_with_email = """
1752+
syntax = "proto3";
1753+
1754+
message Foo {
1755+
string bar = 4 [
1756+
(oompa.loompa) = {
1757+
example: "\\"blah\\"";
1758+
}
1759+
];
1760+
}
1761+
"""
1762+
1763+
print(text_with_email)
1764+
1765+
result = Parser().parse(text_with_email)
1766+
expected = ast.File(
1767+
syntax="proto3",
1768+
file_elements=[
1769+
ast.Message(
1770+
name="Foo",
1771+
elements=[
1772+
ast.Field(
1773+
name="bar",
1774+
number=4,
1775+
type="string",
1776+
options=[
1777+
ast.Option(
1778+
name="(oompa.loompa)",
1779+
value=ast.MessageLiteral(
1780+
fields=[
1781+
ast.MessageLiteralField(
1782+
name="example",
1783+
value='\\"blah\\"',
1784+
)
1785+
]
1786+
),
1787+
)
1788+
],
1789+
),
1790+
],
1791+
),
1792+
],
1793+
)
1794+
assert result == expected
1795+
1796+
1797+
def test_parse_email_compact_option_with_escaped_string():
1798+
text_with_email = """
1799+
syntax = "proto3";
1800+
1801+
message Foo {
1802+
string bar = 4 [
1803+
(oompa.loompa) = {
1804+
example: "\\"mini@mouse.com\\"";
1805+
}
1806+
];
1807+
}
1808+
"""
1809+
1810+
print(text_with_email)
1811+
1812+
result = Parser().parse(text_with_email)
1813+
expected = ast.File(
1814+
syntax="proto3",
1815+
file_elements=[
1816+
ast.Message(
1817+
name="Foo",
1818+
elements=[
1819+
ast.Field(
1820+
name="bar",
1821+
number=4,
1822+
type="string",
1823+
options=[
1824+
ast.Option(
1825+
name="(oompa.loompa)",
1826+
value=ast.MessageLiteral(
1827+
fields=[
1828+
ast.MessageLiteralField(
1829+
name="example",
1830+
value='\\"mini@mouse.com\\"',
1831+
)
1832+
]
1833+
),
1834+
)
1835+
],
1836+
),
1837+
],
1838+
),
1839+
],
1840+
)
1841+
assert result == expected

0 commit comments

Comments
 (0)