@splitsoftware/openfeature-js-split-provider 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGES.txt ADDED
@@ -0,0 +1,8 @@
1
+ 1.0.0
2
+ - First release. Up to date with spec 0.4.0, and @openfeature/nodejs-sdk v0.2.0
3
+ 1.0.1
4
+ - Fixes issues with flag details and error codes in negative cases, adds unit tests
5
+ - Up to date with spec 0.4.0 and @openfeature/nodejs-sdk v0.3.2
6
+ 1.0.2
7
+ - Changes name from Node-specific implementation to generic JSON
8
+ - Up to date with spec 0.4.0 and @openfeature/js-sdk 0.4.0
@@ -0,0 +1,28 @@
1
+ # Contributing to the Split OpenFeature Provider
2
+
3
+ The Split Provider is an open source project and we welcome feedback and contribution. The information below describes how to build the project with your changes, run the tests, and send the Pull Request(PR).
4
+
5
+ ## Development
6
+
7
+ ### Development process
8
+
9
+ 1. Fork the repository and create a topic branch from `development` branch. Please use a descriptive name for your branch.
10
+ 2. While developing, use descriptive messages in your commits. Avoid short or meaningless sentences like "fix bug".
11
+ 3. Make sure to add tests for both positive and negative cases.
12
+ 4. Run the build script and make sure it runs with no errors.
13
+ 5. Run all tests and make sure there are no failures.
14
+ 6. `git push` your changes to GitHub within your topic branch.
15
+ 7. Open a Pull Request(PR) from your forked repo and into the `development` branch of the original repository.
16
+ 8. When creating your PR, please fill out all the fields of the PR template, as applicable, for the project.
17
+ 9. Check for conflicts once the pull request is created to make sure your PR can be merged cleanly into `development`.
18
+ 10. Keep an eye out for any feedback or comments from the Split team.
19
+
20
+ ### Building the Split Provider
21
+ - `npm run build`
22
+
23
+ ### Running tests
24
+ - `npm run test`
25
+
26
+ # Contact
27
+
28
+ If you have any other questions or need to contact us directly in a private manner send us a note at sdks@split.io
package/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright © 2022 Split Software, Inc.
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # Split OpenFeature Provider for NodeJS
2
+ [![Twitter Follow](https://img.shields.io/twitter/follow/splitsoftware.svg?style=social&label=Follow&maxAge=1529000)](https://twitter.com/intent/follow?screen_name=splitsoftware)
3
+
4
+ ## Overview
5
+ This Provider is designed to allow the use of OpenFeature with Split, the platform for controlled rollouts, serving features to your users via the Split feature flag to manage your complete customer experience.
6
+
7
+ ## Compatibility
8
+
9
+
10
+ ## Getting started
11
+ Below is a simple example that describes the instantiation of the Split Provider. Please see the [OpenFeature Documentation](https://docs.openfeature.dev/docs/reference/concepts/evaluation-api) for details on how to use the OpenFeature SDK.
12
+
13
+ ```js
14
+ const OpenFeature = require('@openfeature/js-sdk').OpenFeature;
15
+ const SplitFactory = require('@splitsoftware/splitio').SplitFactory;
16
+ const OpenFeatureSplitProvider = require('@splitsoftware/openfeature-nodejs-split-provider').OpenFeatureSplitProvider;
17
+
18
+ splitClient = SplitFactory({core: {authorizationKey: 'localhost'}}).client();
19
+ provider = new OpenFeatureSplitProvider({splitClient});
20
+ openFeature.setProvider(provider);
21
+ ```
22
+
23
+ ## Use of OpenFeature with Split
24
+ After the initial setup you can use OpenFeature according to their [documentation](https://docs.openfeature.dev/docs/reference/concepts/evaluation-api/).
25
+
26
+ One important note is that the Split Provider **requires a targeting key** to be set. Often times this should be set when evaluating the value of a flag by [setting an EvaluationContext](https://docs.openfeature.dev/docs/reference/concepts/evaluation-context) which contains the targeting key. An example flag evaluation is
27
+ ```js
28
+ const client = openFeature.getClient('CLIENT_NAME');
29
+
30
+ const context: EvaluationContext = {
31
+ targetingKey: 'TARGETING_KEY',
32
+ };
33
+ const boolValue = await client.getBooleanValue('boolFlag', false, context);
34
+ ```
35
+ If the same targeting key is used repeatedly, the evaluation context may be set at the client level
36
+ ```js
37
+ const context: EvaluationContext = {
38
+ targetingKey: 'TARGETING_KEY',
39
+ };
40
+ client.setEvaluationContext(context)
41
+ ```
42
+ or at the OpenFeatureAPI level
43
+ ```js
44
+ const context: EvaluationContext = {
45
+ targetingKey: 'TARGETING_KEY',
46
+ };
47
+ OpenFeatureAPI.getInstance().setCtx(context)
48
+ ````
49
+ If the context was set at the client or api level, it is not required to provide it during flag evaluation.
50
+
51
+ ## Submitting issues
52
+
53
+ The Split team monitors all issues submitted to this [issue tracker](https://github.com/splitio/split-openfeature-provider-nodejs/issues). We encourage you to use this issue tracker to submit any bug reports, feedback, and feature enhancements. We'll do our best to respond in a timely manner.
54
+
55
+ ## Contributing
56
+ Please see [Contributors Guide](CONTRIBUTORS-GUIDE.md) to find all you need to submit a Pull Request (PR).
57
+
58
+ ## License
59
+ Licensed under the Apache License, Version 2.0. See: [Apache License](http://www.apache.org/licenses/).
60
+
61
+ ## About Split
62
+
63
+ Split is the leading Feature Delivery Platform for engineering teams that want to confidently deploy features as fast as they can develop them. Split’s fine-grained management, real-time monitoring, and data-driven experimentation ensure that new features will improve the customer experience without breaking or degrading performance. Companies like Twilio, Salesforce, GoDaddy and WePay trust Split to power their feature delivery.
64
+
65
+ To learn more about Split, contact hello@split.io, or get started with feature flags for free at https://www.split.io/signup.
66
+
67
+ Split has built and maintains SDKs for:
68
+
69
+ * Java [Github](https://github.com/splitio/java-client) [Docs](https://help.split.io/hc/en-us/articles/360020405151-Java-SDK)
70
+ * Javascript [Github](https://github.com/splitio/javascript-client) [Docs](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK)
71
+ * Node [Github](https://github.com/splitio/javascript-client) [Docs](https://help.split.io/hc/en-us/articles/360020564931-Node-js-SDK)
72
+ * .NET [Github](https://github.com/splitio/dotnet-client) [Docs](https://help.split.io/hc/en-us/articles/360020240172--NET-SDK)
73
+ * Ruby [Github](https://github.com/splitio/ruby-client) [Docs](https://help.split.io/hc/en-us/articles/360020673251-Ruby-SDK)
74
+ * PHP [Github](https://github.com/splitio/php-client) [Docs](https://help.split.io/hc/en-us/articles/360020350372-PHP-SDK)
75
+ * Python [Github](https://github.com/splitio/python-client) [Docs](https://help.split.io/hc/en-us/articles/360020359652-Python-SDK)
76
+ * GO [Github](https://github.com/splitio/go-client) [Docs](https://help.split.io/hc/en-us/articles/360020093652-Go-SDK)
77
+ * Android [Github](https://github.com/splitio/android-client) [Docs](https://help.split.io/hc/en-us/articles/360020343291-Android-SDK)
78
+ * iOS [Github](https://github.com/splitio/ios-client) [Docs](https://help.split.io/hc/en-us/articles/360020401491-iOS-SDK)
79
+
80
+ For a comprehensive list of open source projects visit our [Github page](https://github.com/splitio?utf8=%E2%9C%93&query=%20only%3Apublic%20).
81
+
82
+ **Learn more about Split:**
83
+
84
+ Visit [split.io/product](https://www.split.io/product) for an overview of Split, or visit our documentation at [help.split.io](http://help.split.io) for more detailed information.
85
+
package/es/index.js ADDED
@@ -0,0 +1 @@
1
+ export * from './lib/js-split-provider';
@@ -0,0 +1,163 @@
1
+ import { __assign, __awaiter, __generator, __rest } from "tslib";
2
+ import { ParseError, FlagNotFoundError, StandardResolutionReasons } from '@openfeature/js-sdk';
3
+ var OpenFeatureSplitProvider = /** @class */ (function () {
4
+ function OpenFeatureSplitProvider(options) {
5
+ var _this = this;
6
+ this.metadata = {
7
+ name: 'split',
8
+ };
9
+ this.client = options.splitClient;
10
+ this.initialized = new Promise(function (resolve) {
11
+ _this.client.on(_this.client.Event.SDK_READY, function () {
12
+ console.log(_this.metadata.name + " provider initialized");
13
+ resolve();
14
+ });
15
+ });
16
+ }
17
+ OpenFeatureSplitProvider.prototype.resolveBooleanEvaluation = function (flagKey, defaultValue, context) {
18
+ return __awaiter(this, void 0, void 0, function () {
19
+ var details, value;
20
+ return __generator(this, function (_a) {
21
+ switch (_a.label) {
22
+ case 0: return [4 /*yield*/, this.evaluateTreatment(flagKey, this.transformContext(context))];
23
+ case 1:
24
+ details = _a.sent();
25
+ switch (details.value) {
26
+ case 'on':
27
+ value = true;
28
+ break;
29
+ case 'off':
30
+ value = false;
31
+ break;
32
+ case 'true':
33
+ value = true;
34
+ break;
35
+ case 'false':
36
+ value = false;
37
+ break;
38
+ case true:
39
+ value = true;
40
+ break;
41
+ case false:
42
+ value = false;
43
+ break;
44
+ case 'control':
45
+ value = defaultValue;
46
+ details.reason = 'FLAG_NOT_FOUND';
47
+ break;
48
+ default:
49
+ throw new ParseError("Invalid boolean value for " + details.value);
50
+ }
51
+ return [2 /*return*/, __assign(__assign({}, details), { value: value })];
52
+ }
53
+ });
54
+ });
55
+ };
56
+ OpenFeatureSplitProvider.prototype.resolveStringEvaluation = function (flagKey, _, context) {
57
+ return __awaiter(this, void 0, void 0, function () {
58
+ var details;
59
+ return __generator(this, function (_a) {
60
+ switch (_a.label) {
61
+ case 0: return [4 /*yield*/, this.evaluateTreatment(flagKey, this.transformContext(context))];
62
+ case 1:
63
+ details = _a.sent();
64
+ if (details.value == 'control') {
65
+ throw new FlagNotFoundError("Got error for split " + flagKey);
66
+ }
67
+ return [2 /*return*/, details];
68
+ }
69
+ });
70
+ });
71
+ };
72
+ OpenFeatureSplitProvider.prototype.resolveNumberEvaluation = function (flagKey, _, context) {
73
+ return __awaiter(this, void 0, void 0, function () {
74
+ var details;
75
+ return __generator(this, function (_a) {
76
+ switch (_a.label) {
77
+ case 0: return [4 /*yield*/, this.evaluateTreatment(flagKey, this.transformContext(context))];
78
+ case 1:
79
+ details = _a.sent();
80
+ return [2 /*return*/, __assign(__assign({}, details), { value: this.parseValidNumber(details.value) })];
81
+ }
82
+ });
83
+ });
84
+ };
85
+ OpenFeatureSplitProvider.prototype.resolveObjectEvaluation = function (flagKey, _, context) {
86
+ return __awaiter(this, void 0, void 0, function () {
87
+ var details;
88
+ return __generator(this, function (_a) {
89
+ switch (_a.label) {
90
+ case 0: return [4 /*yield*/, this.evaluateTreatment(flagKey, this.transformContext(context))];
91
+ case 1:
92
+ details = _a.sent();
93
+ return [2 /*return*/, __assign(__assign({}, details), { value: this.parseValidJsonObject(details.value) })];
94
+ }
95
+ });
96
+ });
97
+ };
98
+ OpenFeatureSplitProvider.prototype.evaluateTreatment = function (flagKey, consumer) {
99
+ return __awaiter(this, void 0, void 0, function () {
100
+ var details, value, details;
101
+ return __generator(this, function (_a) {
102
+ switch (_a.label) {
103
+ case 0:
104
+ if (!!consumer.key) return [3 /*break*/, 1];
105
+ details = {
106
+ value: 'control',
107
+ variant: 'control',
108
+ reason: StandardResolutionReasons.ERROR,
109
+ errorCode: 'TARGETING_KEY_MISSING'
110
+ };
111
+ return [2 /*return*/, details];
112
+ case 1: return [4 /*yield*/, this.initialized];
113
+ case 2:
114
+ _a.sent();
115
+ value = this.client.getTreatment(consumer.key, flagKey, consumer.attributes);
116
+ details = {
117
+ value: value,
118
+ variant: value,
119
+ reason: StandardResolutionReasons.TARGETING_MATCH
120
+ };
121
+ return [2 /*return*/, details];
122
+ }
123
+ });
124
+ });
125
+ };
126
+ //Transform the context into an object useful for the Split API, an key string with arbitrary Split "Attributes".
127
+ OpenFeatureSplitProvider.prototype.transformContext = function (context) {
128
+ var targetingKey = context.targetingKey, attributes = __rest(context, ["targetingKey"]);
129
+ return {
130
+ key: targetingKey,
131
+ // Stringify context objects include date.
132
+ attributes: JSON.parse(JSON.stringify(attributes)),
133
+ };
134
+ };
135
+ OpenFeatureSplitProvider.prototype.parseValidNumber = function (stringValue) {
136
+ if (stringValue === undefined) {
137
+ throw new ParseError("Invalid 'undefined' value.");
138
+ }
139
+ var result = Number.parseFloat(stringValue);
140
+ if (Number.isNaN(result)) {
141
+ throw new ParseError("Invalid numeric value " + stringValue);
142
+ }
143
+ return result;
144
+ };
145
+ OpenFeatureSplitProvider.prototype.parseValidJsonObject = function (stringValue) {
146
+ if (stringValue === undefined) {
147
+ throw new ParseError("Invalid 'undefined' JSON value.");
148
+ }
149
+ // we may want to allow the parsing to be customized.
150
+ try {
151
+ var value = JSON.parse(stringValue);
152
+ if (typeof value !== 'object') {
153
+ throw new ParseError("Flag value " + stringValue + " had unexpected type " + typeof value + ", expected \"object\"");
154
+ }
155
+ return value;
156
+ }
157
+ catch (err) {
158
+ throw new ParseError("Error parsing " + stringValue + " as JSON, " + err);
159
+ }
160
+ };
161
+ return OpenFeatureSplitProvider;
162
+ }());
163
+ export { OpenFeatureSplitProvider };
package/lib/index.js ADDED
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ var tslib_1 = require("tslib");
4
+ (0, tslib_1.__exportStar)(require("./lib/js-split-provider"), exports);
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OpenFeatureSplitProvider = void 0;
4
+ var tslib_1 = require("tslib");
5
+ var js_sdk_1 = require("@openfeature/js-sdk");
6
+ var OpenFeatureSplitProvider = /** @class */ (function () {
7
+ function OpenFeatureSplitProvider(options) {
8
+ var _this = this;
9
+ this.metadata = {
10
+ name: 'split',
11
+ };
12
+ this.client = options.splitClient;
13
+ this.initialized = new Promise(function (resolve) {
14
+ _this.client.on(_this.client.Event.SDK_READY, function () {
15
+ console.log(_this.metadata.name + " provider initialized");
16
+ resolve();
17
+ });
18
+ });
19
+ }
20
+ OpenFeatureSplitProvider.prototype.resolveBooleanEvaluation = function (flagKey, defaultValue, context) {
21
+ return (0, tslib_1.__awaiter)(this, void 0, void 0, function () {
22
+ var details, value;
23
+ return (0, tslib_1.__generator)(this, function (_a) {
24
+ switch (_a.label) {
25
+ case 0: return [4 /*yield*/, this.evaluateTreatment(flagKey, this.transformContext(context))];
26
+ case 1:
27
+ details = _a.sent();
28
+ switch (details.value) {
29
+ case 'on':
30
+ value = true;
31
+ break;
32
+ case 'off':
33
+ value = false;
34
+ break;
35
+ case 'true':
36
+ value = true;
37
+ break;
38
+ case 'false':
39
+ value = false;
40
+ break;
41
+ case true:
42
+ value = true;
43
+ break;
44
+ case false:
45
+ value = false;
46
+ break;
47
+ case 'control':
48
+ value = defaultValue;
49
+ details.reason = 'FLAG_NOT_FOUND';
50
+ break;
51
+ default:
52
+ throw new js_sdk_1.ParseError("Invalid boolean value for " + details.value);
53
+ }
54
+ return [2 /*return*/, (0, tslib_1.__assign)((0, tslib_1.__assign)({}, details), { value: value })];
55
+ }
56
+ });
57
+ });
58
+ };
59
+ OpenFeatureSplitProvider.prototype.resolveStringEvaluation = function (flagKey, _, context) {
60
+ return (0, tslib_1.__awaiter)(this, void 0, void 0, function () {
61
+ var details;
62
+ return (0, tslib_1.__generator)(this, function (_a) {
63
+ switch (_a.label) {
64
+ case 0: return [4 /*yield*/, this.evaluateTreatment(flagKey, this.transformContext(context))];
65
+ case 1:
66
+ details = _a.sent();
67
+ if (details.value == 'control') {
68
+ throw new js_sdk_1.FlagNotFoundError("Got error for split " + flagKey);
69
+ }
70
+ return [2 /*return*/, details];
71
+ }
72
+ });
73
+ });
74
+ };
75
+ OpenFeatureSplitProvider.prototype.resolveNumberEvaluation = function (flagKey, _, context) {
76
+ return (0, tslib_1.__awaiter)(this, void 0, void 0, function () {
77
+ var details;
78
+ return (0, tslib_1.__generator)(this, function (_a) {
79
+ switch (_a.label) {
80
+ case 0: return [4 /*yield*/, this.evaluateTreatment(flagKey, this.transformContext(context))];
81
+ case 1:
82
+ details = _a.sent();
83
+ return [2 /*return*/, (0, tslib_1.__assign)((0, tslib_1.__assign)({}, details), { value: this.parseValidNumber(details.value) })];
84
+ }
85
+ });
86
+ });
87
+ };
88
+ OpenFeatureSplitProvider.prototype.resolveObjectEvaluation = function (flagKey, _, context) {
89
+ return (0, tslib_1.__awaiter)(this, void 0, void 0, function () {
90
+ var details;
91
+ return (0, tslib_1.__generator)(this, function (_a) {
92
+ switch (_a.label) {
93
+ case 0: return [4 /*yield*/, this.evaluateTreatment(flagKey, this.transformContext(context))];
94
+ case 1:
95
+ details = _a.sent();
96
+ return [2 /*return*/, (0, tslib_1.__assign)((0, tslib_1.__assign)({}, details), { value: this.parseValidJsonObject(details.value) })];
97
+ }
98
+ });
99
+ });
100
+ };
101
+ OpenFeatureSplitProvider.prototype.evaluateTreatment = function (flagKey, consumer) {
102
+ return (0, tslib_1.__awaiter)(this, void 0, void 0, function () {
103
+ var details, value, details;
104
+ return (0, tslib_1.__generator)(this, function (_a) {
105
+ switch (_a.label) {
106
+ case 0:
107
+ if (!!consumer.key) return [3 /*break*/, 1];
108
+ details = {
109
+ value: 'control',
110
+ variant: 'control',
111
+ reason: js_sdk_1.StandardResolutionReasons.ERROR,
112
+ errorCode: 'TARGETING_KEY_MISSING'
113
+ };
114
+ return [2 /*return*/, details];
115
+ case 1: return [4 /*yield*/, this.initialized];
116
+ case 2:
117
+ _a.sent();
118
+ value = this.client.getTreatment(consumer.key, flagKey, consumer.attributes);
119
+ details = {
120
+ value: value,
121
+ variant: value,
122
+ reason: js_sdk_1.StandardResolutionReasons.TARGETING_MATCH
123
+ };
124
+ return [2 /*return*/, details];
125
+ }
126
+ });
127
+ });
128
+ };
129
+ //Transform the context into an object useful for the Split API, an key string with arbitrary Split "Attributes".
130
+ OpenFeatureSplitProvider.prototype.transformContext = function (context) {
131
+ var targetingKey = context.targetingKey, attributes = (0, tslib_1.__rest)(context, ["targetingKey"]);
132
+ return {
133
+ key: targetingKey,
134
+ // Stringify context objects include date.
135
+ attributes: JSON.parse(JSON.stringify(attributes)),
136
+ };
137
+ };
138
+ OpenFeatureSplitProvider.prototype.parseValidNumber = function (stringValue) {
139
+ if (stringValue === undefined) {
140
+ throw new js_sdk_1.ParseError("Invalid 'undefined' value.");
141
+ }
142
+ var result = Number.parseFloat(stringValue);
143
+ if (Number.isNaN(result)) {
144
+ throw new js_sdk_1.ParseError("Invalid numeric value " + stringValue);
145
+ }
146
+ return result;
147
+ };
148
+ OpenFeatureSplitProvider.prototype.parseValidJsonObject = function (stringValue) {
149
+ if (stringValue === undefined) {
150
+ throw new js_sdk_1.ParseError("Invalid 'undefined' JSON value.");
151
+ }
152
+ // we may want to allow the parsing to be customized.
153
+ try {
154
+ var value = JSON.parse(stringValue);
155
+ if (typeof value !== 'object') {
156
+ throw new js_sdk_1.ParseError("Flag value " + stringValue + " had unexpected type " + typeof value + ", expected \"object\"");
157
+ }
158
+ return value;
159
+ }
160
+ catch (err) {
161
+ throw new js_sdk_1.ParseError("Error parsing " + stringValue + " as JSON, " + err);
162
+ }
163
+ };
164
+ return OpenFeatureSplitProvider;
165
+ }());
166
+ exports.OpenFeatureSplitProvider = OpenFeatureSplitProvider;
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@splitsoftware/openfeature-js-split-provider",
3
+ "version": "1.0.2",
4
+ "description": "Split OpenFeature Provider",
5
+ "files": [
6
+ "README.md",
7
+ "CONTRIBUTORS-GUIDE.md",
8
+ "LICENSE",
9
+ "CHANGES.txt",
10
+ "lib",
11
+ "types",
12
+ "es",
13
+ "src"
14
+ ],
15
+ "repository": "splitio/openfeature-split-provider-js",
16
+ "homepage": "https://github.com/splitio/openfeature-split-provider-nodejs#readme",
17
+ "bugs": "https://github.com/splitio/openfeature-split-provider-nodejs/issues",
18
+ "license": "Apache-2.0",
19
+ "author": "Josh Sirota <josh.sirota@split.io>",
20
+ "contributors": [
21
+ ],
22
+ "main": "lib/index.js",
23
+ "module": "es/index.js",
24
+ "types": "types",
25
+ "engines": {
26
+ "npm": ">=3",
27
+ "node": ">=6"
28
+ },
29
+ "dependencies": {
30
+ "@openfeature/js-sdk": "^0.4.0",
31
+ "@splitsoftware/splitio": "^10.21.1"
32
+ },
33
+ "devDependencies": {
34
+ "copyfiles": "^2.4.1",
35
+ "cross-env": "^7.0.3",
36
+ "replace": "^1.2.1",
37
+ "rimraf": "^3.0.2",
38
+ "tap-min": "^2.0.0",
39
+ "tape": "4.13.2",
40
+ "tape-catch": "1.0.6",
41
+ "ts-node": "^10.5.0",
42
+ "typescript": "4.4.4"
43
+ },
44
+ "scripts": {
45
+ "build-esm": "rimraf es && tsc -outDir es",
46
+ "postbuild-esm": "cross-env NODE_ENV=es node scripts/copy.packages.json.js && ./scripts/build_esm_replace_imports.sh",
47
+ "build-cjs": "rimraf lib && tsc -outDir lib -m CommonJS",
48
+ "postbuild-cjs": "cross-env NODE_ENV=cjs node scripts/copy.packages.json.js && ./scripts/build_cjs_replace_imports.sh",
49
+ "build": "rimraf lib es && npm run build-cjs && npm run build-esm",
50
+ "check": "npm run check:version",
51
+ "check:version": "cross-env NODE_ENV=test tape -r ./ts-node.register src/settings/__tests__/defaults.spec.js",
52
+ "pretest-ts-decls": "npm run build-esm && npm run build-cjs && npm link",
53
+ "test-ts-decls": "./scripts/ts-tests.sh",
54
+ "posttest-ts-decls": "npm unlink && npm install",
55
+ "test": "cross-env NODE_ENV=test tape -r ./ts-node.register src/__tests__/node.spec.js | tap-min",
56
+ "publish:rc": "npm run check && npm run build && npm publish --tag canary",
57
+ "publish:stable": "npm run check && npm run build && npm publish"
58
+ },
59
+ "greenkeeper": {
60
+ "ignore": [
61
+ "karma",
62
+ "karma-tap",
63
+ "karma-webpack"
64
+ ]
65
+ }
66
+ }
@@ -0,0 +1,8 @@
1
+ import tape from 'tape-catch';
2
+
3
+ import clientSuite from './nodeSuites/client.spec.js';
4
+ import providerSuite from './nodeSuites/provider.spec.js';
5
+
6
+ tape('## OpenFeature NodeJS Split Provider - tests', async function (assert) {
7
+ assert.test('Client Tests', clientSuite);
8
+ });
@@ -0,0 +1,188 @@
1
+ const OpenFeature = require('@openfeature/js-sdk').OpenFeature;
2
+ const SplitFactory = require('@splitsoftware/splitio').SplitFactory;
3
+ import { OpenFeatureSplitProvider } from '../..';
4
+
5
+ export default async function(assert) {
6
+
7
+ const useDefaultTest = async (client) => {
8
+ let flagName = 'random-non-existent-feature';
9
+
10
+ let result = await client.getBooleanValue(flagName, false);
11
+ assert.equal(result, false);
12
+
13
+ let result2 = await client.getBooleanValue(flagName, true);
14
+ assert.equal(result2, true);
15
+
16
+ let defaultString = 'blah';
17
+ let resultString = await client.getStringValue(flagName, defaultString);
18
+ assert.equals(resultString, defaultString);
19
+
20
+ let defaultInt = 100;
21
+ let resultInt = await client.getNumberValue(flagName, defaultInt);
22
+ assert.equals(resultInt, defaultInt);
23
+
24
+ let defaultStructure = {
25
+ foo: 'bar'
26
+ };
27
+ let resultStructure = await client.getObjectValue(flagName, defaultStructure);
28
+ assert.equals(resultStructure, defaultStructure);
29
+ };
30
+
31
+ const missingTargetingKeyTest = async (client) => {
32
+ let details = await client.getBooleanDetails('non-existent-feature', false, { targetingKey: undefined });
33
+ assert.equals(details.value, false);
34
+ assert.equals(details.errorCode, 'TARGETING_KEY_MISSING');
35
+ };
36
+
37
+ const getControlVariantNonExistentSplit = async (client) => {
38
+ let details = await client.getBooleanDetails('non-existent-feature', false);
39
+ assert.equals(details.value, false);
40
+ assert.equals(details.variant, 'control');
41
+ assert.equals(details.reason, 'FLAG_NOT_FOUND');
42
+ };
43
+
44
+ const getBooleanSplitTest = async (client) => {
45
+ let result = await client.getBooleanValue('some_other_feature', true);
46
+ assert.equals(result, false);
47
+ };
48
+
49
+ const getBooleanSplitWithKeyTest = async (client) => {
50
+ let result = await client.getBooleanValue('my_feature', false);
51
+ assert.equals(result, true);
52
+
53
+ result = await client.getBooleanValue('my_feature', true, { targetingKey: 'randomKey' });
54
+ assert.equals(result, false);
55
+ };
56
+
57
+ const getStringSplitTest = async (client) => {
58
+ let result = await client.getStringValue('some_other_feature', 'on');
59
+ assert.equals(result, 'off');
60
+ };
61
+
62
+ const getNumberSplitTest = async (client) => {
63
+ let result = await client.getNumberValue('int_feature', 0);
64
+ assert.equals(result, 32);
65
+ };
66
+
67
+ const getObjectSplitTest = async (client) => {
68
+ let result = await client.getObjectValue('obj_feature', {});
69
+ assert.looseEquals(result, { 'key': 'value' });
70
+ };
71
+
72
+ const getMetadataNameTest = async (client) => {
73
+ assert.equals(client.metadata.name, 'test');
74
+ };
75
+
76
+ const getBooleanDetailsTest = async (client) => {
77
+ let details = await client.getBooleanDetails('some_other_feature', true);
78
+ assert.equals(details.flagKey, 'some_other_feature');
79
+ assert.equals(details.reason, 'TARGETING_MATCH');
80
+ assert.equals(details.value, false);
81
+ assert.equals(details.variant, 'off');
82
+ assert.equals(details.errorCode, undefined);
83
+ };
84
+
85
+ const getNumberDetailsTest = async (client) => {
86
+ let details = await client.getNumberDetails('int_feature', 0);
87
+ assert.equals(details.flagKey, 'int_feature');
88
+ assert.equals(details.reason, 'TARGETING_MATCH');
89
+ assert.equals(details.value, 32);
90
+ assert.equals(details.variant, '32');
91
+ assert.equals(details.errorCode, undefined);
92
+ };
93
+
94
+ const getStringDetailsTest = async (client) => {
95
+ let details = await client.getStringDetails('some_other_feature', 'blah');
96
+ assert.equals(details.flagKey, 'some_other_feature');
97
+ assert.equals(details.reason, 'TARGETING_MATCH');
98
+ assert.equals(details.value, 'off');
99
+ assert.equals(details.variant, 'off');
100
+ assert.equals(details.errorCode, undefined);
101
+ };
102
+
103
+ const getObjectDetailsTest = async (client) => {
104
+ let details = await client.getObjectDetails('obj_feature', {});
105
+ assert.equals(details.flagKey, 'obj_feature');
106
+ assert.equals(details.reason, 'TARGETING_MATCH');
107
+ assert.looseEquals(details.value, { key: 'value' });
108
+ assert.equals(details.variant, '{"key": "value"}');
109
+ assert.equals(details.errorCode, undefined);
110
+ };
111
+
112
+ const getBooleanFailTest = async (client) => {
113
+ let value = await client.getBooleanValue('obj_feature', false);
114
+ assert.equals(value, false);
115
+
116
+ let details = await client.getBooleanDetails('obj_feature', false);
117
+ assert.equals(details.value, false);
118
+ assert.equals(details.errorCode, 'PARSE_ERROR');
119
+ assert.equals(details.reason, 'ERROR');
120
+ assert.equals(details.variant, undefined);
121
+ };
122
+
123
+ const getNumberFailTest = async (client) => {
124
+ let value = await client.getNumberValue('obj_feature', 10);
125
+ assert.equals(value, 10);
126
+
127
+ let details = await client.getNumberDetails('obj_feature', 10);
128
+ assert.equals(details.value, 10);
129
+ assert.equals(details.errorCode, 'PARSE_ERROR');
130
+ assert.equals(details.reason, 'ERROR');
131
+ assert.equals(details.variant, undefined);
132
+ };
133
+
134
+ const getObjectFailTest = async (client) => {
135
+ let defaultObject = { foo: 'bar' };
136
+ let value = await client.getObjectValue('int_feature', defaultObject);
137
+ assert.equals(value, defaultObject);
138
+
139
+ let details = await client.getObjectDetails('int_feature', defaultObject);
140
+ assert.equals(details.value, defaultObject);
141
+ assert.equals(details.errorCode, 'PARSE_ERROR');
142
+ assert.equals(details.reason, 'ERROR');
143
+ assert.equals(details.variant, undefined);
144
+ };
145
+
146
+ let splitClient = SplitFactory({
147
+ core: {
148
+ authorizationKey: 'localhost'
149
+ },
150
+ features: './split.yaml',
151
+ debug: 'DEBUG'
152
+ }).client();
153
+
154
+ let provider = new OpenFeatureSplitProvider({splitClient});
155
+ OpenFeature.setProvider(provider);
156
+
157
+ let client = OpenFeature.getClient('test');
158
+ let evaluationContext = {
159
+ targetingKey: 'key'
160
+ };
161
+ client.setContext(evaluationContext);
162
+
163
+ await useDefaultTest(client);
164
+ await missingTargetingKeyTest(client);
165
+ await getControlVariantNonExistentSplit(client);
166
+
167
+ await getBooleanSplitTest(client);
168
+ await getBooleanSplitWithKeyTest(client);
169
+
170
+ await getStringSplitTest(client);
171
+ await getNumberSplitTest(client);
172
+ await getObjectSplitTest(client);
173
+
174
+ await getMetadataNameTest(client);
175
+
176
+ await getBooleanDetailsTest(client);
177
+ await getNumberDetailsTest(client);
178
+ await getStringDetailsTest(client);
179
+ await getObjectDetailsTest(client);
180
+
181
+ await getBooleanFailTest(client);
182
+ await getNumberFailTest(client);
183
+ await getObjectFailTest(client);
184
+
185
+ splitClient.destroy(); // Shut down open handles
186
+
187
+ assert.end();
188
+ }
@@ -0,0 +1,109 @@
1
+ export default async function(assert) {
2
+
3
+ const shouldFailWithBadApiKeyTest = () => {
4
+ assert.equal(1, 1);
5
+ };
6
+
7
+ const evalBooleanNullEmptyTest = () => {
8
+ assert.equal(2, 2);
9
+ };
10
+
11
+ const evalBooleanControlTest = () => {
12
+ assert.equal(1, 1);
13
+ };
14
+
15
+ const evalBooleanTrueTest = () => {
16
+ assert.equal(1, 1);
17
+ };
18
+
19
+ const evalBooleanOnTest = () => {
20
+ assert.equal(1, 1);
21
+ };
22
+
23
+ const evalBooleanFalseTest = () => {
24
+ assert.equal(1, 1);
25
+ };
26
+
27
+ const evalBooleanOffTest = () => {
28
+ assert.equal(1, 1);
29
+ };
30
+
31
+ const evalBooleanErrorTest = () => {
32
+ assert.equal(1, 1);
33
+ };
34
+
35
+ const evalStringNullEmptyTest = () => {
36
+ assert.equal(1, 1);
37
+ };
38
+
39
+ const evalStringControlTest = () => {
40
+ assert.equal(1, 1);
41
+ };
42
+
43
+ const evalStringRegularTest = () => {
44
+ assert.equal(1, 1);
45
+ };
46
+
47
+ const evalNumberNullEmptyTest = () => {
48
+ assert.equal(1, 1);
49
+ };
50
+
51
+ const evalNumberControlTest = () => {
52
+ assert.equal(1, 1);
53
+ };
54
+
55
+ const evalNumberRegularTest = () => {
56
+ assert.equal(1, 1);
57
+ };
58
+
59
+ const evalNumberErrorTest = () => {
60
+ assert.equal(1, 1);
61
+ };
62
+
63
+ const evalStructureNullEmptyTest = () => {
64
+ assert.equal(1, 1);
65
+ };
66
+
67
+ const evalStructureControlTest = () => {
68
+ assert.equal(1, 1);
69
+ };
70
+
71
+ const evalStructureRegularTest = () => {
72
+ assert.equal(1, 1);
73
+ };
74
+
75
+ const evalStructureComplexTest = () => {
76
+ assert.equal(1, 1);
77
+ };
78
+
79
+ const evalStructureErrorTest = () => {
80
+ assert.equal(1, 1);
81
+ };
82
+
83
+ shouldFailWithBadApiKeyTest();
84
+
85
+ evalBooleanNullEmptyTest();
86
+ evalBooleanControlTest();
87
+ evalBooleanTrueTest();
88
+ evalBooleanOnTest();
89
+ evalBooleanFalseTest();
90
+ evalBooleanOffTest();
91
+ evalBooleanErrorTest();
92
+
93
+ evalStringNullEmptyTest();
94
+ evalStringControlTest();
95
+ evalStringRegularTest();
96
+
97
+ evalNumberNullEmptyTest();
98
+ evalNumberControlTest();
99
+ evalNumberRegularTest();
100
+ evalNumberErrorTest();
101
+
102
+ evalStructureNullEmptyTest();
103
+ evalStructureControlTest();
104
+ evalStructureRegularTest();
105
+ evalStructureComplexTest();
106
+ evalStructureErrorTest();
107
+
108
+ assert.end();
109
+ }
@@ -0,0 +1,7 @@
1
+ // Util method to trigger 'unload' DOM event
2
+ export function triggerUnloadEvent() {
3
+ const event = document.createEvent('HTMLEvents');
4
+ event.initEvent('unload', true, true);
5
+ event.eventName = 'unload';
6
+ window.dispatchEvent(event);
7
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * EventEmitter mock based on https://github.com/gcedo/eventsourcemock/blob/master/src/EventSource.js
3
+ *
4
+ * To setup the mock assign it to the window object.
5
+ * ```
6
+ * import EventSource from 'eventsourcemock';
7
+ * Object.defineProperty(window, 'EventSource', {
8
+ * value: EventSource,
9
+ * });
10
+ * ```
11
+ *
12
+ */
13
+
14
+ import EventEmitter from 'events';
15
+
16
+ const defaultOptions = {
17
+ withCredentials: false
18
+ };
19
+
20
+ export const sources = {};
21
+ let __listener;
22
+ export function setMockListener(listener) {
23
+ __listener = listener;
24
+ }
25
+
26
+ export default class EventSource {
27
+
28
+ constructor(
29
+ url,
30
+ eventSourceInitDict = defaultOptions
31
+ ) {
32
+ this.url = url;
33
+ this.withCredentials = eventSourceInitDict.withCredentials;
34
+ this.readyState = 0;
35
+ // eslint-disable-next-line no-undef
36
+ this.__emitter = new EventEmitter();
37
+ this.__eventSourceInitDict = arguments[1];
38
+ sources[url] = this;
39
+ if (__listener) setTimeout(__listener, 0, this);
40
+ }
41
+
42
+ addEventListener(eventName, listener) {
43
+ this.__emitter.addListener(eventName, listener);
44
+ }
45
+
46
+ removeEventListener(eventName, listener) {
47
+ this.__emitter.removeListener(eventName, listener);
48
+ }
49
+
50
+ close() {
51
+ this.readyState = 2;
52
+ }
53
+
54
+ // The following methods can be used to mock EventSource behavior and events
55
+ emit(eventName, messageEvent) {
56
+ this.__emitter.emit(eventName, messageEvent);
57
+
58
+ let listener;
59
+ switch (eventName) {
60
+ case 'error': listener = this.onerror; break;
61
+ case 'open': listener = this.onopen; break;
62
+ case 'message': listener = this.onmessage; break;
63
+ }
64
+ if (typeof listener === 'function') {
65
+ listener(messageEvent);
66
+ }
67
+ }
68
+
69
+ emitError(error) {
70
+ this.emit('error', error);
71
+ }
72
+
73
+ emitOpen() {
74
+ this.readyState = 1;
75
+ this.emit('open');
76
+ }
77
+
78
+ emitMessage(message) {
79
+ this.emit('message', message);
80
+ }
81
+ }
82
+
83
+ EventSource.CONNECTING = 0;
84
+ EventSource.OPEN = 1;
85
+ EventSource.CLOSED = 2;
@@ -0,0 +1,6 @@
1
+ import fetchMock from 'fetch-mock';
2
+
3
+ // config the fetch mock to chain routes (appends the new route to the list of routes)
4
+ fetchMock.config.overwriteRoutes = false;
5
+
6
+ export default fetchMock;
@@ -0,0 +1,11 @@
1
+ import fetchMock from 'fetch-mock';
2
+ import { __setFetch } from '../../../platform/getFetch/node';
3
+
4
+ const sandboxFetchMock = fetchMock.sandbox();
5
+
6
+ // config the fetch mock to chain routes (appends the new route to the list of routes)
7
+ sandboxFetchMock.config.overwriteRoutes = false;
8
+
9
+ __setFetch(sandboxFetchMock);
10
+
11
+ export default sandboxFetchMock;
@@ -0,0 +1,4 @@
1
+ {
2
+ "main": "./node.js",
3
+ "browser": "./browser.js"
4
+ }
@@ -0,0 +1,69 @@
1
+ const DEFAULT_ERROR_MARGIN = 50; // 0.05 secs
2
+
3
+ /**
4
+ * Assert if an `actual` and `expected` numeric values are nearlyEqual.
5
+ *
6
+ * @param {number} actual actual time lapse in millis
7
+ * @param {number} expected expected time lapse in millis
8
+ * @param {number} epsilon error margin in millis
9
+ * @returns {boolean} whether the absolute difference is minor to epsilon value or not
10
+ */
11
+ export function nearlyEqual(actual, expected, epsilon = DEFAULT_ERROR_MARGIN) {
12
+ const diff = Math.abs(actual - expected);
13
+ return diff <= epsilon;
14
+ }
15
+
16
+ /**
17
+ * mock the basic behaviour for `/segmentChanges` endpoint:
18
+ * - when `?since=-1`, it returns the given segment `keys` in `added` list.
19
+ * - otherwise, it returns empty `added` and `removed` lists, and the same since and till values.
20
+ *
21
+ * @param {Object} fetchMock see http://www.wheresrhys.co.uk/fetch-mock
22
+ * @param {string|RegExp|...} matcher see http://www.wheresrhys.co.uk/fetch-mock/#api-mockingmock_matcher
23
+ * @param {string[]} keys array of segment keys to fetch
24
+ * @param {number} changeNumber optional changeNumber
25
+ */
26
+ export function mockSegmentChanges(fetchMock, matcher, keys, changeNumber = 1457552620999) {
27
+ fetchMock.get(matcher, function (url) {
28
+ const since = parseInt(url.split('=').pop());
29
+ const name = url.split('?')[0].split('/').pop();
30
+ return {
31
+ status: 200, body: {
32
+ 'name': name,
33
+ 'added': since === -1 ? keys : [],
34
+ 'removed': [],
35
+ 'since': since,
36
+ 'till': since === -1 ? changeNumber : since,
37
+ }
38
+ };
39
+ });
40
+ }
41
+
42
+ export function hasNoCacheHeader(fetchMockOpts) {
43
+ return fetchMockOpts.headers['Cache-Control'] === 'no-cache';
44
+ }
45
+
46
+ const eventsEndpointMatcher = /^\/(testImpressions|metrics|events)/;
47
+ const authEndpointMatcher = /^\/v2\/auth/;
48
+ const streamingEndpointMatcher = /^\/(sse|event-stream)/;
49
+
50
+ /**
51
+ * Switch URLs servers based on target.
52
+ * Only used for testing purposes.
53
+ *
54
+ * @param {Object} settings settings object
55
+ * @param {String} target url path
56
+ * @return {String} completed url
57
+ */
58
+ export function url(settings, target) {
59
+ if (eventsEndpointMatcher.test(target)) {
60
+ return `${settings.urls.events}${target}`;
61
+ }
62
+ if (authEndpointMatcher.test(target)) {
63
+ return `${settings.urls.auth}${target}`;
64
+ }
65
+ if (streamingEndpointMatcher.test(target)) {
66
+ return `${settings.urls.streaming}${target}`;
67
+ }
68
+ return `${settings.urls.sdk}${target}`;
69
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './lib/js-split-provider';
@@ -0,0 +1,156 @@
1
+ import { EvaluationContext, Provider, ResolutionDetails, ParseError, FlagNotFoundError, JsonValue, OpenFeatureError, StandardResolutionReasons } from '@openfeature/js-sdk';
2
+ import SplitIO from '@splitsoftware/splitio/types/splitio';
3
+
4
+ export interface SplitProviderOptions {
5
+ splitClient: SplitIO.IClient;
6
+ }
7
+
8
+ type Consumer = {
9
+ key: string | undefined;
10
+ attributes: SplitIO.Attributes;
11
+ };
12
+
13
+ export class OpenFeatureSplitProvider implements Provider {
14
+ metadata = {
15
+ name: 'split',
16
+ };
17
+ private initialized: Promise<void>;
18
+ private client: SplitIO.IClient;
19
+
20
+ constructor(options: SplitProviderOptions) {
21
+ this.client = options.splitClient;
22
+ this.initialized = new Promise((resolve) => {
23
+ this.client.on(this.client.Event.SDK_READY, () => {
24
+ console.log(`${this.metadata.name} provider initialized`);
25
+ resolve();
26
+ });
27
+ });
28
+ }
29
+
30
+ async resolveBooleanEvaluation(
31
+ flagKey: string,
32
+ defaultValue: boolean,
33
+ context: EvaluationContext
34
+ ): Promise<ResolutionDetails<boolean>> {
35
+ const details = await this.evaluateTreatment(flagKey, this.transformContext(context));
36
+
37
+ let value: boolean;
38
+ switch (details.value as unknown) {
39
+ case 'on':
40
+ value = true;
41
+ break;
42
+ case 'off':
43
+ value = false;
44
+ break;
45
+ case 'true':
46
+ value = true;
47
+ break;
48
+ case 'false':
49
+ value = false;
50
+ break;
51
+ case true:
52
+ value = true;
53
+ break;
54
+ case false:
55
+ value = false;
56
+ break;
57
+ case 'control':
58
+ value = defaultValue;
59
+ details.reason = 'FLAG_NOT_FOUND';
60
+ break;
61
+ default:
62
+ throw new ParseError(`Invalid boolean value for ${details.value}`);
63
+ }
64
+ return { ...details, value };
65
+ }
66
+
67
+ async resolveStringEvaluation(
68
+ flagKey: string,
69
+ _: string,
70
+ context: EvaluationContext
71
+ ): Promise<ResolutionDetails<string>> {
72
+ const details = await this.evaluateTreatment(flagKey, this.transformContext(context));
73
+ if (details.value == 'control') {
74
+ throw new FlagNotFoundError(`Got error for split ${flagKey}`);
75
+ }
76
+ return details;
77
+ }
78
+
79
+ async resolveNumberEvaluation(
80
+ flagKey: string,
81
+ _: number,
82
+ context: EvaluationContext
83
+ ): Promise<ResolutionDetails<number>> {
84
+ const details = await this.evaluateTreatment(flagKey, this.transformContext(context));
85
+ return { ...details, value: this.parseValidNumber(details.value) };
86
+ }
87
+
88
+ async resolveObjectEvaluation<U extends JsonValue>(
89
+ flagKey: string,
90
+ _: U,
91
+ context: EvaluationContext
92
+ ): Promise<ResolutionDetails<U>> {
93
+ const details = await this.evaluateTreatment(flagKey, this.transformContext(context));
94
+ return { ...details, value: this.parseValidJsonObject(details.value) };
95
+ }
96
+
97
+ private async evaluateTreatment(flagKey: string, consumer: Consumer): Promise<ResolutionDetails<string>> {
98
+ if (!consumer.key) {
99
+ const details: ResolutionDetails<string> = {
100
+ value: 'control',
101
+ variant: 'control',
102
+ reason: StandardResolutionReasons.ERROR,
103
+ errorCode: 'TARGETING_KEY_MISSING'
104
+ }
105
+ return details;
106
+ } else {
107
+ await this.initialized;
108
+ const value = this.client.getTreatment(consumer.key, flagKey, consumer.attributes);
109
+ const details: ResolutionDetails<string> = {
110
+ value: value,
111
+ variant: value,
112
+ reason: StandardResolutionReasons.TARGETING_MATCH
113
+ };
114
+ return details;
115
+ }
116
+ }
117
+
118
+ //Transform the context into an object useful for the Split API, an key string with arbitrary Split "Attributes".
119
+ private transformContext(context: EvaluationContext): Consumer {
120
+ const { targetingKey, ...attributes } = context;
121
+ return {
122
+ key: targetingKey,
123
+ // Stringify context objects include date.
124
+ attributes: JSON.parse(JSON.stringify(attributes)),
125
+ };
126
+ }
127
+
128
+ private parseValidNumber(stringValue: string | undefined) {
129
+ if (stringValue === undefined) {
130
+ throw new ParseError(`Invalid 'undefined' value.`);
131
+ }
132
+ const result = Number.parseFloat(stringValue);
133
+ if (Number.isNaN(result)) {
134
+ throw new ParseError(`Invalid numeric value ${stringValue}`);
135
+ }
136
+ return result;
137
+ }
138
+
139
+ private parseValidJsonObject<T extends JsonValue>(stringValue: string | undefined): T {
140
+ if (stringValue === undefined) {
141
+ throw new ParseError(`Invalid 'undefined' JSON value.`);
142
+ }
143
+ // we may want to allow the parsing to be customized.
144
+ try {
145
+ const value = JSON.parse(stringValue);
146
+ if (typeof value !== 'object') {
147
+ throw new ParseError(
148
+ `Flag value ${stringValue} had unexpected type ${typeof value}, expected "object"`
149
+ );
150
+ }
151
+ return value;
152
+ } catch (err) {
153
+ throw new ParseError(`Error parsing ${stringValue} as JSON, ${err}`);
154
+ }
155
+ }
156
+ }