Skip to content

Commit c0f247e

Browse files
committed
Refactor ordering query parsing and enhance validation error handling
1 parent 58ae0c7 commit c0f247e

File tree

2 files changed

+46
-60
lines changed

2 files changed

+46
-60
lines changed

packages/models-library/src/models_library/rest_ordering.py

Lines changed: 41 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
from enum import Enum
2-
from typing import Annotated, Generic, TypeVar
1+
from typing import Annotated, Generic
32

43
from common_library.basic_types import DEFAULT_FACTORY
54
from common_library.json_serialization import json_dumps
@@ -13,16 +12,17 @@
1312
from pydantic.generics import GenericModel
1413

1514
from .basic_types import IDStr
15+
from .list_operations import OrderClause, OrderDirection, TField, check_ordering_list
1616
from .rest_base import RequestParameters
17-
from .utils.common_validators import parse_json_pre_validator
18-
17+
from .utils.common_validators import (
18+
parse_json_pre_validator,
19+
)
1920

20-
class OrderDirection(str, Enum):
21-
ASC = "asc"
22-
DESC = "desc"
21+
__all__: tuple[str, ...] = ("OrderDirection",)
2322

2423

2524
class OrderBy(BaseModel):
25+
# NOTE: use instead OrderClause[TField] where TField is Literal of valid fields
2626
field: Annotated[IDStr, Field(description="field name identifier")]
2727
direction: Annotated[
2828
OrderDirection,
@@ -128,21 +128,10 @@ class _OrderJsonQueryParams(_BaseOrderQueryParams):
128128
return _OrderJsonQueryParams
129129

130130

131-
#
132-
#
133-
# NOTE: OrderingQueryParams is a more flexible variant for generic usage and that
134-
# does include multiple ordering clauses
135-
#
136-
137-
TField = TypeVar("TField", bound=str)
138-
139-
140-
class OrderClause(GenericModel, Generic[TField]):
141-
field: TField
142-
direction: OrderDirection = OrderDirection.ASC
143-
144-
145131
class OrderingQueryParams(GenericModel, Generic[TField]):
132+
# NOTE: OrderingQueryParams is a more flexible variant for generic usage and that
133+
# does include multiple ordering clauses
134+
#
146135
order_by: Annotated[
147136
list[OrderClause[TField]],
148137
Field(
@@ -153,41 +142,39 @@ class OrderingQueryParams(GenericModel, Generic[TField]):
153142

154143
@field_validator("order_by", mode="before")
155144
@classmethod
156-
def _parse_order_by(cls, v):
157-
"""
158-
Example:
159-
160-
GET /items?order_by=-created_at,name
161-
162-
Parses to:
163-
[
164-
OrderClause(field="created_at", direction=OrderDirection.DESC),
165-
OrderClause(field="name", direction=OrderDirection.ASC),
166-
]
145+
def _parse_order_by_string(cls, v):
146+
"""Parses a comma-separated string into a list of OrderClause
147+
148+
Example, given the query parameter `order_by` in a request like `GET /items?order_by=-created_at,name`
149+
It parses to:
150+
[
151+
OrderClause(field="created_at", direction=OrderDirection.DESC),
152+
OrderClause(field="name", direction=OrderDirection.ASC),
153+
]
167154
"""
168155
if not v:
169156
return []
157+
170158
if isinstance(v, str):
159+
# 1. from comma-separated string to list of OrderClause
171160
v = v.split(",")
172-
clauses = []
173-
for t in v:
174-
token = t.strip()
175-
if not token:
176-
continue
177-
if token.startswith("-"):
178-
clauses.append(
179-
OrderClause[TField](
180-
field=token[1:].strip(), direction=OrderDirection.DESC
181-
)
182-
)
183-
elif token.startswith("+"):
184-
clauses.append(
185-
OrderClause[TField](
186-
field=token[1:].strip(), direction=OrderDirection.ASC
187-
)
188-
)
189-
else:
190-
clauses.append(
191-
OrderClause[TField](field=token, direction=OrderDirection.ASC)
192-
)
193-
return clauses
161+
clauses: list[tuple[str, OrderDirection]] = []
162+
for t in v:
163+
token = t.strip()
164+
if not token:
165+
continue
166+
if token.startswith("-"):
167+
clauses.append((token[1:].strip(), OrderDirection.DESC))
168+
elif token.startswith("+"):
169+
clauses.append((token[1:].strip(), OrderDirection.ASC))
170+
else:
171+
clauses.append((token, OrderDirection.ASC))
172+
# 2. check for duplicates and conflicting directions
173+
return [
174+
{"field": field, "direction": direction}
175+
for field, direction in check_ordering_list(clauses)
176+
]
177+
178+
# NOTE: Parses ONLY strings into list[OrderClause], otherwise raises TypeError
179+
msg = f"Invalid type for order_by: expected str, got {type(v)}"
180+
raise TypeError(msg)

packages/models-library/tests/test_rest_ordering.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -277,10 +277,9 @@ class TestOrderingParams(OrderingQueryParams[ValidField]):
277277

278278
# Verify the validation error details
279279
exc = err_info.value
280-
assert exc.error_count() > 0
280+
assert exc.error_count() == 1
281281

282-
# Check that the error mentions the invalid field
283-
error_messages = [error["msg"] for error in exc.errors()]
284-
assert any(
285-
"Input should be 'created_at' or 'name'" in msg for msg in error_messages
286-
)
282+
error = exc.errors()[0]
283+
assert error["loc"] == ("order_by", 1, "field")
284+
assert error["type"] == "literal_error"
285+
assert error["input"] == "invalid_field"

0 commit comments

Comments
 (0)