From cb88d2b0628b701fef83e0a0dbd0cae61d5c110f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 22:49:34 +0000 Subject: [PATCH] fix: coerce numeric nutrition values to strings in Cookbook model (fixes #708) Agent-Logs-Url: https://github.com/dylanlangston/nextcloud-mcp-server/sessions/d7163bb6-ad5d-4406-8d11-145c5317ec19 Co-authored-by: dylanlangston <16236219+dylanlangston@users.noreply.github.com> --- nextcloud_mcp_server/models/cookbook.py | 29 +++++++++- tests/unit/test_nutrition_model.py | 77 +++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 tests/unit/test_nutrition_model.py diff --git a/nextcloud_mcp_server/models/cookbook.py b/nextcloud_mcp_server/models/cookbook.py index c058989a..d713358e 100644 --- a/nextcloud_mcp_server/models/cookbook.py +++ b/nextcloud_mcp_server/models/cookbook.py @@ -2,7 +2,7 @@ from typing import List, Optional, Union -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, Field, field_validator from .base import BaseResponse, IdResponse, StatusResponse @@ -40,6 +40,33 @@ class Nutrition(BaseModel): model_config = ConfigDict(populate_by_name=True) + @field_validator( + "calories", + "carbohydrateContent", + "cholesterolContent", + "fatContent", + "fiberContent", + "proteinContent", + "saturatedFatContent", + "servingSize", + "sodiumContent", + "sugarContent", + "transFatContent", + "unsaturatedFatContent", + mode="before", + ) + @classmethod + def coerce_to_str(cls, v: object) -> object: + """Coerce numeric values to strings. + + The schema.org/NutritionInformation spec allows values as either + strings (e.g. '650 kcal') or numbers (e.g. 650). Nextcloud Cookbook + stores whatever the source provided. + """ + if isinstance(v, (int, float)): + return str(v) + return v + class RecipeStub(BaseModel): """Stub of a recipe with basic information.""" diff --git a/tests/unit/test_nutrition_model.py b/tests/unit/test_nutrition_model.py new file mode 100644 index 00000000..e2b7ba01 --- /dev/null +++ b/tests/unit/test_nutrition_model.py @@ -0,0 +1,77 @@ +"""Unit tests for Nutrition model numeric coercion (issue #708).""" + +import pytest + +from nextcloud_mcp_server.models.cookbook import Nutrition + + +@pytest.mark.unit +def test_nutrition_calories_accepts_string(): + """String calories values should be accepted as-is.""" + n = Nutrition(calories="650 kcal") + assert n.calories == "650 kcal" + + +@pytest.mark.unit +def test_nutrition_calories_coerces_int(): + """Integer calories values should be coerced to strings.""" + n = Nutrition(calories=260) + assert n.calories == "260" + + +@pytest.mark.unit +def test_nutrition_calories_coerces_float(): + """Float calories values should be coerced to strings.""" + n = Nutrition(calories=260.5) + assert n.calories == "260.5" + + +@pytest.mark.unit +def test_nutrition_calories_accepts_none(): + """None calories should remain None.""" + n = Nutrition(calories=None) + assert n.calories is None + + +@pytest.mark.unit +def test_nutrition_all_fields_coerce_int(): + """All nutrition content fields should coerce integer values to strings.""" + n = Nutrition( + calories=260, + carbohydrateContent=30, + cholesterolContent=10, + fatContent=15, + fiberContent=5, + proteinContent=20, + saturatedFatContent=3, + servingSize=1, + sodiumContent=500, + sugarContent=8, + transFatContent=0, + unsaturatedFatContent=12, + ) + assert n.calories == "260" + assert n.carbohydrateContent == "30" + assert n.cholesterolContent == "10" + assert n.fatContent == "15" + assert n.fiberContent == "5" + assert n.proteinContent == "20" + assert n.saturatedFatContent == "3" + assert n.servingSize == "1" + assert n.sodiumContent == "500" + assert n.sugarContent == "8" + assert n.transFatContent == "0" + assert n.unsaturatedFatContent == "12" + + +@pytest.mark.unit +def test_nutrition_mixed_types(): + """Nutrition model should handle a mix of string, int, and None values.""" + n = Nutrition( + calories="650 kcal", + proteinContent=18, + fatContent=None, + ) + assert n.calories == "650 kcal" + assert n.proteinContent == "18" + assert n.fatContent is None