Skip to content

Commit

Permalink
fix: make $switch expression evaluate case conditions sequentially
Browse files Browse the repository at this point in the history
  • Loading branch information
simonfan committed Apr 26, 2021
1 parent 4b11d2f commit ac970c5
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 15 deletions.
80 changes: 77 additions & 3 deletions src/expressions/logical.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { evaluate } from '../evaluate'
import { evaluate, evaluateAsync } from '../evaluate'
import { interpreterList } from '../interpreter/interpreter'
import { VALUE_EXPRESSIONS } from './value'
import { BOOLEAN_EXPRESSIONS } from './boolean'
Expand All @@ -19,6 +19,8 @@ import {
$asyncEcho,
} from '../../spec/specUtil'

import { InterpreterSpec } from '../types'

const EXP = {
...VALUE_EXPRESSIONS,
...BOOLEAN_EXPRESSIONS,
Expand Down Expand Up @@ -359,8 +361,8 @@ describe('$if', () => {
})

test('then and else expressions should be evaluated only after condition evaluation', () => {
const $expA = jest.fn(() => 'expA-result')
const $expB = jest.fn(() => 'expB-result')
const $expA: InterpreterSpec = jest.fn(() => 'expA-result')
const $expB: InterpreterSpec = jest.fn(() => 'expB-result')

expect(
evaluate(
Expand Down Expand Up @@ -446,6 +448,78 @@ describe('$switch', () => {
[30, $switchExpr, -30],
])
})

test('sync - condition expressions evaluated sequentially', () => {
const $expA: InterpreterSpec = jest.fn(() => false)
const $expB: InterpreterSpec = jest.fn(() => true)
const $expC: InterpreterSpec = jest.fn(() => true)

expect(
evaluate(
{
interpreters: interpreterList({
...EXP,
$expA,
$expB,
$expC,
}),
scope: { $$VALUE: 'any-value' },
},
[
'$switch',
[
[['$expA'], 'VALUE_A'],
[['$expB'], 'VALUE_B'],
[['$expC'], 'VALUE_C'],
],
]
)
).toEqual('VALUE_B')

expect($expA).toHaveBeenCalledTimes(1)
expect($expB).toHaveBeenCalledTimes(1)
expect($expC).not.toHaveBeenCalled()
})

test('async - condition expressions evaluated sequentially', () => {
const $expA: InterpreterSpec = {
sync: null,
async: jest.fn(() => delay(false)),
}
const $expB: InterpreterSpec = {
sync: null,
async: jest.fn(() => delay(true)),
}
const $expC: InterpreterSpec = {
sync: null,
async: jest.fn(() => delay(true)),
}

return evaluateAsync(
{
interpreters: interpreterList({
...EXP,
$expA,
$expB,
$expC,
}),
scope: { $$VALUE: 'any-value' },
},
[
'$switch',
[
[['$expA'], 'VALUE_A'],
[['$expB'], 'VALUE_B'],
[['$expC'], 'VALUE_C'],
],
]
).then((result) => {
expect(result).toEqual('VALUE_B')
expect($expA.async).toHaveBeenCalledTimes(1)
expect($expB.async).toHaveBeenCalledTimes(1)
expect($expC.async).not.toHaveBeenCalled()
})
})
})

describe('$switchKey', () => {
Expand Down
71 changes: 59 additions & 12 deletions src/expressions/logical.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { indefiniteArrayOfType, tupleType, anyType } from '@orioro/typing'
import { evaluate } from '../evaluate'
import { evaluate, evaluateSync } from '../evaluate'
import {
Expression,
EvaluationContext,
PlainObject,
InterpreterSpecSingle,
InterpreterSpec,
} from '../types'
import { _pseudoSymbol } from '../util/misc'

const _UNRESOLVED = _pseudoSymbol()

/**
* @function $and
Expand Down Expand Up @@ -89,23 +93,55 @@ export const $if: InterpreterSpec = [

type Case = [Expression, Expression]

/**
* @function $switch
* @param {Array} cases
* @param {Expression} defaultExp
* @returns {*} result
*/
export const $switch: InterpreterSpec = [
const _switchSync: InterpreterSpecSingle = [
(cases: Case[], defaultExp: Expression, context: EvaluationContext): any => {
const correspondingCase = cases.find(([condition]) => Boolean(condition))
const correspondingCase = cases.find(([condition]) =>
Boolean(evaluateSync(context, condition))
)

return correspondingCase
? evaluate(context, correspondingCase[1])
: evaluate(context, defaultExp)
? evaluateSync(context, correspondingCase[1])
: evaluateSync(context, defaultExp)
},
[
indefiniteArrayOfType(
tupleType([
anyType({ skipEvaluation: true }),
anyType({ skipEvaluation: true }),
])
),
anyType({ skipEvaluation: true }),
],
{
defaultParam: -1,
},
]

const _switchAsync: InterpreterSpecSingle = [
(cases: Case[], defaultExp: Expression, context: EvaluationContext): any =>
cases
.reduce(
(accPromise, [conditionExp, valueExp]) =>
accPromise.then((acc) =>
acc !== _UNRESOLVED
? acc
: Promise.resolve(
evaluate(context, conditionExp)
).then((condition) =>
condition ? evaluate(context, valueExp) : _UNRESOLVED
)
),
Promise.resolve(_UNRESOLVED)
)
.then((result) =>
result === _UNRESOLVED ? evaluate(context, defaultExp) : result
),
[
indefiniteArrayOfType(
tupleType(['any', anyType({ skipEvaluation: true })])
tupleType([
anyType({ skipEvaluation: true }),
anyType({ skipEvaluation: true }),
])
),
anyType({ skipEvaluation: true }),
],
Expand All @@ -114,6 +150,17 @@ export const $switch: InterpreterSpec = [
},
]

/**
* @function $switch
* @param {Array} cases
* @param {Expression} defaultExp
* @returns {*} result
*/
export const $switch: InterpreterSpec = {
sync: _switchSync,
async: _switchAsync,
}

/**
* @function $switchKey
* @param {Cases[]} cases
Expand Down

0 comments on commit ac970c5

Please sign in to comment.