Skip to content

Commit 8152bec

Browse files
committed
Allow null as a constant
1 parent 82cf6ac commit 8152bec

File tree

12 files changed

+58
-21
lines changed

12 files changed

+58
-21
lines changed

lib/flipper/expression.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def self.build(object)
2222
args = args.is_a?(Hash) ? [args] : Array(args)
2323

2424
new(name, args.map { |o| build(o) })
25-
when String, Numeric, FalseClass, TrueClass
25+
when String, Numeric, FalseClass, TrueClass, nil
2626
Expression::Constant.new(object)
2727
when Symbol
2828
Expression::Constant.new(object.to_s)

lib/flipper/expressions/percentage.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ module Flipper
22
module Expressions
33
class Percentage
44
def self.call(value)
5-
value.to_f.clamp(0, 100)
5+
value.clamp(0, 100)
66
end
77
end
88
end

lib/flipper/gates/expression.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def open?(context)
3939
end
4040

4141
def protects?(thing)
42-
thing.is_a?(Flipper::Expression) || thing.is_a?(Hash)
42+
thing.is_a?(Flipper::Expression) || thing.is_a?(Flipper::Expression::Constant) || thing.is_a?(Hash)
4343
end
4444

4545
def wrap(thing)

packages/expressions/examples/Equal.json

+8
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@
88
"expression": { "Equal": ["a", "a"] },
99
"result": { "enum": [true] }
1010
},
11+
{
12+
"expression": { "Equal": [null, null] },
13+
"result": { "enum": [true] }
14+
},
15+
{
16+
"expression": { "Equal": [null, false] },
17+
"result": { "enum": [false] }
18+
},
1119
{
1220
"expression": { "Equal": [1, 2] },
1321
"result": { "enum": [false] }

packages/expressions/examples/expressions.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,13 @@
1919
{
2020
"expression": 1.1,
2121
"result": { "enum": [1.1] }
22+
},
23+
{
24+
"expression": null,
25+
"result": { "enum": [null] }
2226
}
2327
],
2428
"invalid": [
25-
null,
2629
{},
2730
[]
2831
]

