|
| 1 | +"""https://docs.flagsmith.com/managing-flags/updating-flags""" |
| 2 | + |
| 3 | +from typing import get_args |
| 4 | + |
| 5 | +from rest_framework import serializers |
| 6 | + |
| 7 | +from features.feature_states.exceptions import FeatureValueError |
| 8 | +from features.feature_states.models import FeatureValueType |
| 9 | +from features.feature_types import MULTIVARIATE |
| 10 | +from features.future.types import ( |
| 11 | + EnvironmentDefaultRequest, |
| 12 | + FlagValue, |
| 13 | + SegmentOverrideRequest, |
| 14 | + UpdateFlagRequest, |
| 15 | + Variant, |
| 16 | +) |
| 17 | +from features.models import Feature, FeatureStateValue |
| 18 | +from segments.models import Segment |
| 19 | + |
| 20 | + |
| 21 | +class FlagValueSerializer(serializers.Serializer[FlagValue]): |
| 22 | + """A flag value, typed by the caller so it survives the round trip as a string.""" |
| 23 | + |
| 24 | + type = serializers.ChoiceField(choices=get_args(FeatureValueType)) |
| 25 | + value = serializers.CharField(allow_blank=True) |
| 26 | + |
| 27 | + |
| 28 | +class VariantSerializer(serializers.Serializer[Variant]): |
| 29 | + """The share of an environment or segment a multivariate variant is served to.""" |
| 30 | + |
| 31 | + id = serializers.IntegerField() |
| 32 | + weight = serializers.FloatField(min_value=0, max_value=100) |
| 33 | + |
| 34 | + |
| 35 | +class SegmentReferenceSerializer(serializers.Serializer[Segment]): |
| 36 | + """A segment of the feature's project, referenced by an override.""" |
| 37 | + |
| 38 | + id = serializers.IntegerField() |
| 39 | + |
| 40 | + def validate_id(self, id: int) -> int: |
| 41 | + feature: Feature = self.context["feature"] |
| 42 | + if not Segment.live_objects.filter( |
| 43 | + id=id, project_id=feature.project_id |
| 44 | + ).exists(): |
| 45 | + raise serializers.ValidationError("Segment not found.") |
| 46 | + return id |
| 47 | + |
| 48 | + |
| 49 | +class FlagStateSerializer(serializers.Serializer[dict[str, object]]): |
| 50 | + """What a flag serves somewhere in an environment.""" |
| 51 | + |
| 52 | + enabled = serializers.BooleanField(required=False) |
| 53 | + value = FlagValueSerializer(required=False) |
| 54 | + variants = VariantSerializer(many=True, required=False) |
| 55 | + |
| 56 | + def validate_value(self, value: FlagValue) -> FlagValue: |
| 57 | + try: |
| 58 | + FeatureStateValue().set_value(value["value"], value["type"]) |
| 59 | + except FeatureValueError as error: |
| 60 | + raise serializers.ValidationError(str(error)) from error |
| 61 | + return value |
| 62 | + |
| 63 | + def validate_variants(self, variants: list[Variant]) -> list[Variant]: |
| 64 | + feature: Feature = self.context["feature"] |
| 65 | + if feature.type != MULTIVARIATE: |
| 66 | + raise serializers.ValidationError("Feature is not multivariate.") |
| 67 | + known_ids = set(feature.multivariate_options.values_list("id", flat=True)) |
| 68 | + given_ids = {variant["id"] for variant in variants} |
| 69 | + if given_ids - known_ids: |
| 70 | + raise serializers.ValidationError("Variant not found.") |
| 71 | + if known_ids - given_ids: |
| 72 | + raise serializers.ValidationError("Must include all feature's variants.") |
| 73 | + if sum(variant["weight"] for variant in variants) > 100: |
| 74 | + raise serializers.ValidationError("Total weight must not exceed 100.") |
| 75 | + return variants |
| 76 | + |
| 77 | + |
| 78 | +class EnvironmentDefaultSerializer(FlagStateSerializer): |
| 79 | + """What the flag serves to everyone the segment overrides do not match.""" |
| 80 | + |
| 81 | + def validate(self, attrs: EnvironmentDefaultRequest) -> EnvironmentDefaultRequest: |
| 82 | + feature: Feature = self.context["feature"] |
| 83 | + if ( |
| 84 | + self.context["replace"] |
| 85 | + and feature.type == MULTIVARIATE |
| 86 | + and "variants" not in attrs |
| 87 | + ): |
| 88 | + raise serializers.ValidationError( |
| 89 | + {"variants": ["Must include all feature's variants."]} |
| 90 | + ) |
| 91 | + return attrs |
| 92 | + |
| 93 | + |
| 94 | +class SegmentOverrideSerializer(FlagStateSerializer): |
| 95 | + """What the flag serves to the identities a segment matches.""" |
| 96 | + |
| 97 | + segment = SegmentReferenceSerializer() |
| 98 | + priority = serializers.IntegerField(min_value=0, required=False) |
| 99 | + |
| 100 | + |
| 101 | +class UpdateFlagSerializer(serializers.Serializer[UpdateFlagRequest]): |
| 102 | + """The parts of a flag a caller wants to write in one request.""" |
| 103 | + |
| 104 | + environment_default = EnvironmentDefaultSerializer(required=False) |
| 105 | + segment_overrides = SegmentOverrideSerializer(many=True, required=False) |
| 106 | + |
| 107 | + def validate_segment_overrides( |
| 108 | + self, segment_overrides: list[SegmentOverrideRequest] |
| 109 | + ) -> list[SegmentOverrideRequest]: |
| 110 | + seen: set[int] = set() |
| 111 | + for override in segment_overrides: |
| 112 | + segment_id = override["segment"]["id"] |
| 113 | + if segment_id in seen: |
| 114 | + raise serializers.ValidationError(f"Duplicate segment: {segment_id}.") |
| 115 | + seen.add(segment_id) |
| 116 | + return segment_overrides |
0 commit comments