Skip to content

Commit dcdfd4e

Browse files
committed
4378 - Field Path
1 parent 1a5cfa4 commit dcdfd4e

File tree

2 files changed

+176
-0
lines changed

2 files changed

+176
-0
lines changed

firestore/google/cloud/firestore_v1beta1/_helpers.py

Lines changed: 52 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,57 @@ 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: (one or more strings)
128+
Indicating path of the key to be used.
129+
"""
130+
pattern = re.compile(r'[A-Za-z_][A-Za-z_0-9]*')
131+
132+
def __init__(self, *parts):
133+
for part in parts:
134+
if not isinstance(part, str):
135+
raise ValueError("One or more components is not a string")
136+
self.parts = tuple(parts)
137+
138+
@classmethod
139+
def from_string(cls, string):
140+
""" Creates a FieldPath with a string representation.
141+
142+
Returns:
143+
A :class: `FieldPath` instance with the string as path.
144+
"""
145+
invalid_characters = '~*/[]'
146+
string = string.split('.')
147+
for part in string:
148+
if (not part or part in invalid_characters):
149+
raise ValueError("Invalid characters or no string present")
150+
return FieldPath(*string)
151+
152+
def to_api_repr(self):
153+
""" Returns string representation of the FieldPath
154+
155+
Returns: string representation of the path stored within
156+
"""
157+
ans = []
158+
for part in self.parts:
159+
match = re.match(self.pattern, part)
160+
if match:
161+
ans.append(part)
162+
else:
163+
replaced = part.replace('\\', '\\\\').replace('`', '\`')
164+
ans.append('`' + replaced + '`')
165+
return '.'.join(ans)
166+
167+
def __hash__(self):
168+
return hash(self.to_api_repr())
169+
170+
def __eq__(self, other):
171+
return self.parts == other.parts
172+
173+
122174
class FieldPathHelper(object):
123175
"""Helper to convert field names and paths for usage in a request.
124176

firestore/tests/unit/test__helpers.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,130 @@ 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_none_fails(self):
101+
with self.assertRaises(ValueError):
102+
field_path = self._make_one('a', None, 'b')
103+
104+
def test_integer_fails(self):
105+
with self.assertRaises(ValueError):
106+
field_path = self._make_one('a', 3, 'b')
107+
108+
def test_iterable_fails(self):
109+
with self.assertRaises(ValueError):
110+
field_path = self._make_one('a', ['a'], 'b')
111+
112+
def test_invalid_chars_in_constructor(self):
113+
parts = '~*/[].'
114+
for part in parts:
115+
field_path = self._make_one(part)
116+
self.assertEqual(field_path.parts, (part, ))
117+
118+
def test_component(self):
119+
field_path = self._make_one('a..b')
120+
self.assertEquals(field_path.parts, ('a..b',))
121+
122+
def test_constructor_iterable(self):
123+
field_path = self._make_one('a', 'b', 'c')
124+
self.assertEqual(field_path.parts, ('a', 'b', 'c'))
125+
126+
def test_to_api_repr_a(self):
127+
parts = 'a'
128+
field_path = self._make_one(parts)
129+
self.assertEqual('a', field_path.to_api_repr())
130+
131+
def test_to_api_repr_tick(self):
132+
parts = '`'
133+
field_path = self._make_one(parts)
134+
self.assertEqual('`\``', field_path.to_api_repr())
135+
136+
def test_to_api_repr_slash(self):
137+
parts = '\\'
138+
field_path = self._make_one(parts)
139+
self.assertEqual(r'`\\`', field_path.to_api_repr())
140+
141+
def test_to_api_repr_double_slash(self):
142+
parts = r'\\'
143+
field_path = self._make_one(parts)
144+
self.assertEqual(r'`\\\\`', field_path.to_api_repr())
145+
146+
def test_to_api_repr_underscore_valid(self):
147+
parts = '_33132'
148+
field_path = self._make_one(parts)
149+
self.assertEqual('_33132', field_path.to_api_repr())
150+
151+
def test_to_api_repr_number_invalid(self):
152+
parts = '03'
153+
field_path = self._make_one(parts)
154+
self.assertEqual('`03`', field_path.to_api_repr())
155+
156+
def test_to_api_repr_valid_part(self):
157+
parts = 'a0332432'
158+
field_path = self._make_one(parts)
159+
self.assertEqual('a0332432', field_path.to_api_repr())
160+
161+
def test_to_api_repr_chain(self):
162+
parts = 'a', '`', '\\', '_3', '03', 'a03', '\\\\'
163+
field_path = self._make_one(*parts)
164+
self.assertEqual(r'a.`\``.`\\`._3.`03`.a03.`\\\\`',
165+
field_path.to_api_repr())
166+
167+
def test_from_string(self):
168+
field_path = self._get_target_class().from_string('a.b.c')
169+
self.assertEqual(field_path.parts, ('a', 'b', 'c'))
170+
171+
def test_list_splat(self):
172+
parts = ['a', 'b', 'c']
173+
field_path = self._make_one(*parts)
174+
self.assertEqual(field_path.parts, ('a', 'b', 'c'))
175+
176+
def test_tuple_splat(self):
177+
parts = ('a', 'b', 'c')
178+
field_path = self._make_one(*parts)
179+
self.assertEqual(field_path.parts, ('a', 'b', 'c'))
180+
181+
def test_invalid_chars_from_string_fails(self):
182+
parts = '~*/[].'
183+
for part in parts:
184+
with self.assertRaises(ValueError):
185+
field_path = self._get_target_class().from_string(part)
186+
187+
def test_list_fails(self):
188+
parts = ['a', 'b', 'c']
189+
with self.assertRaises(ValueError):
190+
field_path = self._make_one(parts)
191+
192+
def test_tuple_fails(self):
193+
parts = ('a', 'b', 'c')
194+
with self.assertRaises(ValueError):
195+
field_path = self._make_one(parts)
196+
197+
def test_key(self):
198+
parts = 'a'
199+
field_path = self._make_one('a321', 'b456')
200+
field_path_same = self._get_target_class().from_string('a321.b456')
201+
field_path_different = self._make_one('a321', 'b457')
202+
keys = {field_path: '',
203+
field_path_same: '',
204+
field_path_different: ''
205+
}
206+
for key in keys:
207+
if key == field_path_different:
208+
self.assertNotEqual(key, field_path)
209+
else:
210+
self.assertEqual(key, field_path)
211+
212+
89213
class TestFieldPathHelper(unittest.TestCase):
90214

91215
@staticmethod

0 commit comments

Comments
 (0)