Skip to content
Open
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
27 changes: 27 additions & 0 deletions api/api/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
"""
Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions api/app/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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".
Expand Down
82 changes: 82 additions & 0 deletions api/tests/unit/api/test_unit_openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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() -> (
Expand Down Expand Up @@ -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"},
]
Comment on lines +273 to +310

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Cover both request and response trait schemas.

This test exercises only TraitInput, while the contract also requires Trait.trait_value to be constrained. Add a second matching component, or assert the generated schema, so the traversal cannot regress to updating only the request schema.



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": {}}
2 changes: 2 additions & 0 deletions sdk/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,7 @@ components:
- type: number
- type: boolean
- type: string
maxLength: 2000
- type: 'null'
title: Trait Value
transient:
Expand Down Expand Up @@ -517,6 +518,7 @@ components:
- type: number
- type: boolean
- type: string
maxLength: 2000
- type: 'null'
title: Trait Value
required:
Expand Down
Loading