1212"use strict" ;
1313
1414// TODO locale for validation messages
15- // TODO custom validation should be configurable in yaml file
15+ // TODO document all validations scopes, regex, and module and remove allowedScopes
16+ // TODO document clearSubScopes option
1617
1718const { promisify } = require ( "util" ) ;
1819const path = require ( "path" ) ;
@@ -24,7 +25,7 @@ const { REDIS_INTEGRATION_MODE } = redis;
2425const { Logger } = require ( "./logger" ) ;
2526const { isOnCF, cfEnv } = require ( "./env" ) ;
2627const { HandlerCollection } = require ( "./shared/handlerCollection" ) ;
27- const { ENV , isObject } = require ( "./shared/static" ) ;
28+ const { ENV , isObject, tryRequire } = require ( "./shared/static" ) ;
2829const { promiseAllDone } = require ( "./shared/promiseAllDone" ) ;
2930const { LimitedLazyCache } = require ( "./shared/cache" ) ;
3031
@@ -42,21 +43,19 @@ const SCOPE_ROOT_KEY = "//";
4243const CONFIG_KEY = Object . freeze ( {
4344 TYPE : "TYPE" ,
4445 ACTIVE : "ACTIVE" ,
45- VALIDATION : "VALIDATION" ,
46- VALIDATION_REG_EXP : "VALIDATION_REG_EXP" ,
46+ VALIDATIONS : "VALIDATIONS" ,
47+ VALIDATIONS_SCOPES_MAP : "VALIDATIONS_SCOPES_MAP" ,
48+ VALIDATIONS_REG_EXP : "VALIDATIONS_REG_EXP" ,
4749 APP_URL : "APP_URL" ,
4850 APP_URL_ACTIVE : "APP_URL_ACTIVE" ,
49- ALLOWED_SCOPES : "ALLOWED_SCOPES" ,
50- ALLOWED_SCOPES_CHECK_MAP : "ALLOWED_SCOPES_CHECK_MAP" ,
5151} ) ;
5252
5353const CONFIG_INFO_KEY = {
5454 [ CONFIG_KEY . TYPE ] : true ,
5555 [ CONFIG_KEY . ACTIVE ] : true ,
56- [ CONFIG_KEY . VALIDATION ] : true ,
56+ [ CONFIG_KEY . VALIDATIONS ] : true ,
5757 [ CONFIG_KEY . APP_URL ] : true ,
5858 [ CONFIG_KEY . APP_URL_ACTIVE ] : true ,
59- [ CONFIG_KEY . ALLOWED_SCOPES ] : true ,
6059} ;
6160
6261const COMPONENT_NAME = "/FeatureToggles" ;
@@ -121,11 +120,11 @@ class FeatureToggles {
121120 /**
122121 * Populate this.__config.
123122 */
124- _processConfig ( config ) {
123+ _processConfig ( config , configFilepath ) {
125124 const { uris : cfAppUris } = cfEnv . cfApp ;
126125
127126 const configEntries = Object . entries ( config ) ;
128- for ( const [ featureKey , { type, active, appUrl, validation , fallbackValue, allowedScopes } ] of configEntries ) {
127+ for ( const [ featureKey , { type, active, appUrl, validations , fallbackValue } ] of configEntries ) {
129128 this . __featureKeys . push ( featureKey ) ;
130129 this . __fallbackValues [ featureKey ] = fallbackValue ;
131130 this . __config [ featureKey ] = { } ;
@@ -138,9 +137,76 @@ class FeatureToggles {
138137 this . __config [ featureKey ] [ CONFIG_KEY . ACTIVE ] = active ;
139138 }
140139
141- if ( validation ) {
142- this . __config [ featureKey ] [ CONFIG_KEY . VALIDATION ] = validation ;
143- this . __config [ featureKey ] [ CONFIG_KEY . VALIDATION_REG_EXP ] = new RegExp ( validation ) ;
140+ if ( validations ) {
141+ this . __config [ featureKey ] [ CONFIG_KEY . VALIDATIONS ] = validations ;
142+
143+ const workingDir = process . cwd ( ) ;
144+ const configDir = configFilepath ? path . dirname ( configFilepath ) : __dirname ;
145+ const { validationsScopesMap, validationsRegExp, validationsCode } = validations . reduce (
146+ ( acc , validation ) => {
147+ if ( Array . isArray ( validation . scopes ) ) {
148+ for ( const scope of validation . scopes ) {
149+ acc . validationsScopesMap [ scope ] = true ;
150+ }
151+ return acc ;
152+ }
153+
154+ if ( validation . regex ) {
155+ acc . validationsRegExp . push ( new RegExp ( validation . regex ) ) ;
156+ return acc ;
157+ }
158+
159+ if ( validation . module ) {
160+ let modulePath = validation . module . replace ( "$CONFIG_DIR" , configDir ) ;
161+ if ( ! path . isAbsolute ( modulePath ) ) {
162+ modulePath = path . join ( workingDir , modulePath ) ;
163+ }
164+ let validator = tryRequire ( modulePath ) ;
165+
166+ if ( validation . call ) {
167+ validator = validator ?. [ validation . call ] ;
168+ }
169+
170+ const validatorType = typeof validator ;
171+ if ( validatorType === "function" ) {
172+ acc . validationsCode . push ( validator ) ;
173+ } else {
174+ logger . warning (
175+ new VError (
176+ {
177+ name : VERROR_CLUSTER_NAME ,
178+ info : {
179+ featureKey,
180+ validation : JSON . stringify ( validation ) ,
181+ modulePath,
182+ validatorType,
183+ } ,
184+ } ,
185+ "could not load module validation"
186+ )
187+ ) ;
188+ }
189+ return acc ;
190+ }
191+
192+ throw new VError (
193+ {
194+ name : VERROR_CLUSTER_NAME ,
195+ info : {
196+ featureKey,
197+ validation : JSON . stringify ( validation ) ,
198+ } ,
199+ } ,
200+ "found invalid validation, only scopes, regex, and module validations are supported"
201+ ) ;
202+ } ,
203+ { validationsScopesMap : { } , validationsRegExp : [ ] , validationsCode : [ ] }
204+ ) ;
205+ this . __config [ featureKey ] [ CONFIG_KEY . VALIDATIONS_SCOPES_MAP ] = validationsScopesMap ;
206+ this . __config [ featureKey ] [ CONFIG_KEY . VALIDATIONS_REG_EXP ] = validationsRegExp ;
207+ for ( const validator of validationsCode ) {
208+ this . registerFeatureValueValidation ( featureKey , validator ) ;
209+ }
144210 }
145211
146212 if ( appUrl ) {
@@ -151,14 +217,6 @@ class FeatureToggles {
151217 ! Array . isArray ( cfAppUris ) ||
152218 cfAppUris . reduce ( ( accumulator , cfAppUri ) => accumulator && appUrlRegex . test ( cfAppUri ) , true ) ;
153219 }
154-
155- if ( Array . isArray ( allowedScopes ) ) {
156- this . __config [ featureKey ] [ CONFIG_KEY . ALLOWED_SCOPES ] = allowedScopes ;
157- this . __config [ featureKey ] [ CONFIG_KEY . ALLOWED_SCOPES_CHECK_MAP ] = allowedScopes . reduce ( ( acc , scope ) => {
158- acc [ scope ] = true ;
159- return acc ;
160- } , { } ) ;
161- }
162220 }
163221
164222 this . __isConfigProcessed = true ;
@@ -287,7 +345,7 @@ class FeatureToggles {
287345 } ,
288346 ] ;
289347 }
290- const allowedScopesCheckMap = this . __config [ featureKey ] [ CONFIG_KEY . ALLOWED_SCOPES_CHECK_MAP ] ;
348+ const validationsScopesMap = this . __config [ featureKey ] [ CONFIG_KEY . VALIDATIONS_SCOPES_MAP ] ;
291349 for ( const [ scope , value ] of Object . entries ( scopeMap ) ) {
292350 if ( ! FeatureToggles . _isValidScopeMapValue ( value ) ) {
293351 return [
@@ -298,7 +356,7 @@ class FeatureToggles {
298356 } ,
299357 ] ;
300358 }
301- if ( allowedScopesCheckMap && ! allowedScopesCheckMap [ scope ] ) {
359+ if ( validationsScopesMap && ! validationsScopesMap [ scope ] ) {
302360 return [
303361 {
304362 featureKey,
@@ -359,16 +417,19 @@ class FeatureToggles {
359417 ] ;
360418 }
361419
362- const validationRegExp = this . __config [ featureKey ] [ CONFIG_KEY . VALIDATION_REG_EXP ] ;
363- if ( validationRegExp && ! validationRegExp . test ( value ) ) {
364- return [
365- {
366- featureKey,
367- ...( scopeKey && { scopeKey } ) ,
368- errorMessage : 'value "{0}" does not match validation regular expression {1}' ,
369- errorMessageValues : [ value , this . __config [ featureKey ] [ CONFIG_KEY . VALIDATION ] ] ,
370- } ,
371- ] ;
420+ const validationsRegExp = this . __config [ featureKey ] [ CONFIG_KEY . VALIDATIONS_REG_EXP ] ;
421+ if ( Array . isArray ( validationsRegExp ) && validationsRegExp . length > 0 ) {
422+ const failingRegExp = validationsRegExp . find ( ( validationRegExp ) => ! validationRegExp . test ( value ) ) ;
423+ if ( failingRegExp ) {
424+ return [
425+ {
426+ featureKey,
427+ ...( scopeKey && { scopeKey } ) ,
428+ errorMessage : 'value "{0}" does not match validation regular expression {1}' ,
429+ errorMessageValues : [ value , failingRegExp . toString ( ) ] ,
430+ } ,
431+ ] ;
432+ }
372433 }
373434
374435 const validators = this . __featureValueValidators . getHandlers ( featureKey ) ;
@@ -609,7 +670,7 @@ class FeatureToggles {
609670
610671 let toggleCount ;
611672 try {
612- toggleCount = this . _processConfig ( config ) ;
673+ toggleCount = this . _processConfig ( config , configFilepath ) ;
613674 } catch ( err ) {
614675 throw new VError (
615676 {
0 commit comments