Skip to content

Commit ed331d7

Browse files
committed
#4378 - Field Path
1 parent 9cb3d06 commit ed331d7

File tree

2 files changed

+161
-0
lines changed

2 files changed

+161
-0
lines changed

firestore/google/cloud/firestore_v1beta1/_helpers.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import collections
1919
import contextlib
2020
import datetime
21+
import re
2122
import sys
2223

2324
import google.gax
@@ -119,6 +120,56 @@ def __ne__(self, other):
119120
return not equality_val
120121

121122

123+
class FieldPath(object):
124+
""" Field Path object for client use.
125+
126+
Args:
127+
parts (string or iterable): Indicating path of the key to be used.
128+
"""
129+
130+
def __init__(self, *parts):
131+
invalid_characters = '~*/[]'
132+
if len(parts) == 1:
133+
parts = parts[0]
134+
if isinstance(parts, basestring):
135+
parts = tuple(parts.split('.'))
136+
for part in parts:
137+
if (not part
138+
or not isinstance(part, basestring)
139+
or part in invalid_characters):
140+
raise ValueError
141+
self.parts = tuple(parts)
142+
143+
def to_api_repr(self):
144+
""" Returns string representation of the FieldPath
145+
146+
Returns: string representation of the path stored within
147+
"""
148+
pattern = re.compile(r'[A-Za-z_][A-Za-z_0-9]*')
149+
ans = []
150+
for part in self.parts:
151+
match = re.match(pattern, part)
152+
if match:
153+
ans.append(part)
154+
else:
155+
part_ans = ''
156+
for letter in part:
157+
if letter == '\\':
158+
part_ans += '\\'
159+
elif letter == '`':
160+
part_ans += '\`'
161+
else:
162+
part_ans += letter
163+
ans.append('`' + part_ans + '`')
164+
return '.'.join(ans)
165+
166+
def __hash__(self):
167+
return hash(self.to_api_repr())
168+
169+
def __eq__(self, other):
170+
return self.parts == other.parts
171+
172+
122173
class FieldPathHelper(object):
123174
"""Helper to convert field names and paths for usage in a request.
124175

firestore/tests/unit/test__helpers.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,116 @@ def test___ne__type_differ(self):
8686
self.assertIs(geo_pt1.__ne__(geo_pt2), NotImplemented)
8787

8888

89+
class TestFieldPath(unittest.TestCase):
90+
91+
@staticmethod
92+
def _get_target_class():
93+
from google.cloud.firestore_v1beta1._helpers import FieldPath
94+
return FieldPath
95+
96+
def _make_one(self, *args, **kwargs):
97+
klass = self._get_target_class()
98+
return klass(*args, **kwargs)
99+
100+
def test_empty_string_inside_string_fails(self):
101+
with self.assertRaises(ValueError):
102+
field_path = self._make_one('a..b')
103+
104+
def test_none_fails(self):
105+
with self.assertRaises(ValueError):
106+
field_path = self._make_one('a', None, 'b')
107+
108+
def test_integer_fails(self):
109+
with self.assertRaises(ValueError):
110+
field_path = self._make_one('a', 3, 'b')
111+
112+
def test_iterable_fails(self):
113+
with self.assertRaises(ValueError):
114+
field_path = self._make_one('a', ['a'], 'b')
115+
116+
def test_invalid_chars(self):
117+
parts = '~*/[].'
118+
for part in parts:
119+
with self.assertRaises(ValueError):
120+
field_path = self._make_one(part)
121+
122+
def test_list(self):
123+
parts = ['a', 'b', 'c']
124+
field_path = self._make_one(parts)
125+
self.assertEqual(field_path.parts, ('a', 'b', 'c'))
126+
127+
def test_tuple(self):
128+
parts = ('a', 'b', 'c')
129+
field_path = self._make_one(parts)
130+
self.assertEqual(field_path.parts, ('a', 'b', 'c'))
131+
132+
def test_constructor_string(self):
133+
field_path = self._make_one('a.b.c')
134+
self.assertEqual(field_path.parts, ('a', 'b', 'c'))
135+
136+
def test_constructor_iterable(self):
137+
field_path = self._make_one('a', 'b', 'c')
138+
self.assertEqual(field_path.parts, ('a', 'b', 'c'))
139+
140+
def test_to_api_repr_a(self):
141+
parts = 'a'
142+
field_path = self._make_one(parts)
143+
self.assertEqual('a', field_path.to_api_repr())
144+
145+
def test_to_api_repr_tick(self):
146+
parts = '`'
147+
field_path = self._make_one(parts)
148+
self.assertEqual('`\``', field_path.to_api_repr())
149+
150+
def test_to_api_repr_slash(self):
151+
parts = '\\'
152+
field_path = self._make_one(parts)
153+
self.assertEqual('`\\`', field_path.to_api_repr())
154+
155+
def test_to_api_repr_double_slash(self):
156+
parts = '\\\\'
157+
field_path = self._make_one(parts)
158+
self.assertEqual('`\\\\`', field_path.to_api_repr())
159+
160+
def test_to_api_repr_underscore_valid(self):
161+
parts = '_33132'
162+
field_path = self._make_one(parts)
163+
self.assertEqual('_33132', field_path.to_api_repr())
164+
165+
def test_to_api_repr_number_invalid(self):
166+
parts = '03'
167+
field_path = self._make_one(parts)
168+
self.assertEqual('`03`', field_path.to_api_repr())
169+
170+
def test_to_api_repr_valid_part(self):
171+
parts = 'a0332432'
172+
field_path = self._make_one(parts)
173+
self.assertEqual('a0332432', field_path.to_api_repr())
174+
175+
def test_to_api_repr_chain(self):
176+
parts = 'a', '`', '\\', '_3', '03', 'a03', '\\\\'
177+
field_path = self._make_one(parts)
178+
self.assertEqual('a.`\``.`\\`._3.`03`.a03.`\\\\`',
179+
field_path.to_api_repr())
180+
181+
def test_key(self):
182+
parts = 'a'
183+
field_path = self._make_one('a321', 'b456')
184+
field_path_same_str = self._make_one('a321.b456')
185+
field_path_same_iter = self._make_one(['a321', 'b456'])
186+
field_path_different = self._make_one('a321', 'b457')
187+
keys = {field_path: '',
188+
field_path_same_str: '',
189+
field_path_same_iter: '',
190+
field_path_different: ''
191+
}
192+
for key in keys:
193+
if key == field_path_different:
194+
self.assertNotEqual(key, field_path)
195+
else:
196+
self.assertEqual(key, field_path)
197+
198+
89199
class TestFieldPathHelper(unittest.TestCase):
90200

91201
@staticmethod

0 commit comments

Comments
 (0)