packages/expressions/lib/constant.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Schema } from './schemas'
55
//
66
// Implements the same interface as Expression
77
export class Constant {
8-
constructor (value, { id = uuidv4(), schema = Schema.resolve('#/definitions/constant') } = {}) {
8+
constructor (value, { id = uuidv4(), schema = Schema.resolve('#') } = {}) {
99
this.value = value
1010
this.id = id
1111
this.schema = schema

packages/expressions/lib/expression.js

+6-10
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,9 @@ import { Constant } from './constant'
33
import { Schema } from './schemas'
44

55
function toArray (arg) {
6-
if (Array.isArray(arg)) {
7-
return arg
8-
} else if (arg === null) {
9-
return []
10-
} else {
11-
return [arg]
12-
}
6+
if (Array.isArray(arg)) return arg
7+
if (arg === null) return []
8+
return [arg]
139
}
1410

1511
// Simple model to transform this: `{ All: [{ Boolean: [true] }]`
@@ -20,14 +16,14 @@ export class Expression {
2016
return expression
2117
}
2218

23-
if (typeof expression === 'object') {
19+
if (['number', 'string', 'boolean'].includes(typeof expression) || expression === null) {
20+
return new Constant(expression, { schema })
21+
} else if (typeof expression === 'object') {
2422
if (Object.keys(expression).length !== 1) {
2523
throw new TypeError(`Invalid expression: ${JSON.stringify(expression)}`)
2624
}
2725
const name = Object.keys(expression)[0]
2826
return new Expression({ name, args: expression[name] })
29-
} else if (['number', 'string', 'boolean'].includes(typeof expression)) {
30-
return new Constant(expression, { schema })
3127
} else {
3228
throw new TypeError(`Invalid expression: ${JSON.stringify(expression)}`)
3329
}

packages/expressions/schemas/schema.json

+3
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
"title": "Boolean",
2525
"type": "boolean",
2626
"enum": [true, false]
27+
},
28+
{
29+
"type": "null"
2730
}
2831
]
2932
},

packages/expressions/test/constant.test.js

+2-3
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { Constant, Schema } from '../lib'
33

44
describe('Constant', () => {
55
describe('schema', () => {
6-
test('defaults to Constant schema', () => {
7-
expect(new Constant('string').schema.title).toEqual('Constant')
6+
test('defaults to expression schema', () => {
7+
expect(new Constant('string').schema.title).toEqual('Expression')
88
})
99

1010
test('uses provided schema', () => {
@@ -26,7 +26,6 @@ describe('Constant', () => {
2626

2727
test('returns false for invalid value', () => {
2828
expect(new Constant(['array']).validate().valid).toBe(false)
29-
expect(new Constant({Now: []}).validate().valid).toBe(false)
3029
})
3130

3231
test('uses provided schema', () => {

packages/expressions/test/expression.test.js

+24-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,24 @@ describe('Expression', () => {
1111
expect(expression.value).toEqual({ All: [true] })
1212
})
1313

14+
test('builds an expression from a boolean constant', () => {
15+
const expression = Expression.build(true)
16+
expect(expression).toBeInstanceOf(Constant)
17+
expect(expression.value).toEqual(true)
18+
})
19+
20+
test('builds an expression from a string constant', () => {
21+
const expression = Expression.build("hello")
22+
expect(expression).toBeInstanceOf(Constant)
23+
expect(expression.value).toEqual("hello")
24+
})
25+
26+
test('builds an expression from a null constant', () => {
27+
const expression = Expression.build(null)
28+
expect(expression).toBeInstanceOf(Constant)
29+
expect(expression.value).toEqual(null)
30+
})
31+
1432
test('throws error on invalid expression', () => {
1533
expect(() => Expression.build([])).toThrowError(TypeError)
1634
expect(() => Expression.build(new Date())).toThrowError(TypeError)
@@ -25,8 +43,13 @@ describe('Expression', () => {
2543
expect(expression.args[1].schema).toEqual(schema.items[1])
2644
})
2745

46+
test('sets schema for constant', () => {
47+
const expression = Expression.build(false)
48+
expect(expression.schema.$id).toEqual(Schema.resolve('#').$id)
49+
})
50+
2851
test('each subexpression uses its own schema', () => {
29-
const expression = Expression.build({ GreaterThan: [ { Now: [] }, { Property: ['released_at'] } ] })
52+
const expression = Expression.build({ GreaterThan: [{ Now: [] }, { Property: ['released_at'] }] })
3053
expect(expression.schema).toEqual(Schema.resolve('GreaterThan.schema.json'))
3154
expect(expression.args[0].schema).toEqual(Schema.resolve('Now.schema.json'))
3255
expect(expression.args[1].schema).toEqual(Schema.resolve('Property.schema.json'))

packages/expressions/test/schemas.test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,12 @@ describe('schema.json', () => {
5555
describe('resolveAnyOf', () => {
5656
test('returns nested anyOf', () => {
5757
const ref = Schema.resolve('#')
58-
expect(ref.resolveAnyOf()).toHaveLength(4)
58+
expect(ref.resolveAnyOf()).toHaveLength(5)
5959
})
6060

6161
test('returns array of schemas', () => {
6262
const ref = Schema.resolve('#/definitions/constant')
63-
expect(ref.resolveAnyOf()).toHaveLength(3)
63+
expect(ref.resolveAnyOf()).toHaveLength(4)
6464
expect(ref.resolveAnyOf()).toEqual(ref.anyOf)
6565
})
6666
})

spec/flipper/gates/expression_spec.rb

+5
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@ def context(expression, properties: {})
8383
expect(subject.protects?(expression)).to be(true)
8484
end
8585

86+
it 'returns true for Flipper::Constant' do
87+
expression = Flipper.boolean(true)
88+
expect(subject.protects?(expression)).to be(true)
89+
end
90+
8691
it 'returns true for Hash' do
8792
expression = Flipper.number(20).eq(20)
8893
expect(subject.protects?(expression.value)).to be(true)

0 commit comments

Comments
 (0)