Skip to content

Commit a1c899e

Browse files
committed
Add support for union tags used as route attributes.
Summary: Support union tags as route attribute values. Test Plan: Added test. Reviewed By: krieb
1 parent 9c3cf58 commit a1c899e

File tree

5 files changed

+166
-26
lines changed

5 files changed

+166
-26
lines changed

babelapi/babel/parser.py

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -169,20 +169,25 @@ def __repr__(self):
169169

170170
class BabelTagRef(_Element):
171171

172-
def __init__(self, path, lineno, lexpos, tag, union_name=None):
172+
def __init__(self, path, lineno, lexpos, tag, union_name=None, ns=None):
173173
"""
174174
Args:
175175
tag (str): Name of the referenced type.
176-
union_name (str): The name of the union the tag belongs to.
176+
union_name (Optional[str]): The name of the union the tag belongs
177+
to.
178+
ns (Optional[str]): Namespace that referred type is a member of.
179+
If none, then refers to the current namespace.
177180
"""
178181
super(BabelTagRef, self).__init__(path, lineno, lexpos)
179182
self.tag = tag
180183
self.union_name = union_name
184+
self.ns = ns
181185

182186
def __repr__(self):
183-
return 'BabelTagRef({!r}, {!r})'.format(
187+
return 'BabelTagRef({!r}, {!r}, {!r})'.format(
184188
self.tag,
185189
self.union_name,
190+
self.ns,
186191
)
187192

188193
class BabelField(_Element):
@@ -526,14 +531,6 @@ def p_foreign_type_ref(self, p):
526531
ns=p[1],
527532
)
528533

529-
def p_tag_ref(self, p):
530-
"""tag_ref : ID DOT ID
531-
| ID"""
532-
if len(p) > 2:
533-
p[0] = BabelTagRef(self.path, p.lineno(1), p.lexpos(1), p[3], p[1])
534-
else:
535-
p[0] = BabelTagRef(self.path, p.lineno(1), p.lexpos(1), p[1])
536-
537534
# --------------------------------------------------------------
538535
# Structs
539536
#
@@ -635,7 +632,7 @@ def p_field_deprecation(self, p):
635632

636633
def p_default_option(self, p):
637634
"""default_option : EQ primitive
638-
| EQ tag_ref
635+
| EQ short_tag_ref
639636
| empty"""
640637
if p[1]:
641638
if isinstance(p[2], BabelTagRef):
@@ -657,6 +654,10 @@ def p_field(self, p):
657654
if has_docstring:
658655
p[0].set_doc(p[7])
659656

657+
def p_short_tag_ref(self, p):
658+
'short_tag_ref : ID'
659+
p[0] = BabelTagRef(self.path, p.lineno(1), p.lexpos(1), p[1])
660+
660661
# --------------------------------------------------------------
661662
# Unions
662663
#
@@ -763,12 +764,21 @@ def p_attr_fields_add(self, p):
763764
p[0].append(p[2])
764765

765766
def p_attr_field(self, p):
766-
'attr_field : ID EQ primitive NEWLINE'
767+
"""attr_field : ID EQ primitive NEWLINE
768+
| ID EQ tag_ref NEWLINE"""
767769
if p[3] is BabelNull:
768770
p[0] = (p[1], None)
769771
else:
770772
p[0] = (p[1], p[3])
771773

774+
def p_tag_ref(self, p):
775+
"""tag_ref : ID DOT ID DOT ID
776+
| ID DOT ID"""
777+
if len(p) > 4:
778+
p[0] = BabelTagRef(self.path, p.lineno(1), p.lexpos(1), p[5], p[3], p[1])
779+
else:
780+
p[0] = BabelTagRef(self.path, p.lineno(1), p.lexpos(1), p[3], p[1])
781+
772782
# --------------------------------------------------------------
773783
# Doc sections
774784
#

babelapi/babel/tower.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,9 @@ def _populate_type_attributes(self):
320320
for route in namespace.routes:
321321
self._populate_route_attributes(env, route)
322322

323+
# TODO(kelkabany): Infer the type of each route attr and ensure that
324+
# the type is consistent across all routes.
325+
323326
assert len(self._resolution_in_progress) == 0
324327

325328
def _populate_struct_type_attributes(self, env, data_type):
@@ -458,13 +461,37 @@ def _populate_route_attributes(self, env, route):
458461
else:
459462
deprecated = None
460463

464+
new_attrs = {}
465+
for k, v in route._token.attrs.items():
466+
if isinstance(v, BabelTagRef):
467+
type_ref = BabelTypeRef(
468+
v.path, v.lineno, v.lexpos, v.union_name, args=((), {}),
469+
nullable=False, ns=v.ns)
470+
data_type = self._resolve_type(env, type_ref, True)
471+
for field in data_type.fields:
472+
if v.tag == field.name:
473+
if not isinstance(field.data_type, Void):
474+
raise InvalidSpec(
475+
'Tag %s referenced by route attribute must be '
476+
'void.' % quote('%s.%s' % (data_type.name, v.tag)),
477+
v.lineno, v.path)
478+
break
479+
else:
480+
raise InvalidSpec(
481+
'%s has no tag %s.' %
482+
(quote(data_type.name), quote(v.tag)),
483+
v.lineno, v.path)
484+
new_attrs[k] = TagRef(data_type, v.tag)
485+
else:
486+
new_attrs[k] = v
487+
461488
route.set_attributes(
462489
deprecated=deprecated,
463490
doc=route._token.doc,
464491
request_data_type=request_dt,
465492
response_data_type=response_dt,
466493
error_data_type=error_dt,
467-
attrs=route._token.attrs)
494+
attrs=new_attrs)
468495

