Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions endpoint_json2/models/endpoint_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ def _json2_is_valid_response_field(self, Model, field_spec):
and sub in self.env[fd.comodel_name]._fields
)

@api.constrains("json2_response_fields", "json2_model_id")
@api.constrains("json2_response_fields", "json2_model_id", "json2_method")
def _check_json2_response_fields(self):
for rec in self:
if not rec.json2_response_fields or not rec.json2_model_name:
Expand All @@ -209,10 +209,22 @@ def _check_json2_response_fields(self):
continue
Model = self.env[rec.json2_model_name]
field_names, _aliases = rec._json2_parse_response_fields()
# Plain names are checked only for these two methods. This is a
# policy choice, not a taxonomy of the ORM: read_group returns model
# field values too and is deliberately excluded, because its rows
# carry keys of its own (__count, __domain) as well. A module wanting
# its own method checked should constrain it where the payload is
# defined, which can pin the exact keys rather than merely "is a
# field of the model".
# Dotted specs stay checkable whatever the method is, because
# _json2_resolve_dotted_fields resolves their base against
# Model._fields before injecting the related values.
reads_fields = rec.json2_method in ("read", "search_read")
invalid = [
f
for f in field_names
if not rec._json2_is_valid_response_field(Model, f)
if ("." in f or reads_fields)
and not rec._json2_is_valid_response_field(Model, f)
]
if invalid:
raise ValidationError(
Expand Down
6 changes: 6 additions & 0 deletions endpoint_json2/readme/CONFIGURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ set to **JSON-2 API**.
country_id.name country
write_date last_modified
```

Works with any method whose rows are objects; rows of another shape (ids,
`(id, name)` pairs, a scalar) pass through untouched. Plain names are validated
against the model only for `read` and `search_read` — for anything else the keys
are the method's own, so a typo is not reported and simply drops that key from
the response. Dotted specs are always validated.
- **Default Domain**: A JSON-formatted domain filter applied to every request
(e.g. `[["active", "=", true]]`).
- **Response Language**: Optionally force a language on the execution context so
Expand Down
46 changes: 46 additions & 0 deletions endpoint_json2/tests/test_endpoint_json2.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,52 @@ def test_invalid_response_fields(self):
with self.assertRaises(ValidationError):
self.endpoint.json2_response_fields = "name\ncountry_id.nonexistent"

def test_plain_names_unvalidated_when_not_a_field_reader(self):
"""Neither is read/search_read, so their declared names go unchecked.

The snippet body is never run by the constraint; it is only there
because an endpoint needs either a method or a snippet.
"""
for name, response_fields, vals in (
("counts", "__count total", {"json2_method": "read_group"}),
("summary", "branch_code", {"json2_code_snippet": "result = []"}),
):
endpoint = self._create_endpoint(
dict(vals, name=f"get_{name}", json2_response_fields=response_fields)
)
self.assertEqual(endpoint.json2_response_fields, response_fields)

def test_dotted_specs_validated_whatever_the_method(self):
"""Their base must resolve against the model, method or not."""
endpoint = self._create_endpoint(
{
"name": "get_counts_dotted",
"json2_method": "read_group",
"json2_response_fields": "__count\ncountry_id.name country",
}
)
bad = ("nonexistent_id.name", "email.something", "country_id.nonexistent")
for spec in bad:
with self.assertRaises(ValidationError):
endpoint.json2_response_fields = f"__count\n{spec}"

def test_plain_names_still_validated_for_search_read(self):
"""__count is not a field of res.partner."""
with self.assertRaises(ValidationError):
self.endpoint.json2_response_fields = "name\n__count"

def test_changing_method_rechecks_response_fields(self):
"""The constraint depends on json2_method, so a switch must re-run it."""
endpoint = self._create_endpoint(
{
"name": "get_switched",
"json2_method": "read_group",
"json2_response_fields": "country_id\n__count",
}
)
with self.assertRaises(ValidationError):
endpoint.json2_method = "search_read"

def test_empty_response_fields(self):
self.endpoint.json2_response_fields = False
fields, aliases = self.endpoint._json2_parse_response_fields()
Expand Down
Loading