Skip to content
Merged
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
37 changes: 37 additions & 0 deletions frontend/common/utils/__tests__/featureStateToValue.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { featureStateToValue } from 'common/utils/featureStateToValue'

describe('featureStateToValue', () => {
it.each([
['unicode', { string_value: '{"a":1}', type: 'unicode' }, '{"a":1}'],
['bool', { boolean_value: true, type: 'bool' }, true],
['float', { float_value: 1.5, type: 'float' }, 1.5],
['int', { integer_value: 7, type: 'int' }, 7],
])('flattens a nested %s value', (_label, nested, expected) => {
expect(featureStateToValue(nested as never)).toBe(expected)
})

it('reads value_type when present (core traits)', () => {
expect(
featureStateToValue({ integer_value: 3, value_type: 'int' } as never),
).toBe(3)
})

it('normalises a missing int/float to null', () => {
expect(featureStateToValue({ type: 'int' } as never)).toBeNull()
expect(featureStateToValue({ type: 'float' } as never)).toBeNull()
})

it('returns null for null or undefined', () => {
expect(featureStateToValue(null)).toBeNull()
expect(featureStateToValue(undefined)).toBeNull()
})

it.each([
['string', 'already-flat'],
['number', 42],
['boolean false', false],
['empty string', ''],
])('passes an already-flat %s through unchanged', (_label, flat) => {
expect(featureStateToValue(flat)).toBe(flat)
})
})
35 changes: 35 additions & 0 deletions frontend/common/utils/featureStateToValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { FeatureStateValue, FlagsmithValue } from 'common/types/responses'

/**
* Flattens a feature state (or core trait) value into its typed scalar.
*
* Accepts either the nested `{ type, string_value, ... }` shape returned by the
* featurestates endpoint or an already-flat value, and returns the flat value.
*
* Kept in its own module, free of `common/utils` imports, so consumers (and
* their unit tests) don't pull the Flux stores in through `utils.tsx`.
*/
export function featureStateToValue(
value: FlagsmithValue | FeatureStateValue | undefined,
): FlagsmithValue {
if (value === null || value === undefined) {
return null
}
if (typeof value !== 'object') {
return value
}
// `value_type` is the type key on core traits; `type` on feature states.
const type =
(value as { value_type?: FeatureStateValue['type'] }).value_type ??
Comment thread
talissoncosta marked this conversation as resolved.
value.type
Comment thread
Zaimwa9 marked this conversation as resolved.
switch (type) {
case 'bool':
return value.boolean_value
case 'float':
return value.float_value ?? null
case 'int':
return value.integer_value ?? null
default:
return value.string_value
}
}
22 changes: 4 additions & 18 deletions frontend/common/utils/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import Project from 'common/project'
import {
ContentType,
FeatureState,
FeatureStateValue,
FlagsmithValue,
MultivariateFeatureStateValue,
MultivariateOption,
Expand All @@ -22,6 +21,7 @@ import ErrorMessage from 'components/ErrorMessage'
import WarningMessage from 'components/WarningMessage'
import Constants from 'common/constants'
import { getDefaultVariantKey } from './multivariate'
import { featureStateToValue } from './featureStateToValue'
Comment thread
talissoncosta marked this conversation as resolved.
import { defaultFlags } from 'common/stores/default-flags'
import Color from 'color'
import { selectBuildVersion } from 'common/services/useBuildVersion'
Expand Down Expand Up @@ -180,23 +180,9 @@ const Utils = Object.assign({}, BaseUtils, {
}
return null
},
featureStateToValue(featureState: FeatureStateValue) {
if (!featureState) {
return null
}

//@ts-ignore value_type is the type key on core traits
switch (featureState.value_type || featureState.type) {
case 'bool':
return featureState.boolean_value
case 'float':
return featureState.float_value
case 'int':
return featureState.integer_value
default:
return featureState.string_value
}
},
// Delegates to the standalone, Flux-free module so callers that can't import
// this file (e.g. unit-tested hooks) can use the same logic directly.
featureStateToValue,
findOperator(
operator: SegmentCondition['operator'],
value: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,20 +83,12 @@ describe('pickEnvironmentFlag', () => {
expect(pickEnvironmentFlag(undefined, 99)).toBeUndefined()
})

it.each([
['unicode', { string_value: '{"a":1}', type: 'unicode' }, '{"a":1}'],
['bool', { boolean_value: true, type: 'bool' }, true],
['float', { float_value: 1.5, type: 'float' }, 1.5],
['int', { integer_value: 7, type: 'int' }, 7],
['int missing', { type: 'int' }, null],
['no value', undefined, null],
])(
'flattens a nested feature_state_value (%s) to the typed value',
(_label, nested, expected) => {
const results = [make(11, 99, nested)]
expect(pickEnvironmentFlag(results, 99)?.feature_state_value).toBe(
expected,
)
},
)
it('flattens the nested feature_state_value via featureStateToValue', () => {
// Branch coverage lives in featureStateToValue.test.ts; this just proves the
// nested value is flattened rather than passed through as an object.
const results = [make(11, 99, { string_value: '{"a":1}', type: 'unicode' })]
expect(pickEnvironmentFlag(results, 99)?.feature_state_value).toBe(
'{"a":1}',
)
})
})
Original file line number Diff line number Diff line change
@@ -1,33 +1,5 @@
import type {
FeatureState,
FeatureStateValue,
FlagsmithValue,
} from 'common/types/responses'

type FlattenableFeatureStateValue = FlagsmithValue | FeatureStateValue

// The featurestates endpoint returns a nested value; the list path is flat.
// Mirrors Utils.featureStateToValue, inlined to keep the Flux stores out.
function flattenFeatureStateValue(
value: FlattenableFeatureStateValue | undefined,
): FlagsmithValue {
if (value === null || value === undefined) {
return null
}
if (typeof value !== 'object') {
return value
}
switch (value.type) {
case 'bool':
return value.boolean_value
case 'float':
return value.float_value ?? null
case 'int':
return value.integer_value ?? null
default:
return value.string_value
}
}
import { featureStateToValue } from 'common/utils/featureStateToValue'
import type { FeatureState } from 'common/types/responses'

/**
* Decides whether the `?feature=` deep link targets a feature that is NOT on the
Expand Down Expand Up @@ -68,6 +40,6 @@ export function pickEnvironmentFlag(
}
return {
...match,
feature_state_value: flattenFeatureStateValue(match.feature_state_value),
feature_state_value: featureStateToValue(match.feature_state_value),
}
}
Loading