469496
def _create_struct_field(self, env, babel_field):
470497
"""

doc/generator_ref.rst

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,9 @@ error_data_type
143143
A DataType object of an error.
144144

145145
attrs
146-
A map from string keys to Python primitive values that is a direct copy
147-
of the attrs specified in the route definition.
146+
A map from string keys to values that is a direct copy of the attrs
147+
specified in the route definition. Values are limited to Python primitives
148+
(None, bool, float, int, str) and `TagRef objects <#union-tag-reference>`_.
148149

149150
See the Python object definition for more information.
150151

@@ -497,9 +498,9 @@ a ``Nullable``, then it's returned unmodified.
497498
Union Tag Reference
498499
===================
499500

500-
The default of a struct field with a union data type can be a member of that
501-
union with void type. If this is the case, the value of the default will be a
502-
``TagRef`` object with the following attributes:
501+
Tag references can occur in two instances. First, as the default of a struct
502+
field with a union data type. Second, as the value of a route attribute.
503+
References are limited to members with void type.
503504

504505
TagRef
505506
------

doc/lang_ref.rst

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -589,17 +589,29 @@ to a service. For example, the Dropbox API needs a way to specify some routes
589589
as including a binary body (uploads) for requests. Another example is specifying
590590
which routes can be used without authentication credentials.
591591

592-
To cover this open ended use case, routes can have an ``attrs`` section declared
592+
To cover this open-ended use case, routes can have an ``attrs`` section declared
593593
followed by an arbitrary set of ``key=value`` pairs::
594594

595-
route get_account (GetAccountReq, Account, GetAccountErr)
596-
"Get information about a specified user's account."
595+
route ping (Void, Void, Void)
597596

598597
attrs
599-
key1="value1"
600-
key2=1234
601-
key3=3.14
602-
key4=false
598+
key1 = "value1"
599+
key2 = 1234
600+
key3 = 3.14
601+
key4 = false
602+
key5 = null
603+
604+
A value can reference a union tag with void type::
605+
606+
route ping (Void, Void, Void)
607+
608+
attrs
609+
key = Letters.a
610+
611+
union Letters
612+
a
613+
b
614+
c
603615

604616
Code generators will populate a route object with these attributes.
605617

test/test_babel.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
)
1313
from babelapi.babel.tower import (
1414
InvalidSpec,
15+
TagRef,
1516
TowerOfBabel,
1617
)
1718

@@ -404,6 +405,95 @@ def test_route_decl(self):
404405
self.assertEqual("'S' must be a route.", cm.exception.msg)
405406
self.assertEqual(cm.exception.lineno, 3)
406407

408+
def test_route_attrs(self):
409+
# Test basic attrs
410+
text = textwrap.dedent("""\
411+
namespace test
412+
413+
union U
414+
a
415+
b
416+
417+
route r (Void, Void, Void)
418+
attrs
419+
null_val = null
420+
str_val = "r"
421+
int_val = 3
422+
float_val = 1.2
423+
union_val = U.a
424+
""")
425+
t = TowerOfBabel([('test.babel', text)])
426+
t.parse()
427+
r = t.api.namespaces['test'].route_by_name['r']
428+
self.assertEqual(r.attrs['null_val'], None)
429+
self.assertEqual(r.attrs['str_val'], 'r')
430+
self.assertEqual(r.attrs['int_val'], 3)
431+
self.assertEqual(r.attrs['float_val'], 1.2)
432+
self.assertIsInstance(r.attrs['union_val'], TagRef)
433+
self.assertEqual(r.attrs['union_val'].tag_name, 'a')
434+
self.assertEqual(r.attrs['union_val'].union_data_type,
435+
t.api.namespaces['test'].data_type_by_name['U'])
436+
437+
# Try unknown tag
438+
text = textwrap.dedent("""\
439+
namespace test
440+
441+
union U
442+
a
443+
b
444+
445+
route r (Void, Void, Void)
446+
attrs
447+
union_val = U.z
448+
""")
449+
t = TowerOfBabel([('test.babel', text)])
450+
with self.assertRaises(InvalidSpec) as cm:
451+
t.parse()
452+
self.assertEqual("'U' has no tag 'z'.", cm.exception.msg)
453+
454+
# Try non-void tag
455+
text = textwrap.dedent("""\
456+
namespace test
457+
458+
union U
459+
a
460+
b String
461+
462+
route r (Void, Void, Void)
463+
attrs
464+
union_val = U.b
465+
""")
466+
t = TowerOfBabel([('test.babel', text)])
467+
with self.assertRaises(InvalidSpec) as cm:
468+
t.parse()
469+
self.assertEqual(
470+
"Tag 'U.b' referenced by route attribute must be void.",
471+
cm.exception.msg)
472+
473+
# Test imported union as attr
474+
text1 = textwrap.dedent("""\
475+
namespace shared
476+
477+
union U
478+
a
479+
b
480+
""")
481+
text2 = textwrap.dedent("""\
482+
namespace test
483+
484+
import shared
485+
486+
route r (Void, Void, Void)
487+
attrs
488+
union_val = shared.U.a
489+
""")
490+
t = TowerOfBabel([('test1.babel', text1), ('test2.babel', text2)])
491+
t.parse()
492+
r = t.api.namespaces['test'].route_by_name['r']
493+
self.assertEqual(r.attrs['union_val'].tag_name, 'a')
494+
self.assertEqual(r.attrs['union_val'].union_data_type,
495+
t.api.namespaces['shared'].data_type_by_name['U'])
496+
407497
def test_lexing_errors(self):
408498
text = textwrap.dedent("""\
409499

0 commit comments

Comments
 (0)