diff --git a/api/api/openapi.py b/api/api/openapi.py index d01a2ffdff54..5dd3849d6871 100644 --- a/api/api/openapi.py +++ b/api/api/openapi.py @@ -12,6 +12,10 @@ from rest_framework.request import Request from typing_extensions import is_typeddict +from environments.identities.traits.constants import ( + TRAIT_STRING_VALUE_MAX_LENGTH, +) + def append_meta(schema: dict[str, Any], meta: dict[str, Any]) -> dict[str, Any]: """ @@ -310,3 +314,26 @@ def postprocessing_assign_tags( result["tags"] = TAGS return result + + +def postprocessing_add_trait_value_max_length( + result: dict[str, Any], generator: Any, **kwargs: Any +) -> dict[str, Any]: + """Document the `trait_value` string length limit enforced by the API. + + `trait_value` schemas are generated from `flagsmith_schemas`, which + types the field as `flag_engine.segments.types.ContextValue` — a plain + `str | int | float | bool | None` union with no length constraint. The + API enforces `TRAIT_STRING_VALUE_MAX_LENGTH` at runtime (see + `environments.identities.traits.fields.TraitValueField`), so patch the + generated schema here to keep the two in sync. + """ + for schema in result.get("components", {}).get("schemas", {}).values(): + trait_value_schema = schema.get("properties", {}).get("trait_value") + if not isinstance(trait_value_schema, dict): + continue + for variant in trait_value_schema.get("anyOf", []): + if variant.get("type") == "string": + variant["maxLength"] = TRAIT_STRING_VALUE_MAX_LENGTH + + return result diff --git a/api/app/settings/common.py b/api/app/settings/common.py index f2768dfc454b..c287b8aa5319 100644 --- a/api/app/settings/common.py +++ b/api/app/settings/common.py @@ -588,6 +588,7 @@ "POSTPROCESSING_HOOKS": [ "drf_spectacular.hooks.postprocess_schema_enums", "api.openapi.postprocessing_assign_tags", + "api.openapi.postprocessing_add_trait_value_max_length", ], "ENUM_NAME_OVERRIDES": { # Overrides to use specific schema names for fields named "type". diff --git a/api/tests/unit/api/test_unit_openapi.py b/api/tests/unit/api/test_unit_openapi.py index 86076be7b280..05d2fdc3a798 100644 --- a/api/tests/unit/api/test_unit_openapi.py +++ b/api/tests/unit/api/test_unit_openapi.py @@ -8,9 +8,11 @@ from api.openapi import ( TAGS, TypedDictSchemaExtension, + postprocessing_add_trait_value_max_length, postprocessing_assign_tags, preprocessing_filter_spec, ) +from environments.identities.traits.constants import TRAIT_STRING_VALUE_MAX_LENGTH def test_typeddict_schema_extension__nested_typed_dict__renders_expected_schema() -> ( @@ -266,3 +268,83 @@ class MyModel(TypedDict): # Then assert name == "MyModel" + + +def test_postprocessing_add_trait_value_max_length__string_variant__adds_max_length() -> ( + None +): + # Given + result: dict[str, Any] = { + "components": { + "schemas": { + "TraitInput": { + "properties": { + "trait_value": { + "anyOf": [ + {"type": "integer"}, + {"type": "number"}, + {"type": "boolean"}, + {"type": "string"}, + {"type": "null"}, + ], + }, + }, + }, + }, + }, + } + + # When + postprocessing_add_trait_value_max_length(result, generator=None) + + # Then + trait_value_schema = result["components"]["schemas"]["TraitInput"]["properties"][ + "trait_value" + ] + assert trait_value_schema["anyOf"] == [ + {"type": "integer"}, + {"type": "number"}, + {"type": "boolean"}, + {"type": "string", "maxLength": TRAIT_STRING_VALUE_MAX_LENGTH}, + {"type": "null"}, + ] + + +def test_postprocessing_add_trait_value_max_length__no_trait_value_property__left_unchanged() -> ( + None +): + # Given + result: dict[str, Any] = { + "components": { + "schemas": { + "Organisation": { + "properties": { + "name": {"type": "string"}, + }, + }, + }, + }, + } + + # When + postprocessing_add_trait_value_max_length(result, generator=None) + + # Then + assert result["components"]["schemas"]["Organisation"] == { + "properties": { + "name": {"type": "string"}, + }, + } + + +def test_postprocessing_add_trait_value_max_length__no_components__returns_result_unchanged() -> ( + None +): + # Given + result: dict[str, Any] = {"paths": {}} + + # When + output = postprocessing_add_trait_value_max_length(result, generator=None) + + # Then + assert output == {"paths": {}} diff --git a/sdk/openapi.yaml b/sdk/openapi.yaml index 2422d7dc3133..82371a88554b 100644 --- a/sdk/openapi.yaml +++ b/sdk/openapi.yaml @@ -449,6 +449,7 @@ components: - type: number - type: boolean - type: string + maxLength: 2000 - type: 'null' title: Trait Value transient: @@ -517,6 +518,7 @@ components: - type: number - type: boolean - type: string + maxLength: 2000 - type: 'null' title: Trait Value required: