diff --git a/endpoint_json2/models/endpoint_endpoint.py b/endpoint_json2/models/endpoint_endpoint.py index 6f14f73..8e4b997 100644 --- a/endpoint_json2/models/endpoint_endpoint.py +++ b/endpoint_json2/models/endpoint_endpoint.py @@ -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: @@ -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( diff --git a/endpoint_json2/readme/CONFIGURE.md b/endpoint_json2/readme/CONFIGURE.md index 792da91..86c5210 100644 --- a/endpoint_json2/readme/CONFIGURE.md +++ b/endpoint_json2/readme/CONFIGURE.md @@ -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 diff --git a/endpoint_json2/tests/test_endpoint_json2.py b/endpoint_json2/tests/test_endpoint_json2.py index 1b028ed..edd0a8a 100644 --- a/endpoint_json2/tests/test_endpoint_json2.py +++ b/endpoint_json2/tests/test_endpoint_json2.py @@ -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()