|
| 1 | +# Copyright 2026, Optimizely |
| 2 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 3 | +# you may not use this file except in compliance with the License. |
| 4 | +# You may obtain a copy of the License at |
| 5 | +# |
| 6 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 7 | +# |
| 8 | +# Unless required by applicable law or agreed to in writing, software |
| 9 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 10 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 11 | +# See the License for the specific language governing permissions and |
| 12 | +# limitations under the License. |
| 13 | + |
| 14 | +"""Normalization helpers for decision-event ID fields. |
| 15 | +
|
| 16 | +This module provides byte-equivalent, cross-SDK normalization for the |
| 17 | +``campaign_id``, ``variation_id``, and impression ``entity_id`` fields that |
| 18 | +appear in dispatched decision events. |
| 19 | +
|
| 20 | +Rules: |
| 21 | + * ``campaign_id`` and impression ``entity_id`` accept **any non-empty |
| 22 | + string** (numeric like ``"12345"`` or opaque like ``"default-12345"`` / |
| 23 | + ``"layer_abc"``). The fallback to ``experiment_id`` fires ONLY when the |
| 24 | + value is the empty string, ``None``, or missing. Non-string types are |
| 25 | + out of scope for this normalization path (the upstream datafile |
| 26 | + producer delivers string or null values). |
| 27 | + * ``variation_id`` retains the stricter contract: it MUST be a non-empty |
| 28 | + string of decimal digits ``0-9`` (leading zeros allowed). Empty, |
| 29 | + whitespace, non-string, and non-numeric inputs are normalized to |
| 30 | + ``None`` so the wire payload carries an explicit null. |
| 31 | + * ``entity_id`` on impression events shares the campaign_id normalization |
| 32 | + and is therefore byte-equivalent to the normalized campaign_id for the |
| 33 | + same impression. |
| 34 | +
|
| 35 | +The normalization path MUST NOT log, warn, or raise. It must never drop or |
| 36 | +defer event dispatch. |
| 37 | +""" |
| 38 | + |
| 39 | + |
| 40 | +from sys import version_info |
| 41 | +from typing import Any, Optional |
| 42 | + |
| 43 | +if version_info < (3, 10): |
| 44 | + from typing_extensions import TypeGuard |
| 45 | +else: |
| 46 | + from typing import TypeGuard |
| 47 | + |
| 48 | + |
| 49 | +def is_non_empty_string(value: Any) -> TypeGuard[str]: |
| 50 | + """Return ``True`` if ``value`` is a non-empty :class:`str`. |
| 51 | + Any non-empty string is accepted regardless of |
| 52 | + character content (IDs may be opaque, e.g. ``"default-12345"``). |
| 53 | + """ |
| 54 | + return isinstance(value, str) and value != '' |
| 55 | + |
| 56 | + |
| 57 | +def is_numeric_id_string(value: Any) -> TypeGuard[str]: |
| 58 | + """Return ``True`` if ``value`` is a non-empty decimal-digit string. |
| 59 | + Whitespace, signs, decimal points, exponents |
| 60 | + and non-string types all return ``False``. Leading |
| 61 | + zeros are accepted. |
| 62 | + """ |
| 63 | + if not isinstance(value, str): |
| 64 | + return False |
| 65 | + if value == '': |
| 66 | + return False |
| 67 | + return value.isascii() and value.isdigit() |
| 68 | + |
| 69 | + |
| 70 | +def normalize_campaign_id(campaign_id: Any, experiment_id: Any) -> str: |
| 71 | + """Normalize a decision-event ``campaign_id``. |
| 72 | +
|
| 73 | + Returns ``campaign_id`` unchanged when it is a non-empty string (any |
| 74 | + character content — numeric like ``"12345"`` or opaque like |
| 75 | + ``"default-12345"``). Otherwise falls back to ``experiment_id`` (when it |
| 76 | + is itself a non-empty string). If neither is a non-empty string, returns |
| 77 | + an empty string so the event still dispatches. |
| 78 | + """ |
| 79 | + if is_non_empty_string(campaign_id): |
| 80 | + return campaign_id |
| 81 | + if is_non_empty_string(experiment_id): |
| 82 | + return experiment_id |
| 83 | + return '' |
| 84 | + |
| 85 | + |
| 86 | +def normalize_variation_id(variation_id: Any) -> Optional[str]: |
| 87 | + """Normalize a decision-event ``variation_id``. |
| 88 | +
|
| 89 | + Returns the original value if it is a valid numeric ID string. Otherwise |
| 90 | + returns ``None`` so the event payload carries an explicit null for the |
| 91 | + downstream consumer. |
| 92 | + """ |
| 93 | + return variation_id if is_numeric_id_string(variation_id) else None |
0 commit comments