Skip to content

Commit ab6fdad

Browse files
[core] feat: add support for parsing resource_index and nested_field customizers
1 parent 38fc989 commit ab6fdad

File tree

3 files changed

+122
-2
lines changed

3 files changed

+122
-2
lines changed

libs/core/garf_core/parsers.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,43 @@ def process_customizer(
130130
) -> api_clients.ApiRowElement:
131131
if customizer.type == 'slice':
132132
return self._process_customizer_slice(row, customizer, field)
133+
if customizer.type == 'nested_field':
134+
return self._process_nested_field(row, customizer, field)
135+
if customizer.type == 'resource_index':
136+
return self._process_resource_index(row, customizer, field)
133137
return row
134138

135139
def _process_customizer_slice(self, row, customizer, field):
136140
slice_object = customizer.value.slice_literal
137141
return [r.get(customizer.value.value) for r in row.get(field)[slice_object]]
138142

143+
def _process_nested_field(self, row, customizer, field):
144+
nested_field = row.get(field)
145+
try:
146+
return operator.attrgetter(customizer.value)(nested_field)
147+
except AttributeError as e:
148+
raise query_parser.GarfCustomizerError(
149+
f'nested field {customizer.value} is missing in row {row}'
150+
) from e
151+
152+
def _process_resource_index(self, row, customizer, field):
153+
resource = row.get(field, '/')
154+
_, *elements = row.get(field, '/').split('/')
155+
if not elements:
156+
raise query_parser.GarfCustomizerError(
157+
f'Not a valid resource: {resource}'
158+
)
159+
resource_elements = elements[-1].split('~')
160+
try:
161+
try:
162+
return int(resource_elements[customizer.value])
163+
except ValueError:
164+
return resource_elements[customizer.value]
165+
except IndexError as e:
166+
raise query_parser.GarfCustomizerError(
167+
'Not a valid position in resource: %s, %d', resource, customizer.value
168+
) from e
169+
139170
def parse_row(
140171
self,
141172
row: api_clients.ApiResponseRow,

libs/core/garf_core/query_parser.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ class Customizer(pydantic.BaseModel):
5858

5959
def __bool__(self) -> bool:
6060
"""Evaluates whether all fields are not empty."""
61-
return bool(self.type and self.value)
61+
return bool(self.type and self.value is not None)
6262

6363

6464
class SliceField(pydantic.BaseModel):

libs/core/tests/unit/test_parsers.py

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,19 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import pydantic
1516
import pytest
16-
from garf_core import api_clients, parsers, query_editor
17+
from garf_core import api_clients, parsers, query_editor, query_parser
1718

1819
test_specification = query_editor.QuerySpecification(
1920
'SELECT test_column_1 FROM test'
2021
).generate()
2122

2223

24+
class NestedResource(pydantic.BaseModel):
25+
nested_element: int
26+
27+
2328
class TestDictParser:
2429
@pytest.fixture
2530
def test_parser(self):
@@ -117,6 +122,90 @@ def test_parse_response_returns_correct_result_for_arrays(
117122

118123
assert parsed_row == expected_row
119124

125+
@pytest.mark.parametrize(
126+
('index', 'expected'),
127+
[
128+
('0', [[0], [1]]),
129+
('1', [['TEXT'], ['IMAGE']]),
130+
('2', [[54321], [12345]]),
131+
],
132+
)
133+
def test_parse_response_returns_correct_resource_index(self, index, expected):
134+
spec = query_editor.QuerySpecification(
135+
text=f'SELECT resource~{index} AS column FROM test'
136+
).generate()
137+
test_parser = parsers.DictParser(spec)
138+
test_response = api_clients.GarfApiResponse(
139+
results=[
140+
{'resource': 'resource/1/test/0~TEXT~54321'},
141+
{'resource': 'resource/1/test/1~IMAGE~12345'},
142+
]
143+
)
144+
parsed_row = test_parser.parse_response(test_response)
145+
assert parsed_row == expected
146+
147+
def test_parse_response_raises_customizer_error_on_invalid_position(self):
148+
spec = query_editor.QuerySpecification(
149+
text='SELECT resource~4 AS column FROM test'
150+
).generate()
151+
test_parser = parsers.DictParser(spec)
152+
test_response = api_clients.GarfApiResponse(
153+
results=[
154+
{'resource': 'resource/1/test/0~TEXT~54321'},
155+
]
156+
)
157+
with pytest.raises(
158+
query_parser.GarfCustomizerError, match='Not a valid position in resource'
159+
):
160+
test_parser.parse_response(test_response)
161+
162+
def test_parse_response_raises_customizer_error_on_invalid_resource(self):
163+
spec = query_editor.QuerySpecification(
164+
text='SELECT resource~0 AS column FROM test'
165+
).generate()
166+
test_parser = parsers.DictParser(spec)
167+
test_response = api_clients.GarfApiResponse(
168+
results=[
169+
{'resource': 'resource'},
170+
]
171+
)
172+
with pytest.raises(
173+
query_parser.GarfCustomizerError, match='Not a valid resource'
174+
):
175+
test_parser.parse_response(test_response)
176+
177+
def test_parse_response_returns_correct_nested_attribute(self):
178+
spec = query_editor.QuerySpecification(
179+
text='SELECT resource:nested_element AS column FROM test'
180+
).generate()
181+
test_parser = parsers.DictParser(spec)
182+
test_response = api_clients.GarfApiResponse(
183+
results=[
184+
{'resource': NestedResource(nested_element=1)},
185+
{'resource': NestedResource(nested_element=2)},
186+
]
187+
)
188+
parsed_row = test_parser.parse_response(test_response)
189+
assert parsed_row == [[1], [2]]
190+
191+
def test_parse_response_raises_customizer_error_on_missing_nested_attribute(
192+
self,
193+
):
194+
spec = query_editor.QuerySpecification(
195+
text='SELECT resource:missing_element AS column FROM test'
196+
).generate()
197+
test_parser = parsers.DictParser(spec)
198+
test_response = api_clients.GarfApiResponse(
199+
results=[
200+
{'resource': NestedResource(nested_element=1)},
201+
]
202+
)
203+
with pytest.raises(
204+
query_parser.GarfCustomizerError,
205+
match='nested field missing_element is missing in row',
206+
):
207+
test_parser.parse_response(test_response)
208+
120209
def test_parse_response_skips_omitted_columns(self):
121210
test_specification = query_editor.QuerySpecification(
122211
'SELECT test_column_1 AS _, test_column_2 FROM test'

0 commit comments

Comments
 (0)