Comparing version 0.0.0-alpha to 0.0.14-alpha
{ | ||
"name": "rox-base", | ||
"version": "0.0.0-alpha", | ||
"description": "Rollout.io ROX JS SDK Client", | ||
"version": "0.0.14-alpha", | ||
"description": "Rollout.io ROX JS SDK Base", | ||
"author": "Rollout.io <support@rollout.io>", | ||
@@ -32,11 +32,11 @@ "license": "SEE LICENSE IN LICENSE", | ||
"test:watch": "BABEL_ENV=test jest ./src/** --watch", | ||
"prebuild": "npm run clean:dist", | ||
"build": "BABEL_ENV=build webpack --config webpack.config.build.js --progress --colors", | ||
"predeploy": "node ./deploy.js && npm run build", | ||
"prepublishOnly": "npm run test && npm run build", | ||
"clean:dist": "rm -rf dist/*" | ||
}, | ||
"ROX": { | ||
"api_version": "1.3.0" | ||
"api_version": "1.4.0" | ||
}, | ||
"main": "dist/rox-base.js", | ||
"browser": "dist/rox-base.min.js", | ||
"main": "src/index.js", | ||
"module": "src/index.js", | ||
@@ -53,4 +53,8 @@ "dependencies": { | ||
"devDependencies": { | ||
"babel-loader": "^7.1.1", | ||
"babel-plugin-transform-class-properties": "^6.24.1", | ||
"eslint": "^4.3.0", | ||
"jest": "^20.0.4", | ||
"webpack": "^3.3.0" | ||
"uglifyjs-webpack-plugin": "1.0.0-beta.2", | ||
"webpack": "^3.5.5" | ||
}, | ||
@@ -57,0 +61,0 @@ "jest": { |
@@ -7,2 +7,5 @@ ![ROX by Rollout](https://1ko9923xosh2dsbjsxpwqp45-wpengine.netdna-ssl.com/wp-content/themes/rollout/images/rollout_white_logo1.png) | ||
# `Rox-base` | ||
This package is a supporting package for [rox-node](https://www.npmjs.com/package/rox-node) and [rox-browser](https://www.npmjs.com/package/rox-node). Do not use it directly. | ||
## Features | ||
@@ -15,81 +18,2 @@ | ||
* **Remote Conifguraion included:** ROX include a remote configuraiton module that allows developers to defined configuration that can be controlled from the server | ||
## Getting Started | ||
Please see the detailed instructions in our docs how to [add ROX to your project](https://support.rollout.io/docs/installing-the-sdk). | ||
## Documentation | ||
Getting started guide, use cases, examples and videos can be found in [Rollout support site](https://support.rollout.io) | ||
# ROX JS SDK for browser 🦆 | ||
The package is Javascript SDK of [ROX](https://rollout.io) by Rollout.io for integration with web applications. Define and use simple and advanced feature flags, remote configuration variables, static and computed custom properties in code. Control your application via Rollout.io [dashboard](https://app.rollout.io). | ||
## Installation | ||
``` | ||
npm i rox-browser --save | ||
``` | ||
## Usage | ||
Obtain application key by registering at [Rollout.io](https://app.rollout.io). As soon as your app is created, start using the SDK | ||
```javascript | ||
import Rox from '../src'; | ||
const appSettingsContainer = { | ||
shouldShoWelcome: new Rox.Flag(), | ||
textColor: new Rox.Configuration('black') | ||
}; | ||
const onRoxReady = () => { | ||
if (appSettingsContainer.shouldShoWelcome.isEnabled) { | ||
const color = appSettingsContainer.textColor.value | ||
const div = document | ||
.createElement('div') | ||
.textContent('Hello world!') | ||
.setAttribute('style', `color: ${color}`); | ||
const body = document.getElementsByTagName('body')[0] | ||
body.appendChild(div) | ||
} | ||
}; | ||
Rox.register('settingsNamespace', appSettingsContainer); | ||
Rox.setup('<app key', { | ||
syncCompletionHandler: onRoxReady | ||
}); | ||
``` | ||
## Configure the SDK with different options | ||
Override the default configuration by prodiving predefined configuration preset for `Rox.setup`. | ||
For example: | ||
```javascript | ||
import Rox from '../src'; | ||
/* | ||
- roxOptions.distinctId | ||
- roxOptions.version | ||
- roxOptions.syncComplitionHandler | ||
*/ | ||
const roxOptions = { | ||
distinctId: 'sessionDistinctId', | ||
version: '1.0', | ||
syncCompletionHandler: () => {} | ||
}; | ||
Rox.setup('appKey', roxOptions); | ||
``` | ||
## Activating Manual Overrides UI | ||
You can overide flags manually using a built-in UI debug utility that can be activated by invoking `Rox.showOverrides()`. | ||
Running it will display an UI element that allows to explicitly set values of flags. | ||
Possible option is the location of the UI element: | ||
- `'top left'` | ||
- `'top right'` | ||
- `'bottom left'` | ||
- `'bottom right' (default)` | ||
## LICENSE | ||
@@ -96,0 +20,0 @@ |
@@ -0,2 +1,4 @@ | ||
import * as Manager from './Manager'; | ||
const cloneDeep = require('lodash.clonedeep'); | ||
export function getMergedContext(context, contextToBeMerged) { | ||
@@ -6,2 +8,9 @@ if (!context) return cloneDeep(contextToBeMerged); | ||
return cloneDeep(mergedContext); | ||
} | ||
export function getMergedContextWithGlobal(contextToBeMerged) { | ||
let globalContext = Manager.getContext(); | ||
if (!contextToBeMerged) return globalContext; | ||
const mergedContext = Object.assign({}, globalContext, contextToBeMerged); | ||
return cloneDeep(mergedContext); | ||
} |
@@ -0,9 +1,15 @@ | ||
import { RoxxParser } from '../parsers'; | ||
import * as Context from '../context'; | ||
const parser = new RoxxParser(); | ||
const types = ['string', 'boolean', 'number']; | ||
export default class RoxConfiguration { | ||
constructor (defaultValue) { | ||
constructor(defaultValue) { | ||
let valueType = typeof defaultValue; | ||
this._enitityType = 'configuration' | ||
this._entityType = 'configuration'; | ||
if (types.indexOf(valueType) === -1) { | ||
throw new Error(`RoxConfiguration initialized with wrong type '${valueType}'`); | ||
throw new Error( | ||
`RoxConfiguration initialized with wrong type '${valueType}'` | ||
); | ||
} | ||
@@ -15,3 +21,3 @@ | ||
enumerable: false | ||
}) | ||
}); | ||
@@ -22,3 +28,3 @@ Object.defineProperty(this, '_value', { | ||
enumerable: false | ||
}) | ||
}); | ||
@@ -29,3 +35,3 @@ Object.defineProperty(this, '_type', { | ||
enumerable: false | ||
}) | ||
}); | ||
@@ -36,3 +42,3 @@ Object.defineProperty(this, '_externalType', { | ||
enumerable: false | ||
}) | ||
}); | ||
@@ -43,55 +49,29 @@ Object.defineProperty(this, '_name', { | ||
enumerable: false | ||
}) | ||
Object.defineProperty(this, '_freezeValue', { | ||
value: undefined, | ||
writable: true, | ||
enumerable: false | ||
}) | ||
Object.defineProperty(this, '_freezed', { | ||
value: false, | ||
writable: true, | ||
enumerable: false | ||
}) | ||
}); | ||
} | ||
unfreeze () { | ||
this._freezed = false; | ||
} | ||
get name () { | ||
get name() { | ||
return this._name; | ||
} | ||
get type () { | ||
get type() { | ||
return this._type; | ||
} | ||
get value () { | ||
if (!this._freezed) { | ||
this._freezed = true; | ||
this._freezeValue = this._value; | ||
} | ||
return this._freezeValue; | ||
} | ||
get externalType () { | ||
get externalType() { | ||
return this._externalType; | ||
} | ||
get defaultValue () { | ||
get defaultValue() { | ||
return this._defaultValue; | ||
} | ||
set value (newValue) { | ||
if (this._type !== typeof newValue) { | ||
this._value = this._defaultValue; | ||
return; | ||
getValue(context = {}) { | ||
if (this.condition) { | ||
let mergedContext = Context.Actions.getMergedContextWithGlobal(context); | ||
let value = parser.evaluateExpression(this.condition, mergedContext); | ||
value ? this._value = value : this._value = this.defaultValue; | ||
} | ||
this._value = newValue; | ||
return this._value; | ||
} | ||
} |
@@ -1,25 +0,32 @@ | ||
export default class CustomProperty { | ||
_validate(name, type, value) { | ||
if (value && value.constructor !== type && value.constructor !== Function) { | ||
throw new Error(`Custom property initialized with an invalid type / value combination. (Type: ${type}, Value: ${value})`); | ||
} | ||
const _validateName = function (name) { | ||
if (typeof name === 'undefined' || name === '') { | ||
throw new Error('Custom property must be initialized with a name.'); | ||
} | ||
} | ||
if (typeof name === 'undefined') { | ||
throw new Error('Custom property must be initialized with a name.'); | ||
} | ||
const _validateExplicitValue = function (type, value) { | ||
if (value && value.constructor !== type && value.constructor !== Function) { | ||
throw new Error(`Custom property initialized with an invalid type / value combination. (Type: ${type}, Value: ${value})`); | ||
} | ||
} | ||
const _validateDynamicValue = function (value) { | ||
const argsExpected = value.length; | ||
if (argsExpected != 1) { | ||
throw new Error('Dynamic value of a custom property should be a function with exactly 1 argument'); | ||
} | ||
} | ||
export default class CustomProperty { | ||
constructor(name, type, value) { | ||
this._validate(name, type, value); | ||
_validateName(name) | ||
this._name = name; | ||
this._type = type; | ||
if (typeof value !== 'function') { | ||
this._value = () => { | ||
return value; | ||
} | ||
} | ||
else { | ||
if (typeof value === 'function') { | ||
_validateDynamicValue(value); | ||
this._value = value; | ||
} else { | ||
_validateExplicitValue(type, value); | ||
this._value = () => value; | ||
} | ||
this._type = type; | ||
} | ||
@@ -39,2 +46,6 @@ | ||
getValue(context = {}) { | ||
return this._value(context) | ||
} | ||
get value() { | ||
@@ -41,0 +52,0 @@ return this._value(); |
@@ -6,4 +6,3 @@ export { default as CustomProperty } from './CustomProperty' | ||
export { default as Configuration } from './Configuration' | ||
export { default as ConfigurationModel } from './ConfigurationModel' | ||
export { default as TargetGroup } from './TargetGroup' | ||
export { default as Variant } from './Variant' |
@@ -5,14 +5,16 @@ import { hasOverride, getOverride } from '../lib/Overrider'; | ||
constructor(defaultValue, options, name) { | ||
this._entityType = 'variant' | ||
this._options = [...options] | ||
this._entityType = 'variant'; | ||
this._options = [...options]; | ||
this._validate(defaultValue); | ||
this._name = name; | ||
this._value = this._defaultValue = defaultValue; | ||
this._freezeValue = undefined; | ||
this._type = 'string'; | ||
this._freezed = false; | ||
this._frozen = false; | ||
} | ||
_validate(defaultValue) { | ||
const optionsError = new Error(`RoxVariant options must be a non-empty array of strings. Received '${this._options}'`) | ||
const optionsError = new Error( | ||
`RoxVariant options must be a non-empty array of strings. Received '${this | ||
._options}'` | ||
); | ||
@@ -56,3 +58,3 @@ if (typeof defaultValue !== 'string') { | ||
getNameDetails () { | ||
getNameDetails() { | ||
if (!this.name) return; | ||
@@ -63,3 +65,3 @@ const els = this.name.split('.'); | ||
name: els.join('.') | ||
} | ||
}; | ||
} | ||
@@ -75,5 +77,5 @@ | ||
overridingValue: this.overridenValue, | ||
value: this.value | ||
value: this.getValue() | ||
}; | ||
} | ||
} |
@@ -6,2 +6,3 @@ import * as Setters from './setters'; | ||
import * as lib from './lib'; | ||
import * as Context from './context'; | ||
import Config from './config'; | ||
@@ -14,3 +15,4 @@ | ||
Setters, | ||
Repositories | ||
Repositories, | ||
Context | ||
}) |
@@ -1,10 +0,17 @@ | ||
// import Rox from './' | ||
import RoxBase from './' | ||
// describe('exports', () => { | ||
// it('exports all', () => { | ||
// expect(Rox).not.toEqual(undefined) | ||
// expect(Rox.Flag).not.toEqual(undefined) | ||
// expect(Rox.Configuration).not.toEqual(undefined) | ||
// expect(Rox.Variant).not.toEqual(undefined) | ||
// }) | ||
// }) | ||
describe('exports', () => { | ||
it('exports all from rox base', () => { | ||
expect(RoxBase).not.toEqual(undefined) | ||
expect(RoxBase.Config).not.toEqual(undefined) | ||
expect(RoxBase.Parsers).not.toEqual(undefined) | ||
expect(RoxBase.Setters).not.toEqual(undefined) | ||
expect(RoxBase.Repositories).not.toEqual(undefined) | ||
expect(RoxBase.Overrider).not.toEqual(undefined) | ||
expect(RoxBase.configurationFetcher).not.toEqual(undefined) | ||
expect(RoxBase.RuntimeRegistry).not.toEqual(undefined) | ||
expect(RoxBase.ClassRegister).not.toEqual(undefined) | ||
expect(RoxBase.DeviceProperties).not.toEqual(undefined) | ||
expect(RoxBase.RoxLogger).not.toEqual(undefined) | ||
}) | ||
}) |
import { buildRequestConfiguration } from '../RequestConfigurationBuilder'; | ||
jest.mock('../../drivers', () => ({ | ||
DeviceProperties: { | ||
getProperties: () => ({}) | ||
} | ||
})); | ||
describe('RemoteConfigurationBuilder', () => { | ||
describe('buildRequestConfiguration()', () => { | ||
it('builds empty arrays', () => { | ||
const registry = { | ||
customProperties: [], | ||
featureFlags: [], | ||
remoteConfiguration: [] | ||
const deviceProperties = { | ||
getProperties: () => ({ | ||
app_key: 'apiKey', | ||
customProperties: [], | ||
featureFlags: [], | ||
remoteConfiguration: [] | ||
}) | ||
}; | ||
expect( | ||
buildRequestConfiguration({ appKey: 'apiKey', registry }) | ||
buildRequestConfiguration({ appKey: 'apiKey', deviceProperties }) | ||
).toMatchObject({ | ||
@@ -20,0 +17,0 @@ feature_flags: '[]', |
@@ -1,6 +0,3 @@ | ||
import RoxClassRegister from '../RoxClassRegister'; | ||
import RoxFlag from '../../experiments/RoxFlag'; | ||
import {Variant as RoxVariant} from '../../drivers'; | ||
import RoxConfiguration from '../../remoteConfigurations/RoxConfiguration' | ||
import ClassRegister from '../ClassRegister'; | ||
import Configuration from '../../entities/Configuration' | ||
import roxConfigurationRepository from '../../repositories/RoxConfigurationRepository' | ||
@@ -12,3 +9,3 @@ import roxFlagRepository from '../../repositories/RoxFlagRepository' | ||
} | ||
describe('RoxClassRegister', () => { | ||
describe('ClassRegister', () => { | ||
let context; | ||
@@ -18,3 +15,3 @@ beforeEach(() => { | ||
let flagRepository = new roxFlagRepository.constructor() | ||
let register = new RoxClassRegister({ | ||
let register = new ClassRegister({ | ||
configurationRepository, | ||
@@ -30,3 +27,3 @@ flagRepository | ||
it('adds the configuration that is defined in container to the remoteConfigruationRepository ', () => { | ||
let container = { myFlag : new RoxConfiguration('100') }; | ||
let container = { myFlag : new Configuration('100') }; | ||
@@ -39,40 +36,40 @@ context.register.handleContainer('', container); | ||
it('adds the Variant that is defined in container to the flagRegistry ', () => { | ||
let container = { myFlag : new RoxVariant('100', ['100']) }; | ||
// it('adds the Variant that is defined in container to the flagRegistry ', () => { | ||
// let container = { myFlag : {defaultValue: '100', _options:['100'] }}; | ||
context.register.handleContainer('', container); | ||
// context.register.handleContainer('', container); | ||
expect(context.flagRepository.flagWithName('myFlag')).toBe(container.myFlag); | ||
expectIteratorLengthOf(context.flagRepository.items).toBe(1) | ||
}) | ||
// expect(context.flagRepository.flagWithName('myFlag')).toBe(container.myFlag); | ||
// expectIteratorLengthOf(context.flagRepository.items).toBe(1) | ||
// }) | ||
it('adds the same flag that is defined in container to the flagRegistry ', () => { | ||
let container = { myFlag : new RoxFlag() }; | ||
// it('adds the same flag that is defined in container to the flagRegistry ', () => { | ||
// let container = { myFlag : {} }; | ||
context.register.handleContainer('', container); | ||
// context.register.handleContainer('', container); | ||
expect(context.flagRepository.flagWithName('myFlag')).toBe(container.myFlag); | ||
expectIteratorLengthOf(context.flagRepository.items).toBe(1) | ||
}) | ||
// expect(context.flagRepository.flagWithName('myFlag')).toBe(container.myFlag); | ||
// expectIteratorLengthOf(context.flagRepository.items).toBe(1) | ||
// }) | ||
it('do not register props that are not RoxFlage', () => { | ||
let container = { | ||
notFlag : '' | ||
}; | ||
// it('do not register props that are not RoxFlage', () => { | ||
// let container = { | ||
// notFlag : '' | ||
// }; | ||
context.register.handleContainer('', container); | ||
// context.register.handleContainer('', container); | ||
expectIteratorLengthOf(context.flagRepository.items).toBe(0) | ||
expectIteratorLengthOf(context.configurationRepository.items).toBe(0) | ||
}) | ||
// expectIteratorLengthOf(context.flagRepository.items).toBe(0) | ||
// expectIteratorLengthOf(context.configurationRepository.items).toBe(0) | ||
// }) | ||
it('register only its own properties', () => { | ||
let ContainerBase = function(){}; | ||
ContainerBase.prototype = { myFlag: {}} | ||
let container = new ContainerBase(); | ||
context.register.handleContainer('', container); | ||
// it('register only its own properties', () => { | ||
// let ContainerBase = function(){}; | ||
// ContainerBase.prototype = { myFlag: {}} | ||
// let container = new ContainerBase(); | ||
// context.register.handleContainer('', container); | ||
expectIteratorLengthOf(context.flagRepository.items).toBe(0) | ||
expectIteratorLengthOf(context.configurationRepository.items).toBe(0) | ||
}) | ||
// expectIteratorLengthOf(context.flagRepository.items).toBe(0) | ||
// expectIteratorLengthOf(context.configurationRepository.items).toBe(0) | ||
// }) | ||
}) |
@@ -1,14 +0,12 @@ | ||
import { RoxClient } from '../RoxClient'; | ||
import RoxFlag from '../../experiments/RoxFlag'; | ||
import { Client } from '../Client'; | ||
jest.mock('../RoxCache', () => ({ | ||
set: jest.fn(), | ||
get: jest.fn() | ||
})); | ||
describe('RoxClient', () => { | ||
it('RoxClient setGetContainer', () => { | ||
let registerContainer = {'duck': new RoxFlag()}; | ||
describe('Client', () => { | ||
it('Client setGetContainer', () => { | ||
let registerContainer = {'duck': { | ||
_options: [ 'false', 'true' ], | ||
_name: 'duck', | ||
_entityType: 'flag', | ||
_defaultValue: 'false'}}; | ||
const mockApi = '1234' | ||
const client = new RoxClient(mockApi); | ||
const client = new Client(mockApi); | ||
client.register('duffy', registerContainer); | ||
@@ -15,0 +13,0 @@ let container = client.getContainer('duffy'); |
@@ -9,3 +9,3 @@ import RoxCache from '../RoxCache'; | ||
clearAllOverrides | ||
} from '../RoxOverrider'; | ||
} from '../Overrider'; | ||
@@ -12,0 +12,0 @@ const getRandomString = () => { |
@@ -1,11 +0,9 @@ | ||
import RoxRuntimeRegistry from '../RoxRuntimeRegistry' | ||
import RoxRuntimeRegistry from '../RuntimeRegistry' | ||
import RoxFlagRepository from '../../repositories/RoxFlagRepository'; | ||
import RoxFlag from '../../experiments/RoxFlag' | ||
import {Variant as RoxVariant} from '../../drivers/' | ||
import RoxRemoteConfiguration from '../../remoteConfigurations/RoxConfiguration' | ||
import RoxRemoteConfiguration from '../../entities/Configuration' | ||
import RoxConfigurationRepository from '../../repositories/RoxConfigurationRepository' | ||
let flag = new RoxFlag(); | ||
let flag2 = new RoxFlag(); | ||
let variant = new RoxVariant('red', ['blue']); | ||
let flag = {defaultValue : 'false', _options : ['false', 'true']}; | ||
let flag2 = {defaultValue : 'false', _options : ['false', 'true']}; | ||
let variant = {defaultValue : 'red', _options : ['blue', 'red']}; | ||
let remoteConfiguration1 = new RoxRemoteConfiguration(true); | ||
@@ -12,0 +10,0 @@ |
import { fetchRemoteConfiguration } from './RequestConfiguration'; | ||
import { buildRequestConfiguration } from './RequestConfigurationBuilder'; | ||
import { ConfigurationParser, RoxxParser } from '../parsers'; | ||
import { ConfigurationParser } from '../parsers'; | ||
import { ConfigurationSetter, FlagsSetter } from '../setters'; | ||
@@ -12,2 +12,3 @@ | ||
const CACHE_TTL = Config.get('CLIENT_DATA_CACHE_TTL_MIN'); | ||
let isdispatchPeriodicallyRuning = false; | ||
@@ -19,10 +20,9 @@ import TargetGroupRepository from '../repositories/TargetGroupRepository'; | ||
class ConfigurationFetcher { | ||
get hasCachedConfiguration () { | ||
get hasCachedConfiguration() { | ||
const cached = Cache.get(CACHE_KEY); | ||
if (cached) | ||
return true; | ||
if (cached) return true; | ||
return false; | ||
} | ||
runHandler (handler) { | ||
runHandler(handler) { | ||
if (handler instanceof Function) { | ||
@@ -33,3 +33,4 @@ handler(); | ||
dispatch ({appKey, deviceProperties, handler, options}) { | ||
dispatch({ appKey, deviceProperties, handler, options }) { | ||
RoxLogger.debug('Dispatch'); | ||
var useCache = options.useCache; | ||
@@ -40,20 +41,37 @@ var shouldProcess = options.shouldProcess; | ||
const networkPromise = this.fetchFromNetwork({appKey, deviceProperties}) | ||
.then(response => { | ||
this.storeInCache(response) | ||
if (shouldProcess) { | ||
return this.process(response).then(() => this.runHandler(handler)) | ||
} else { | ||
this.runHandler() | ||
} | ||
}); | ||
const networkPromise = this.fetchFromNetwork({ | ||
appKey, | ||
deviceProperties | ||
}).then(response => { | ||
this.storeInCache(response); | ||
if (shouldProcess) { | ||
return this.process(response).then(() => this.runHandler(handler)); | ||
} else { | ||
this.runHandler(); | ||
} | ||
}); | ||
return Promise.all([ | ||
cached, | ||
networkPromise | ||
]).catch(() => {}) | ||
return Promise.all([cached, networkPromise]).catch(() => {}); | ||
} | ||
fetchFromNetwork ({appKey, deviceProperties }) { | ||
const rc = buildRequestConfiguration({ appKey, deviceProperties}) | ||
dispatchPeriodically({ appKey, deviceProperties, periodTimeInSec }) { | ||
if (isdispatchPeriodicallyRuning) { | ||
RoxLogger.debug('Dispatch Periodically already running'); | ||
return Promise.resolve(); | ||
} | ||
isdispatchPeriodicallyRuning = true; | ||
RoxLogger.debug('Dispatch Periodically'); | ||
setInterval(() => { | ||
this.fetchFromNetwork({ | ||
appKey, | ||
deviceProperties | ||
}).then(response => this.process(response)); | ||
}, periodTimeInSec * 1000); | ||
} | ||
fetchFromNetwork({ appKey, deviceProperties }) { | ||
RoxLogger.debug(`fetch from network for appKey ${appKey}`); | ||
const rc = buildRequestConfiguration({ appKey, deviceProperties }); | ||
RoxLogger.debug(rc); | ||
@@ -63,8 +81,13 @@ return fetchRemoteConfiguration(rc); | ||
fetchFromCache () { | ||
fetchFromCache() { | ||
RoxLogger.debug('fetch From Cache'); | ||
const cached = Cache.get(CACHE_KEY); | ||
let parsed; | ||
if (cached) { | ||
try { parsed = JSON.parse(cached); } catch (e) { | ||
RoxLogger.warn(`Configuration retreived from cache, but has corrupted. Aborting. (Error: ${e})`); | ||
try { | ||
parsed = JSON.parse(cached); | ||
} catch (e) { | ||
RoxLogger.warn( | ||
`Configuration retreived from cache, but has corrupted. Aborting. (Error: ${e})` | ||
); | ||
} | ||
@@ -78,3 +101,3 @@ if (parsed && parsed.constructor === Object) { | ||
storeInCache (response) { | ||
storeInCache(response) { | ||
RoxLogger.debug(`Store in cache response = ${JSON.stringify(response)}`); | ||
@@ -84,14 +107,18 @@ Cache.set(CACHE_KEY, JSON.stringify(response), CACHE_TTL); | ||
process (payload) { | ||
process(payload) { | ||
if (payload) { | ||
this.calculatePayload(this.parsePayload(payload)); | ||
return Promise.resolve() | ||
return Promise.resolve(); | ||
} | ||
return Promise.reject() | ||
return Promise.reject(); | ||
} | ||
parsePayload (response) { | ||
parsePayload(response) { | ||
const parser = new ConfigurationParser(response, this.app_key); | ||
if (!parser.parse()) { | ||
RoxLogger.debug(`failed to parse payload. response = ${JSON.stringify(response)} deviceProps = ${this.deviceProps} app_key = ${this.app_key}`) | ||
RoxLogger.debug( | ||
`failed to parse payload. response = ${JSON.stringify( | ||
response | ||
)} deviceProps = ${this.deviceProps} app_key = ${this.app_key}` | ||
); | ||
} | ||
@@ -101,8 +128,12 @@ return parser; | ||
calculatePayload (parser) { | ||
calculatePayload(parser) { | ||
TargetGroupRepository.setTargetGroups(parser.targetGroups()); | ||
ExperimentsRepository.setExperiments(parser.experiments()); | ||
new ConfigurationSetter(new RoxxParser()).setWithConfigurations(parser.remoteConfigurations()); | ||
new FlagsSetter(new RoxxParser()).setWithExperiments(parser.experiments()); | ||
new ConfigurationSetter().prepareConfigurations( | ||
parser.remoteConfigurations() | ||
); | ||
new FlagsSetter().prepareFlagsWithExperiments( | ||
parser.experiments() | ||
); | ||
@@ -113,2 +144,2 @@ return; | ||
export default new ConfigurationFetcher() | ||
export default new ConfigurationFetcher(); |
@@ -8,6 +8,8 @@ import Cache from '../lib/RoxCache'; | ||
this.app_release = '0.0'; | ||
this.distinctIdSetExplicitly = false; | ||
} | ||
setDistinctId (id) { | ||
this.distinct_id = id | ||
this.distinctIdSetExplicitly = true; | ||
this.distinct_id = id; | ||
} | ||
@@ -14,0 +16,0 @@ |
@@ -7,2 +7,3 @@ import * as Overrider from './Overrider'; | ||
export { default as RoxLogger } from './RoxLogger'; | ||
export { default as createRoxClient } from './Client'; | ||
export { Overrider }; |
@@ -32,5 +32,5 @@ import { get } from 'axios'; | ||
.catch(err => { | ||
RoxLogger.error(`failed in fetch remoteConfiguration ${err}`); | ||
RoxLogger.error(`Unable to fetch ROX configuration ${err}`); | ||
return Promise.reject(new Error('Unable to fetch ROX configuration')); | ||
}); | ||
} |
@@ -7,3 +7,4 @@ import Config from '../config'; | ||
export const buildRequestConfiguration = function ({appKey, deviceProperties: properties }) { | ||
export const buildRequestConfiguration = function ({appKey, deviceProperties }) { | ||
const properties = deviceProperties.getProperties(); | ||
@@ -10,0 +11,0 @@ properties.app_key = appKey; |
@@ -8,2 +8,3 @@ import Logger from 'loglevel'; | ||
logger.setLevel('debug'); | ||
logger.debug = logger.info; | ||
logger.debug('Active verbose mode'); | ||
@@ -10,0 +11,0 @@ } else { |
import RoxxParser from '../RoxxParser'; | ||
const parser = new RoxxParser(); | ||
const parser = new RoxxParser | ||
@@ -77,3 +77,3 @@ describe('RoxxParser', () => { | ||
it('simple ifThen', () => { | ||
// expect(parser.evaluateExpression(' | ||
expect(parser.evaluateExpression('ifThen(true, "blue", undefined)')).toEqual('blue') | ||
@@ -102,2 +102,22 @@ expect(parser.evaluateExpression('ifThen(and(true, or(true, true)), "AB", "CD")')).toEqual('AB'); | ||
}); | ||
it('includes evaluation', () => { | ||
expect(parser.evaluateExpression('inArray("123", ["222", "233"])')).toEqual(false); | ||
expect(parser.evaluateExpression('inArray("123", ["123", "233"])')).toEqual(true); | ||
expect(parser.evaluateExpression('inArray("123", [123, "233"])')).toEqual(false); | ||
expect(parser.evaluateExpression('inArray("123", [123, "123", "233"])')).toEqual(true); | ||
expect(parser.evaluateExpression('inArray(123, [123, "233"])')).toEqual(true); | ||
expect(parser.evaluateExpression('inArray(123, ["123", "233"])')).toEqual(false); | ||
expect(parser.evaluateExpression('inArray("123", [])')).toEqual(false); | ||
expect(parser.evaluateExpression('inArray("1 [23", ["1 [23", "]")')).toEqual(true); | ||
expect(parser.evaluateExpression('inArray("123", undefined)')).toEqual(false); | ||
expect(parser.evaluateExpression('inArray(undefined, [])')).toEqual(false); | ||
expect(parser.evaluateExpression('inArray(undefined, [undefined, 123])')).toEqual(true); | ||
expect(parser.evaluateExpression('inArray(undefined, undefined)')).toEqual(false); | ||
expect(parser.evaluateExpression('inArray(mergeSeed("123", "456"), ["123.456", "233"])')).toEqual(true); | ||
expect(parser.evaluateExpression('inArray("123.456", [mergeSeed("123", "456"), "233"])')).toEqual(false); // THIS CASE IS NOT SUPPORTED | ||
}); | ||
}); |
import ExperimentsParser from './ExperimentsParser' | ||
import TargetGroupsParser from './TargetGroupsParser' | ||
import RemoteConfigurationsParser from './RemoteConfigurationsParser' | ||
import remoteConfigurationsParser from './RemoteConfigurationsParser' | ||
@@ -14,8 +14,6 @@ const terms = { | ||
export default class ConfigurationParser { | ||
constructor(json, appKey) { | ||
constructor(json) { | ||
if (!json || typeof json !== 'object') { | ||
throw new Error(`ConfigurationParser should be constructed with JSON object. Recieved ${json}`); | ||
} | ||
this._appKey = appKey; | ||
this._json = json; | ||
@@ -58,4 +56,4 @@ } | ||
_parseRemoteConfigurations(configurations) { | ||
this._configurations = new RemoteConfigurationsParser(configurations).parse(); | ||
this._configurations = remoteConfigurationsParser.parse(configurations); | ||
} | ||
} |
@@ -1,2 +0,1 @@ | ||
import { ConfigurationModel as RoxConfigurationModel } from '../entities/' | ||
@@ -6,29 +5,10 @@ const terms = { | ||
REMOTE_CONFIG_TYPE: 'type', | ||
REMOTE_CONFIG_CONDITIONS: 'conditions', | ||
} | ||
REMOTE_CONFIG_CONDITIONS: 'conditions' | ||
}; | ||
export default class RemoteConfigurationsParser { | ||
constructor(json) { | ||
this._json = json; | ||
} | ||
const validateConfig = i => i[terms.REMOTE_CONFIG_NAME] && | ||
i[terms.REMOTE_CONFIG_TYPE] && | ||
typeof i[terms.REMOTE_CONFIG_CONDITIONS] !== 'undefined' | ||
parse() { | ||
if (!this._json || !this._json.length) { | ||
return []; | ||
} | ||
let configurations = []; | ||
this._json.forEach(configurationJson => { | ||
if (!configurationJson || !configurationJson[terms.REMOTE_CONFIG_NAME] || !configurationJson[terms.REMOTE_CONFIG_TYPE] || | ||
!configurationJson[terms.REMOTE_CONFIG_CONDITIONS]) { | ||
return; | ||
} | ||
configurations.push(new RoxConfigurationModel(configurationJson[terms.REMOTE_CONFIG_NAME], | ||
configurationJson[terms.REMOTE_CONFIG_TYPE], configurationJson[terms.REMOTE_CONFIG_CONDITIONS])); | ||
}) | ||
return configurations; | ||
} | ||
} | ||
export default { | ||
parse: (json = []) => json.filter(validateConfig)} |
@@ -1,287 +0,105 @@ | ||
import md5 from 'md5'; | ||
import TargetGroupRepository from '../repositories/TargetGroupRepository'; | ||
import CustomPropertyRepository from '../repositories/CustomPropertyRepository'; | ||
import * as RoxxOperatorsMap from '../lib/RoxxOperators'; | ||
import RoxLogger from '../lib/RoxLogger'; | ||
import { RoxxTokenizer, RoxxTokenTypeRand, RoxxTokenTypeRator } from './RoxxTokenizer'; | ||
import LRUCache from 'lru-cache'; | ||
const defaultCache = LRUCache(); | ||
function __(name, f){ | ||
return function() { | ||
let v = f.apply(null, Array.prototype.slice.call(arguments)) | ||
RoxLogger.debug(`Roxx evaluating ${name} with ${JSON.stringify(arguments)} result = ${v}`); | ||
return v; | ||
export default class RoxxParser { | ||
/** | ||
* A parser for Roxx expressions. | ||
* Roxx expression are polish notation expressions {@link https://en.wikipedia.org/wiki/Polish_notation} | ||
* @class | ||
* @module RoxxParser | ||
* @param {*} cache - Optional token cache object. A default cache object is used if none specified. | ||
*/ | ||
constructor (cache) { | ||
this._tokenizer = new RoxxTokenizer(); | ||
this._cache = cache || defaultCache; | ||
} | ||
} | ||
function _versionCompare (v1, v2, options = {zeroExtend: true, lexicographical: true }) { | ||
var lexicographical = options && options.lexicographical, | ||
zeroExtend = options && options.zeroExtend, | ||
v1parts = v1.split('.'), | ||
v2parts = v2.split('.'); | ||
/** | ||
* Given an operator function and stack, return an array of arguments for the operator. | ||
* @param {Function} operator | ||
* @param {Array} stack | ||
* @returns {Array} Array of arguments for operator | ||
* @private | ||
*/ | ||
_argsArrayForOperator (operator, stack) { | ||
var argsArray = []; | ||
var length = operator.length; | ||
for (var i = 0; i < length; i++) { | ||
var arg = stack.pop(); | ||
argsArray.push(arg); | ||
} | ||
function isValidPart (x) { | ||
return (lexicographical ? /[0-9A-Za-z_-]+$/ : /^\d+$/).test(x); | ||
return argsArray; | ||
} | ||
if (!v1parts.every(isValidPart) || !v2parts.every(isValidPart)) { | ||
return NaN; | ||
/** | ||
* Add context to specific operators after operator's args args. | ||
* @param {*} operator, args, context | ||
* @returns {Array} Args array with/without context | ||
*/ | ||
_modifyArgsHook ({operator, args, context}) { | ||
if (context && RoxxOperatorsMap.operatorsWithContext.includes(operator)) { | ||
return [...args, context]; // insert context as a last arg | ||
} | ||
return args; | ||
} | ||
if (zeroExtend) { | ||
while (v1parts.length < v2parts.length) v1parts.push('0'); | ||
while (v2parts.length < v1parts.length) v2parts.push('0'); | ||
} | ||
if (!lexicographical) { | ||
v1parts = v1parts.map(Number); | ||
v2parts = v2parts.map(Number); | ||
} | ||
for (var i = 0; i < v1parts.length; ++i) { | ||
if (v2parts.length == i) { | ||
return 1; | ||
/** | ||
* Tokenizes and caches expr if not available in cache. | ||
* @param {string} expr | ||
* @returns {Array} Tokenized version of expr | ||
*/ | ||
compileExpression (expr) { | ||
var tokens = this._cache.get(expr); | ||
if (!tokens) { | ||
tokens = this._tokenizer.tokenize(expr).reverse(); | ||
this._cache.set(expr, tokens); | ||
} | ||
if (v1parts[i] == v2parts[i]) { | ||
continue; | ||
} else if (v1parts[i] > v2parts[i]) { | ||
return 1; | ||
} else { | ||
return -1; | ||
} | ||
return tokens; | ||
} | ||
if (v1parts.length != v2parts.length) { | ||
return -1; | ||
} | ||
/** | ||
* Evaluates a Roxx expression. | ||
* | ||
* @param {string} expr - Roxx expression string. | ||
* @returns {*} Result of Roxx expression evaluation. | ||
*/ | ||
evaluateExpression (expr, context = {}) { | ||
var stack = []; | ||
var tokens = this.compileExpression(expr); | ||
return 0; | ||
} | ||
var result = undefined; | ||
var length = tokens.length; | ||
try { | ||
for (var i = 0; i < length; i++) { | ||
var token = tokens[i]; | ||
if (token.type == RoxxTokenTypeRand) { | ||
stack.push(token.value); | ||
} else if (token.type == RoxxTokenTypeRator) { | ||
var operator = RoxxOperatorsMap[token.value]; | ||
var args = this._argsArrayForOperator(operator, stack); | ||
args = this._modifyArgsHook({operator, args, context}); | ||
var value = operator.apply(this, args); | ||
stack.push(value); | ||
const isUndefined = __('isUndefined', (op) => { | ||
return op === undefined; | ||
}); | ||
const now = ('now', () => { | ||
return Date.now(); | ||
}); | ||
const and = __('and', (op1, op2) => { | ||
return op1 && op2; | ||
}); | ||
const or = ('or', (op1, op2) => { | ||
return op1 || op2; | ||
}); | ||
const ne = ('ne', (op1, op2) => { | ||
var arg1 = isUndefined(op1) ? false : op1; | ||
var arg2 = isUndefined(op2) ? false : op2; | ||
return arg1 !== arg2; | ||
}); | ||
const eq = ('eq', (op1, op2) => { | ||
var arg1 = isUndefined(op1) ? false : op1; | ||
var arg2 = isUndefined(op2) ? false : op2; | ||
return arg1 === arg2; | ||
}); | ||
const not = __('not', (op) => { | ||
return !op; | ||
}); | ||
const ifThen = ('ifthen', (conditionExpression, trueExpression, falseExpression) => { | ||
if (conditionExpression) { | ||
return trueExpression; | ||
} else { | ||
return falseExpression; | ||
} | ||
}); | ||
const lt = ('lt', (op1, op2) => { | ||
if (isUndefined(op1) || isUndefined(op2)) return false; | ||
if (typeof op1 !== 'number' || typeof op2 !== 'number') return false; | ||
return op1 < op2; | ||
}); | ||
const lte = __('lte', (op1, op2) => { | ||
if (isUndefined(op1) || isUndefined(op2)) return false; | ||
if (typeof op1 !== 'number' || typeof op2 !== 'number') return false; | ||
return op1 <= op2; | ||
}); | ||
const gt = __('gt', (op1, op2) => { | ||
if (isUndefined(op1) || isUndefined(op2)) return false; | ||
if (typeof op1 !== 'number' || typeof op2 !== 'number') return false; | ||
return op1 > op2; | ||
}); | ||
const gte = __('gte', (op1, op2) => { | ||
if (isUndefined(op1) || isUndefined(op2)) return false; | ||
if (typeof op1 !== 'number' || typeof op2 !== 'number') return false; | ||
return op1 >= op2; | ||
}); | ||
const match = __('match', (op1, op2, op3) => { | ||
var text = op1; | ||
var regex = new RegExp(op2, op3); | ||
var match = regex.exec(text); | ||
if (match) { | ||
return true; | ||
} | ||
return false; | ||
}); | ||
const semverLt = __('semverLt', (op1, op2) => { | ||
if (isUndefined(op1) || isUndefined(op2)) return false; | ||
if (typeof op1 !== 'string' || typeof op2 !== 'string') return false; | ||
return _versionCompare(op1, op2, { zeroExtend: true }) < 0; | ||
}); | ||
const semverLte = __('semverLte', (op1, op2) => { | ||
if (isUndefined(op1) || isUndefined(op2)) return false; | ||
if (typeof op1 !== 'string' || typeof op2 !== 'string') return false; | ||
return _versionCompare(op1, op2, { zeroExtend: true }) <= 0; | ||
}); | ||
const semverGt = __('semverGt', (op1, op2) => { | ||
if (isUndefined(op1) || isUndefined(op2)) return false; | ||
if (typeof op1 !== 'string' || typeof op2 !== 'string') return false; | ||
return _versionCompare(op1, op2, { zeroExtend: true }) > 0; | ||
}); | ||
const semverGte = __('semverGte', (op1, op2) => { | ||
if (isUndefined(op1) || isUndefined(op2)) return false; | ||
if (typeof op1 !== 'string' || typeof op2 !== 'string') return false; | ||
return _versionCompare(op1, op2, { zeroExtend: true }) >= 0; | ||
}); | ||
const semverEq = __('semverEq', (op1, op2) => { | ||
if (isUndefined(op1) || isUndefined(op2)) return false; | ||
if (typeof op1 !== 'string' || typeof op2 !== 'string') return false; | ||
return _versionCompare(op1, op2) == 0; | ||
}); | ||
const semverNe = ('semverNe', (op1, op2) => { | ||
if (isUndefined(op1) || isUndefined(op2)) return false; | ||
if (typeof op1 !== 'string' || typeof op2 !== 'string') return false; | ||
return _versionCompare(op1, op2) != 0; | ||
}); | ||
const mergeSeed = __('mergeSeed', (seed1, seed2) => { | ||
return seed1 + '.' + seed2; | ||
}); | ||
const getBucket = __('getBucket', (seed) => { | ||
var hash = md5(seed, { asBytes: true }); | ||
hash = | ||
((hash[0] & 0xff) | | ||
((hash[1] & 0xff) << 8) | | ||
((hash[2] & 0xff) << 16) | | ||
((hash[3] & 0xff) << 24)) >>> | ||
0; | ||
var bucket = hash / (Math.pow(2, 32) - 1); | ||
return bucket; | ||
}); | ||
const isInPercentage = __('isInPercentage', (percentage, seed) => { | ||
var bucket = getBucket(seed); | ||
var isInPercentage = bucket <= percentage; | ||
return isInPercentage; | ||
}); | ||
const isInPercentageRange = __('isInPercentageRange' , (percentageLow, percentageHigh, seed) => { | ||
var bucket = getBucket(seed); | ||
var isInPercentage = bucket >= percentageLow && bucket <= percentageHigh; | ||
return isInPercentage; | ||
}); | ||
const isInTargetGroup = __('isInTargetGroup', (targetGroup) => { | ||
var condition = TargetGroupRepository.targetGroupWithName(targetGroup) | ||
.condition; | ||
return new RoxxParser().evaluateExpression(condition); | ||
}); | ||
const isTargetGroupPaired = __('isTargetGroupPaired', () => { | ||
return false; | ||
}); | ||
const property = __('property', (propName) => { | ||
var prop = CustomPropertyRepository.get(propName); | ||
if (!prop) { | ||
return undefined; | ||
} else { | ||
return prop.value; | ||
} | ||
}) | ||
const context = { | ||
isUndefined, | ||
now, | ||
and, | ||
or, | ||
ne, | ||
eq, | ||
not, | ||
ifThen, | ||
lt, | ||
lte, | ||
gt, | ||
gte, | ||
match, | ||
semverLt, | ||
semverLte, | ||
semverGt, | ||
semverGte, | ||
semverEq, | ||
semverNe, | ||
mergeSeed, | ||
isInPercentage, | ||
isInPercentageRange, | ||
isInTargetGroup, | ||
isTargetGroupPaired, | ||
property | ||
}; | ||
export default class RoxxParser { | ||
cleanExpression(expr) { | ||
// TODO: CLEAN EXPR BEFORE IT GOES TO EVAL | ||
return expr; | ||
} | ||
evaluateExpression(expr) { | ||
try { | ||
const value = eval(this.cleanExpression(expr)); | ||
RoxLogger.debug(`Roxx evaluate expression ${expr} with value ${value}`); | ||
return value; | ||
RoxLogger.debug(`Roxx: Evaluated ${token.value} with ${JSON.stringify(args)} result = ${value}`); | ||
} else { | ||
stack.push(false); | ||
break; | ||
} | ||
} | ||
result = stack.pop(); | ||
} catch (err) { | ||
RoxLogger.error(`Roxx exception ${err}`); | ||
return false; | ||
result = false; | ||
} finally { | ||
return result; | ||
} | ||
} | ||
} | ||
}; |
import RoxFlagRepository from '../RoxFlagRepository'; | ||
import RoxFlag from '../../experiments/RoxFlag' | ||
@@ -34,5 +33,5 @@ var flag = {'name': 'duck'}; | ||
it('should name a flag', () => { | ||
RoxFlagRepository.addFlag('donald-5', new RoxFlag()); | ||
RoxFlagRepository.addFlag('donald-5', {}); | ||
expect(RoxFlagRepository.flagWithName('donald-5').name).toBe('donald-5'); | ||
}); | ||
}); |
@@ -14,2 +14,6 @@ class CustomPropertyRepository { | ||
clear () { | ||
this.store.clear() | ||
} | ||
get items() { | ||
@@ -16,0 +20,0 @@ return this.store.values(); |
import ConfigurationSetter from '../ConfigurationSetter' | ||
import RoxConfigurationRepository from '../../repositories/RoxConfigurationRepository' | ||
import RoxConfiguration from '../../remoteConfigurations/RoxConfiguration' | ||
import RoxConfigurationModel from '../../remoteConfigurations/RoxConfigurationModel' | ||
import RoxxParser from '../../parsers/RoxxParser' | ||
import RoxConfiguration from '../../entities/Configuration' | ||
beforeEach(() => { | ||
RoxConfigurationRepository.map = {}; | ||
}) | ||
describe('ConfigurationSetter tets', () => { | ||
it('should set value from condition', () => { | ||
it('should set parser and condition to configuration', () => { | ||
let config = new RoxConfiguration(100); | ||
RoxConfigurationRepository.addRemoteConfiguration('config1', config); | ||
new ConfigurationSetter(new RoxxParser()).setWithConfigurations([new RoxConfigurationModel('config1', 'number', 'ifThen(true, 101, undefined)')]); | ||
new ConfigurationSetter().prepareConfigurations([{name: 'config1', type: 'number', conditions: 'ifThen(false, 101, undefined)'}]); | ||
expect(config.value).toBe(101) | ||
expect(config.condition).toEqual('ifThen(false, 101, undefined)'); | ||
}); | ||
it('should set default value from condition', () => { | ||
it('should not set parser and condition to both configurations', () => { | ||
let config = new RoxConfiguration(100); | ||
let config2 = new RoxConfiguration(101); | ||
RoxConfigurationRepository.addRemoteConfiguration('config1', config); | ||
new ConfigurationSetter(new RoxxParser()).setWithConfigurations([new RoxConfigurationModel('config1', 'number', 'ifThen(false, 101, undefined)')]); | ||
new ConfigurationSetter().prepareConfigurations([{name: 'config1', type: 'number', conditions: 'ifThen(true, 101, undefined)'}]); | ||
expect(config.value).toBe(100) | ||
expect(config.condition).toEqual('ifThen(true, 101, undefined)'); | ||
expect(config2.condition).toBeUndefined(); | ||
}); | ||
it('should set default value when wrong type', () => { | ||
let config = new RoxConfiguration(100); | ||
RoxConfigurationRepository.addRemoteConfiguration('config1', config); | ||
new ConfigurationSetter(new RoxxParser()).setWithConfigurations([new RoxConfigurationModel('config1', 'number', 'ifThen(true, "101", undefined)')]); | ||
expect(config.value).toBe(100) | ||
}); | ||
}); |
import FlagsSetter from '../FlagsSetter'; | ||
import RoxFlagsRepository from '../../repositories/RoxFlagRepository'; | ||
import ConfigurationParser from '../../parsers/ConfigurationParser'; | ||
import RoxFlag from '../../experiments/RoxFlag'; | ||
import {Variant as RoxVariant} from '../../drivers/'; | ||
import RoxxParser from '../../parsers/RoxxParser'; | ||
import Experiment from '../../experiments/Experiment' | ||
import DeploymentConfiguration from '../../experiments/DeploymentConfiguration' | ||
import Experiment from '../../entities/Experiment' | ||
import DeploymentConfiguration from '../../entities/DeploymentConfiguration' | ||
@@ -34,5 +31,5 @@ const json = { | ||
}, | ||
featureFlags: [ | ||
'flag2' | ||
] | ||
featureFlags: [{ | ||
name: 'flag2' | ||
}] | ||
} | ||
@@ -42,36 +39,38 @@ ] | ||
}; | ||
beforeEach(() => { | ||
RoxFlagsRepository.map = {}; | ||
}) | ||
describe('ConfigurationSetter tets', () => { | ||
it('should set value from condition', () => { | ||
describe('FlagsSetter tets', () => { | ||
it('should set parser and condition to flags', () => { | ||
const parser = new ConfigurationParser(json, '123'); | ||
parser.parse(); | ||
RoxFlagsRepository.addFlag('flag1', new RoxFlag()); | ||
RoxFlagsRepository.addFlag('flag2', new RoxFlag()); | ||
new FlagsSetter(new RoxxParser()).setWithExperiments(parser.experiments()); | ||
RoxFlagsRepository.addFlag('flag1', {}); | ||
RoxFlagsRepository.addFlag('flag2', {}); | ||
new FlagsSetter().prepareFlagsWithExperiments(parser.experiments()); | ||
expect(RoxFlagsRepository.flagWithName('flag1').isEnabled).toEqual(true); | ||
expect(RoxFlagsRepository.flagWithName('flag2').isEnabled).toEqual(false); | ||
expect(RoxFlagsRepository.flagWithName('flag1').condition).toEqual('ifThen(true, "true", undefined)'); | ||
expect(RoxFlagsRepository.flagWithName('flag2').condition).toEqual('ifThen(false, "true", undefined)'); | ||
}); | ||
it('should set value for flag with default value', () => { | ||
RoxFlagsRepository.addFlag('flag1', new RoxFlag(true)); | ||
expect(RoxFlagsRepository.flagWithName('flag1').isEnabled).toEqual(true); | ||
RoxFlagsRepository.flagWithName('flag1').unfreeze(); | ||
it('should set parser and condition to one flag only', () => { | ||
const parser = new ConfigurationParser(json, '123'); | ||
parser.parse(); | ||
RoxFlagsRepository.addFlag('flag1', {}); | ||
new FlagsSetter().prepareFlagsWithExperiments(parser.experiments()); | ||
const experiment = new Experiment('id1', 'name1', false, false, new DeploymentConfiguration('ifThen(true, "false", undefined)'), [{ name: 'flag1' }]); | ||
new FlagsSetter(new RoxxParser()).setWithExperiments([experiment]); | ||
expect(RoxFlagsRepository.flagWithName('flag1').isEnabled).toEqual(false); | ||
expect(RoxFlagsRepository.flagWithName('flag1').condition).toEqual('ifThen(true, "true", undefined)'); | ||
expect(RoxFlagsRepository.flagWithName('flag2')).toBeUndefined(); | ||
}); | ||
it('should set value for variant', () => { | ||
RoxFlagsRepository.addFlag('flag1', new RoxVariant('red', ['blue'])); | ||
expect(RoxFlagsRepository.flagWithName('flag1').value).toEqual('red'); | ||
RoxFlagsRepository.flagWithName('flag1').unfreeze(); | ||
RoxFlagsRepository.addFlag('flag1', { defaultValue: 'red', options: ['blue'] }); | ||
expect(RoxFlagsRepository.flagWithName('flag1').defaultValue).toEqual('red'); | ||
const experiment = new Experiment('id1', 'name1', false, false, new DeploymentConfiguration('ifThen(true, "blue", undefined)'), [{ name: 'flag1' }]); | ||
new FlagsSetter(new RoxxParser()).setWithExperiments([experiment]); | ||
new FlagsSetter().prepareFlagsWithExperiments([experiment]); | ||
expect(RoxFlagsRepository.flagWithName('flag1').value).toEqual('blue'); | ||
expect(RoxFlagsRepository.flagWithName('flag1').condition).toEqual('ifThen(true, "blue", undefined)'); | ||
}); | ||
}); |
@@ -5,15 +5,7 @@ import RoxConfigurationRepository from '../repositories/RoxConfigurationRepository' | ||
export default class ConfigurationSetter { | ||
constructor(roxxParser) { | ||
this.roxxParser = roxxParser; | ||
} | ||
setWithConfigurations(configurations) { | ||
RoxLogger.debug(`Set configurations ${JSON.stringify(configurations)}`); | ||
configurations = configurations || []; | ||
configurations.forEach(function(element) { | ||
if (!element) { | ||
return; | ||
} | ||
let configuration = RoxConfigurationRepository.remoteConfigurationWithName(element.name); | ||
prepareConfigurations(configFromAPI = []) { | ||
RoxLogger.debug(`Set remote configurations ${JSON.stringify(configFromAPI)}`); | ||
configFromAPI.forEach(({name, conditions}) => { | ||
let configuration = RoxConfigurationRepository.remoteConfigurationWithName(name); | ||
if (!configuration) { | ||
@@ -23,10 +15,5 @@ return; | ||
this._setConfiguration(configuration, element.conditions); | ||
configuration.condition = conditions; | ||
}, this); | ||
} | ||
_setConfiguration(configuration, conditions) { | ||
let expression = this.roxxParser.evaluateExpression(conditions) | ||
configuration.value = expression; | ||
} | ||
} |
@@ -5,7 +5,3 @@ import RoxFlagRepository from '../repositories/RoxFlagRepository'; | ||
export default class FlagsSetter { | ||
constructor (roxxParser) { | ||
this.roxxParser = roxxParser; | ||
} | ||
setWithExperiments (experiments) { | ||
prepareFlagsWithExperiments (experiments) { | ||
RoxLogger.debug(`Set experiments ${JSON.stringify(experiments)}`); | ||
@@ -26,3 +22,3 @@ experiments = experiments || []; | ||
if (flagObject) { | ||
this._setFlag(flagObject, experiment.deploymentConfiguration.condition); | ||
flagObject.condition = experiment.deploymentConfiguration.condition | ||
} | ||
@@ -32,7 +28,2 @@ }); | ||
} | ||
_setFlag (flag, conditions) { | ||
let expression = this.roxxParser.evaluateExpression(conditions); | ||
flag.value = expression; | ||
} | ||
} |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 3 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Deprecated
MaintenanceThe maintainer of the package marked it as deprecated. This could indicate that a single version should not be used, or that the package is no longer maintained and any new vulnerabilities will not be fixed.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
1684190
11545
0
6
82
26
20