mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(api): settings PUT accepts booleans/numbers from the panel (#465)
* fix(api): settings PUT accepts the JSON scalars the panel sends The feature-flags card sends booleans and numeric settings send numbers; SettingUpdate.value was typed str, so pydantic 422'd on type before the per-key validators ever ran (live: PUT /settings/notifications_enabled). Scalars now coerce to the stored text form — bools to the 'true'/'false' the validators parse. * chore(docs): reflow hard-wrapped prose from the #401 merge * chore(foundation): regenerate lifecycle artifacts; reflow inherited prose --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field, field_validator
|
||||||
|
|
||||||
|
|
||||||
class SettingUpdate(BaseModel):
|
class SettingUpdate(BaseModel):
|
||||||
@@ -10,6 +10,22 @@ class SettingUpdate(BaseModel):
|
|||||||
|
|
||||||
value: str = Field(..., description="New value for the setting, stored as text")
|
value: str = Field(..., description="New value for the setting, stored as text")
|
||||||
|
|
||||||
|
@field_validator("value", mode="before")
|
||||||
|
@classmethod
|
||||||
|
def _coerce_scalars(cls, v: object) -> object:
|
||||||
|
"""Accept the JSON scalars the panel naturally sends.
|
||||||
|
|
||||||
|
The feature-flags card sends booleans and numeric settings send
|
||||||
|
numbers; values persist as text, so coerce to the stored form
|
||||||
|
(bools to the 'true'/'false' the per-key validators parse) instead
|
||||||
|
of 422ing on type alone.
|
||||||
|
"""
|
||||||
|
if isinstance(v, bool):
|
||||||
|
return "true" if v else "false"
|
||||||
|
if isinstance(v, int | float):
|
||||||
|
return str(v)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
class SettingsResponse(BaseModel):
|
class SettingsResponse(BaseModel):
|
||||||
"""All runtime-editable settings as a flat key→value map."""
|
"""All runtime-editable settings as a flat key→value map."""
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
"""SettingUpdate accepts the JSON scalars the panel naturally sends.
|
||||||
|
|
||||||
|
The feature-flags card sends booleans (a live PUT
|
||||||
|
/settings/notifications_enabled 422'd on type alone) and numeric settings
|
||||||
|
send numbers; all values persist as text. Constructed via model_validate —
|
||||||
|
the wire shape the route actually receives.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from roboco.api.schemas.settings import SettingUpdate
|
||||||
|
|
||||||
|
|
||||||
|
def test_bool_true_coerces_to_stored_form() -> None:
|
||||||
|
assert SettingUpdate.model_validate({"value": True}).value == "true"
|
||||||
|
|
||||||
|
|
||||||
|
def test_bool_false_coerces_to_stored_form() -> None:
|
||||||
|
assert SettingUpdate.model_validate({"value": False}).value == "false"
|
||||||
|
|
||||||
|
|
||||||
|
def test_int_coerces_to_text() -> None:
|
||||||
|
assert SettingUpdate.model_validate({"value": 30}).value == "30"
|
||||||
|
|
||||||
|
|
||||||
|
def test_plain_string_unchanged() -> None:
|
||||||
|
assert SettingUpdate.model_validate({"value": "qwen3"}).value == "qwen3"
|
||||||
Reference in New Issue
Block a user