diff --git a/package.json b/package.json index 08e62e7..d4d08d3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@unleash/client-specification", - "version": "5.1.9", + "version": "5.2.0", "description": "A collection of test specifications to guide client implementations in various languages", "scripts": { "test": "node index", diff --git a/schema/delta-event-schema.js b/schema/delta-event-schema.js new file mode 100644 index 0000000..e840181 --- /dev/null +++ b/schema/delta-event-schema.js @@ -0,0 +1,29 @@ +const Joi = require('joi'); +const featureSchema = require('./feature-schema'); +const segmentSchema = require('./segment-schema'); + +const deltaEventSchema = Joi.alternatives().try( + Joi.object({ + eventId: Joi.number().required(), + type: Joi.string().valid("hydration").required(), + segments: Joi.array().items(segmentSchema).optional(), + features: Joi.array().items(featureSchema).required(), + }), + Joi.object({ + eventId: Joi.number().required(), + type: Joi.string().valid("feature-updated").required(), + feature: featureSchema.required(), + }), + Joi.object({ + eventId: Joi.number().required(), + type: Joi.string().valid("feature-removed").required(), + featureName: Joi.string().required(), + }), + Joi.object({ + eventId: Joi.number().required(), + type: Joi.string().valid("segment-updated").required(), + segment: segmentSchema.required(), + }) +); + +module.exports = deltaEventSchema; diff --git a/schema/deltas-schema.js b/schema/deltas-schema.js new file mode 100644 index 0000000..a84879c --- /dev/null +++ b/schema/deltas-schema.js @@ -0,0 +1,8 @@ +const Joi = require('joi'); +const deltaEventSchema = require('./delta-event-schema.js'); + +const deltasSchema = Joi.object({ + events: Joi.array().items(deltaEventSchema).required(), +}); + +module.exports = deltasSchema; \ No newline at end of file diff --git a/schema/feature-schema.js b/schema/feature-schema.js new file mode 100644 index 0000000..0c63dea --- /dev/null +++ b/schema/feature-schema.js @@ -0,0 +1,67 @@ +const Joi = require('joi'); + +const featureSchema = Joi.object().keys({ + name: Joi.string().required(), + description: Joi.string().optional(), + enabled: Joi.boolean().required(), + project: Joi.string().optional(), + type: Joi.string().optional(), + description: Joi.string().optional().allow(null), + impressionData: Joi.boolean().optional(), + stale: Joi.boolean().optional(), + dependencies: Joi.array().optional().items( + Joi.object().keys({ + feature: Joi.string().required(), + enabled: Joi.bool().optional(), + variants: Joi.array().items(Joi.string()).optional() + }) + ), + segments: Joi.array().items(Joi.number()), + strategies: Joi.array().items( + Joi.object().keys({ + name: Joi.string().required(), + parameters: Joi.object(), + variants: Joi.array().items( + Joi.object().keys({ + name: Joi.string().required(), + payload: Joi.object().required().keys({ + type: Joi.string().required(), + value: Joi.string().required().allow(""), + }).optional(), + weight: Joi.number().min(0).max(100000), + stickiness: Joi.string().optional(), + }) + ).optional(), + constraints: Joi.array().items( + Joi.object().keys({ + contextName: Joi.string().required(), + operator: Joi.string().required(), + values: Joi.array().items(Joi.string()), + caseInsensitive: Joi.bool().optional(), + inverted: Joi.bool().optional(), + value: Joi.string().optional() + }).optional() + ), + segments: Joi.array().items(Joi.number()).optional() + }) + ), + variants: Joi.array().items( + Joi.object().keys({ + name: Joi.string().required(), + payload: Joi.object().required().keys({ + type: Joi.string().required(), + value: Joi.string().required().allow(""), + }).optional(), + weight: Joi.number().min(0).max(100000), + stickiness: Joi.string().optional(), + overrides: Joi.array().items( + Joi.object().keys({ + contextName: Joi.string().required(), + values: Joi.array().items(Joi.string()), + }).optional() + ), + }) + ) +}); + +module.exports = featureSchema; diff --git a/schema/features-schema.js b/schema/features-schema.js index c369a4a..2f20a1e 100644 --- a/schema/features-schema.js +++ b/schema/features-schema.js @@ -1,85 +1,11 @@ const Joi = require('joi'); +const featureSchema = require('./feature-schema'); +const segmentSchema = require('./segment-schema'); -const schema = Joi.object().keys({ - version: Joi.number() - .min(1) - .required(), - features: Joi.array().items( - Joi.object().keys({ - name: Joi.string().required(), - description: Joi.string().optional(), - enabled: Joi.boolean().required(), - dependencies: Joi.array().optional().items( - Joi.object().keys({ - feature: Joi.string().required(), - enabled: Joi.bool().optional(), - variants: Joi.array().items(Joi.string()).optional() - }) - ), - strategies: Joi.array().items( - Joi.object().keys({ - name: Joi.string().required(), - parameters: Joi.object(), - variants: Joi.array().items( - Joi.object().keys({ - name: Joi.string().required(), - payload: Joi.object().required().keys({ - type: Joi.string().required(), - value: Joi.string().required().allow(""), - }).optional(), - weight: Joi.number().min(0).max(100000), - stickiness: Joi.string().optional(), - })).optional(), - constraints: Joi.array().items( - Joi.object().keys({ - contextName: Joi.string().required(), - operator: Joi.string().required(), - values: Joi.array().items(Joi.string()), - caseInsensitive: Joi.bool().optional(), - inverted: Joi.bool().optional(), - value: Joi.string().optional() - }).optional() - ), - segments: Joi.array().items(Joi.number()).optional() - }) - ), - variants: Joi.array().items( - Joi.object().keys({ - name: Joi.string().required(), - payload: Joi.object().required().keys({ - type: Joi.string().required(), - value: Joi.string().required().allow(""), - }).optional(), - weight: Joi.number().min(0).max(100000), - stickiness: Joi.string().optional(), - overrides: Joi.array().items( - Joi - .object() - .keys({ - contextName: Joi.string().required(), - values: Joi.array().items(Joi.string()), - }) - .optional() - ), - }) - ) - }) - ), - segments: Joi.array().items( - Joi.object().keys({ - id: Joi.number().required(), - constraints: Joi.array().items( - Joi.object().keys({ - contextName: Joi.string().required(), - operator: Joi.string().required(), - values: Joi.array().items(Joi.string()), - caseInsensitive: Joi.bool().optional(), - inverted: Joi.bool().optional(), - value: Joi.string().optional() - }).optional() - ), - }).optional() - ) +const featuresSchema = Joi.object().keys({ + version: Joi.number().min(1).required(), + features: Joi.array().items(featureSchema), + segments: Joi.array().items(segmentSchema) }); -module.exports = schema; +module.exports = featuresSchema; \ No newline at end of file diff --git a/schema/segment-schema.js b/schema/segment-schema.js new file mode 100644 index 0000000..7f3b881 --- /dev/null +++ b/schema/segment-schema.js @@ -0,0 +1,18 @@ +const Joi = require('joi'); + +const segmentSchema = Joi.object().keys({ + id: Joi.number().required(), + name: Joi.string().optional().allow(null), + constraints: Joi.array().items( + Joi.object().keys({ + contextName: Joi.string().required(), + operator: Joi.string().required(), + values: Joi.array().items(Joi.string()), + caseInsensitive: Joi.bool().optional(), + inverted: Joi.bool().optional(), + value: Joi.string().optional() + }).optional() + ), +}); + +module.exports = segmentSchema; diff --git a/schema/test-case-schema.js b/schema/test-case-schema.js index b0201dd..1d44127 100644 --- a/schema/test-case-schema.js +++ b/schema/test-case-schema.js @@ -1,10 +1,11 @@ const Joi = require('joi'); const featuresSchema = require('./features-schema.js'); +const deltasSchema = require('./deltas-schema.js'); const contextSchema = require('./context-schema.js'); const schema = Joi.object().keys({ name: Joi.string(), - state: featuresSchema, + state: Joi.alternatives().try(featuresSchema, deltasSchema).required(), tests: Joi.array().items( Joi.object().keys({ description: Joi.string().required(), diff --git a/specifications/19-delta-api-hydration.json b/specifications/19-delta-api-hydration.json new file mode 100644 index 0000000..cf49ed7 --- /dev/null +++ b/specifications/19-delta-api-hydration.json @@ -0,0 +1,67 @@ +{ + "name": "19-delta-api-hydration", + "state": { + "events": [ + { + "eventId": 1, + "type": "hydration", + "segments": [ + { + "id": 1, + "name": "delta.api.hydration.segment", + "constraints": [ + { + "values": [ + "abc", + "edf", + "gbd" + ], + "inverted": false, + "operator": "IN", + "contextName": "appName", + "caseInsensitive": false + } + ] + } + ], + "features": [ + { + "impressionData": false, + "enabled": true, + "name": "delta.api.hydration", + "description": null, + "project": "default", + "stale": false, + "type": "release", + "variants": [], + "segments": [ + 1 + ], + "strategies": [ + { + "name": "flexibleRollout", + "constraints": [], + "parameters": { + "groupId": "delta.api.hydration", + "rollout": "100", + "stickiness": "default" + }, + "variants": [] + } + ] + } + ] + } + ] + }, + "tests": [ + { + "description": "Hydration feature should be enabled when context criteria are met", + "context": { + "appName": "abc" + }, + "toggleName": "delta.api.hydration", + "expectedResult": true + } + ] +} \ No newline at end of file diff --git a/specifications/20-delta-api-events.json b/specifications/20-delta-api-events.json new file mode 100644 index 0000000..24bdf46 --- /dev/null +++ b/specifications/20-delta-api-events.json @@ -0,0 +1,111 @@ +{ + "name": "19-delta-api-hydration", + "state": { + "events": [ + { + "eventId": 1, + "type": "feature-updated", + "feature": { + "impressionData": false, + "enabled": true, + "name": "delta.api.events.updated.1", + "description": null, + "project": "default", + "stale": false, + "type": "release", + "variants": [], + "strategies": [ + { + "name": "default" + } + ] + } + }, + { + "eventId": 2, + "type": "feature-updated", + "feature": { + "impressionData": false, + "enabled": true, + "name": "delta.api.events.updated.2", + "description": null, + "project": "default", + "stale": false, + "type": "release", + "variants": [], + "strategies": [ + { + "name": "default" + } + ] + } + }, + { + "eventId": 3, + "type": "feature-removed", + "featureName": "delta.api.events.updated.2" + }, + { + "eventId": 4, + "type": "segment-updated", + "segment": { + "id": 1, + "name": "my-segment", + "constraints": [ + { + "values": [ + "abc", + "edf", + "gbd" + ], + "inverted": false, + "operator": "IN", + "contextName": "appName", + "caseInsensitive": false + } + ] + } + }, + { + "eventId": 5, + "type": "feature-updated", + "feature": { + "impressionData": false, + "enabled": true, + "name": "delta.api.events.updated.3", + "description": null, + "project": "default", + "stale": false, + "type": "release", + "variants": [], + "strategies": [ + { + "name": "default", + "segments": [1] + } + ] + } + } + ] + }, + "tests": [ + { + "description": "delta.api.events.updated.1 should be enabled", + "context": {}, + "toggleName": "delta.api.events.updated.1", + "expectedResult": true + }, + { + "description": "delta.api.events.updated.2 should be disabled", + "context": {}, + "toggleName": "delta.api.events.updated.2", + "expectedResult": false + }, + { + "description": "delta.api.events.updated.3 should be enabled with segment", + "context": { "appName": "abc" }, + "toggleName": "delta.api.events.updated.3", + "expectedResult": true + } + ] +} diff --git a/specifications/index.json b/specifications/index.json index 1f1a999..d2c446c 100644 --- a/specifications/index.json +++ b/specifications/index.json @@ -16,5 +16,7 @@ "15-global-constraints.json", "16-strategy-variants.json", "17-dependent-features.json", - "18-utf8-flag-names.json" -] + "18-utf8-flag-names.json", + "19-delta-api-hydration.json", + "20-delta-api-events.json" +] \ No newline at end of file