functional-models 1.0.25 → 1.1.0

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.
Files changed (42) hide show
  1. package/.eslintignore +1 -0
  2. package/.eslintrc +9 -15
  3. package/cucumber.js +10 -0
  4. package/features/arrayFields.feature +7 -7
  5. package/features/basic-ts.feature +13 -0
  6. package/features/functions.feature +2 -3
  7. package/package.json +34 -10
  8. package/src/constants.ts +15 -0
  9. package/src/errors.ts +18 -0
  10. package/src/index.ts +11 -0
  11. package/src/interfaces.ts +323 -0
  12. package/src/lazy.ts +24 -0
  13. package/src/methods.ts +30 -0
  14. package/src/models.ts +183 -0
  15. package/src/properties.ts +375 -0
  16. package/src/serialization.ts +39 -0
  17. package/src/utils.ts +42 -0
  18. package/src/validation.ts +390 -0
  19. package/{features/stepDefinitions/steps.js → stepDefinitions/oldSteps.ts} +76 -53
  20. package/stepDefinitions/tssteps.ts +107 -0
  21. package/test/src/errors.test.ts +31 -0
  22. package/test/src/{lazy.test.js → lazy.test.ts} +4 -4
  23. package/test/src/methods.test.ts +45 -0
  24. package/test/src/models.test.ts +417 -0
  25. package/test/src/{properties.test.js → properties.test.ts} +257 -64
  26. package/test/src/serialization.test.ts +80 -0
  27. package/test/src/{utils.test.js → utils.test.ts} +29 -7
  28. package/test/src/{validation.test.js → validation.test.ts} +278 -210
  29. package/tsconfig.json +100 -0
  30. package/src/functions.js +0 -7
  31. package/src/index.js +0 -7
  32. package/src/lazy.js +0 -19
  33. package/src/models.js +0 -150
  34. package/src/properties.js +0 -261
  35. package/src/serialization.js +0 -47
  36. package/src/utils.js +0 -42
  37. package/src/validation.js +0 -274
  38. package/test/base/index.test.js +0 -5
  39. package/test/src/functions.test.js +0 -45
  40. package/test/src/index.test.js +0 -5
  41. package/test/src/models.test.js +0 -380
  42. package/test/src/serialization.test.js +0 -127
@@ -0,0 +1,390 @@
1
+ import isEmpty from 'lodash/isEmpty'
2
+ import merge from 'lodash/merge'
3
+ import flatMap from 'lodash/flatMap'
4
+ import {
5
+ FunctionalModel,
6
+ ModelInstance,
7
+ Model,
8
+ ModelComponentValidator,
9
+ PropertyValidatorComponent,
10
+ PropertyValidatorComponentType,
11
+ PropertyValidatorComponentSync,
12
+ PropertyValidatorComponentTypeAdvanced,
13
+ PropertyValidator,
14
+ PropertyConfig,
15
+ MaybeFunction,
16
+ PropertyValidators,
17
+ ValueGetter,
18
+ Arrayable,
19
+ FunctionalType,
20
+ ModelErrors,
21
+ } from './interfaces'
22
+
23
+ const TYPE_PRIMITIVES = {
24
+ boolean: 'boolean',
25
+ string: 'string',
26
+ object: 'object',
27
+ number: 'number',
28
+ integer: 'integer',
29
+ }
30
+
31
+ const filterEmpty = <T>(
32
+ array: readonly (T | undefined | null)[]
33
+ ): readonly T[] => {
34
+ return array.filter(x => x) as readonly T[]
35
+ }
36
+
37
+ const _trueOrError =
38
+ (method: Function, error: string): PropertyValidatorComponentSync =>
39
+ (value: any) => {
40
+ if (method(value) === false) {
41
+ return error
42
+ }
43
+ return undefined
44
+ }
45
+
46
+ const _typeOrError =
47
+ (type: string, errorMessage: string): PropertyValidatorComponentSync =>
48
+ (value: any) => {
49
+ if (typeof value !== type) {
50
+ return errorMessage
51
+ }
52
+ return undefined
53
+ }
54
+
55
+ const isType =
56
+ (type: string): PropertyValidatorComponentSync =>
57
+ (value: any) => {
58
+ // @ts-ignore
59
+ return _typeOrError(type, `Must be a ${type}`)(value)
60
+ }
61
+ const isNumber = isType('number')
62
+ const isInteger = _trueOrError(Number.isInteger, 'Must be an integer')
63
+
64
+ const isBoolean = isType('boolean')
65
+ const isString = isType('string')
66
+ const isArray = _trueOrError(
67
+ (v: any) => Array.isArray(v),
68
+ 'Value is not an array'
69
+ )
70
+
71
+ const PRIMITIVE_TO_SPECIAL_TYPE_VALIDATOR = {
72
+ [TYPE_PRIMITIVES.boolean]: isBoolean,
73
+ [TYPE_PRIMITIVES.string]: isString,
74
+ [TYPE_PRIMITIVES.integer]: isInteger,
75
+ [TYPE_PRIMITIVES.number]: isNumber,
76
+ }
77
+
78
+ const arrayType =
79
+ (type: string): PropertyValidatorComponentSync =>
80
+ (value: Arrayable<FunctionalType>) => {
81
+ // @ts-ignore
82
+ const arrayError = isArray(value)
83
+ if (arrayError) {
84
+ return arrayError
85
+ }
86
+ const validator = PRIMITIVE_TO_SPECIAL_TYPE_VALIDATOR[type] || isType(type)
87
+ return (value as readonly []).reduce(
88
+ (acc: string | undefined, v: FunctionalType) => {
89
+ if (acc) {
90
+ return acc
91
+ }
92
+ // @ts-ignore
93
+ return validator(v)
94
+ },
95
+ undefined
96
+ )
97
+ }
98
+
99
+ const meetsRegex =
100
+ (
101
+ regex: string | RegExp,
102
+ flags?: string,
103
+ errorMessage: string = 'Format was invalid'
104
+ ): PropertyValidatorComponentSync =>
105
+ (value: FunctionalType) => {
106
+ const reg = new RegExp(regex, flags)
107
+ // @ts-ignore
108
+ return _trueOrError((v: string) => reg.test(v), errorMessage)(value)
109
+ }
110
+
111
+ const choices =
112
+ (choiceArray: readonly FunctionalType[]): PropertyValidatorComponentSync =>
113
+ (value: Arrayable<FunctionalType>) => {
114
+ if (Array.isArray(value)) {
115
+ const bad = value.find(v => !choiceArray.includes(v))
116
+ if (bad) {
117
+ return `${bad} is not a valid choice`
118
+ }
119
+ } else {
120
+ if (!choiceArray.includes(value as FunctionalType)) {
121
+ return `${value} is not a valid choice`
122
+ }
123
+ }
124
+ return undefined
125
+ }
126
+
127
+ const isDate: PropertyValidatorComponentType<Date> = (value: Date) => {
128
+ if (!value) {
129
+ return 'Date value is empty'
130
+ }
131
+ if (!value.toISOString) {
132
+ return 'Value is not a date'
133
+ }
134
+ return undefined
135
+ }
136
+
137
+ const isRequired: PropertyValidatorComponentSync = (value?: any) => {
138
+ if (value === true || value === false) {
139
+ return undefined
140
+ }
141
+ // @ts-ignore
142
+ if (isNumber(value) === undefined) {
143
+ return undefined
144
+ }
145
+ const empty = isEmpty(value)
146
+ if (empty) {
147
+ // @ts-ignore
148
+ if (isDate(value)) {
149
+ return 'A value is required'
150
+ }
151
+ }
152
+ return undefined
153
+ }
154
+
155
+ const maxNumber =
156
+ (max: Number): PropertyValidatorComponentType<number> =>
157
+ (value: number) => {
158
+ // @ts-ignore
159
+ const numberError = isNumber(value)
160
+ if (numberError) {
161
+ return numberError
162
+ }
163
+ if (value && value > max) {
164
+ return `The maximum is ${max}`
165
+ }
166
+ return undefined
167
+ }
168
+
169
+ const minNumber =
170
+ (min: Number): PropertyValidatorComponentType<number> =>
171
+ (value: Number) => {
172
+ // @ts-ignore
173
+ const numberError = isNumber(value)
174
+ if (numberError) {
175
+ return numberError
176
+ }
177
+ if (value && value < min) {
178
+ return `The minimum is ${min}`
179
+ }
180
+ return undefined
181
+ }
182
+
183
+ const maxTextLength =
184
+ (max: Number): PropertyValidatorComponentType<string> =>
185
+ (value: string) => {
186
+ // @ts-ignore
187
+ const stringError = isString(value)
188
+ if (stringError) {
189
+ return stringError
190
+ }
191
+ if (value && value.length > max) {
192
+ return `The maximum length is ${max}`
193
+ }
194
+ return undefined
195
+ }
196
+
197
+ const minTextLength =
198
+ (min: Number): PropertyValidatorComponentType<string> =>
199
+ (value: string) => {
200
+ // @ts-ignore
201
+ const stringError = isString(value)
202
+ if (stringError) {
203
+ return stringError
204
+ }
205
+ if (value && value.length < min) {
206
+ return `The minimum length is ${min}`
207
+ }
208
+ return undefined
209
+ }
210
+
211
+ const referenceTypeMatch = <TModel extends FunctionalModel>(
212
+ referencedModel: MaybeFunction<Model<TModel>>
213
+ ): PropertyValidatorComponentTypeAdvanced<ModelInstance<TModel>, TModel> => {
214
+ return (value?: ModelInstance<TModel>) => {
215
+ if (!value) {
216
+ return 'Must include a value'
217
+ }
218
+ // This needs to stay here, as it delays the creation long enough for
219
+ // self referencing types.
220
+ const model =
221
+ typeof referencedModel === 'function'
222
+ ? referencedModel()
223
+ : referencedModel
224
+ // Assumption: By the time this is received, value === a model instance.
225
+ const eModel = model.getName()
226
+ const aModel = value.getModel().getName()
227
+ if (eModel !== aModel) {
228
+ return `Model should be ${eModel} instead, received ${aModel}`
229
+ }
230
+ return undefined
231
+ }
232
+ }
233
+
234
+ const aggregateValidator = (
235
+ value: any,
236
+ methodOrMethods:
237
+ | PropertyValidatorComponent
238
+ | readonly PropertyValidatorComponent[]
239
+ ) => {
240
+ const toDo = Array.isArray(methodOrMethods)
241
+ ? methodOrMethods
242
+ : [methodOrMethods]
243
+
244
+ const _aggregativeValidator: PropertyValidator = async (
245
+ instance: ModelInstance<any>,
246
+ instanceData: FunctionalModel
247
+ ) => {
248
+ const values = await Promise.all(
249
+ toDo.map(method => {
250
+ return method(value, instance, instanceData)
251
+ })
252
+ )
253
+ return filterEmpty(values)
254
+ }
255
+ return _aggregativeValidator
256
+ }
257
+
258
+ const emptyValidator: PropertyValidatorComponentSync = () => undefined
259
+
260
+ const _boolChoice =
261
+ (method: (configValue: any) => PropertyValidatorComponentSync) =>
262
+ (configValue: any) => {
263
+ const func = method(configValue)
264
+ const validatorWrapper: PropertyValidatorComponentSync = (
265
+ value: any,
266
+ modelInstance: ModelInstance<any>,
267
+ modelData: FunctionalModel
268
+ ) => {
269
+ return func(value, modelInstance, modelData)
270
+ }
271
+ return validatorWrapper
272
+ }
273
+
274
+ type MethodConfigDict = {
275
+ readonly [s: string]: (config: any) => PropertyValidatorComponentSync
276
+ }
277
+
278
+ const simpleFuncWrap = (validator: PropertyValidatorComponentSync) => () => {
279
+ return validator
280
+ }
281
+
282
+ const CONFIG_TO_VALIDATE_METHOD: MethodConfigDict = {
283
+ required: _boolChoice(simpleFuncWrap(isRequired)),
284
+ isInteger: _boolChoice(simpleFuncWrap(isInteger)),
285
+ isNumber: _boolChoice(simpleFuncWrap(isNumber)),
286
+ isString: _boolChoice(simpleFuncWrap(isString)),
287
+ isArray: _boolChoice(simpleFuncWrap(isArray)),
288
+ isBoolean: _boolChoice(simpleFuncWrap(isBoolean)),
289
+ choices: _boolChoice(choices),
290
+ }
291
+
292
+ const createPropertyValidator = (
293
+ valueGetter: ValueGetter,
294
+ config: PropertyConfig
295
+ ): PropertyValidator => {
296
+ const _propertyValidator: PropertyValidator = async (
297
+ instance,
298
+ instanceData: FunctionalModel
299
+ ) => {
300
+ if (!config) {
301
+ config = {}
302
+ }
303
+ const validators: readonly PropertyValidatorComponent[] = [
304
+ ...Object.entries(config).map(([key, value]) => {
305
+ const method = CONFIG_TO_VALIDATE_METHOD[key]
306
+ if (method) {
307
+ return method(value)
308
+ }
309
+ return emptyValidator
310
+ }),
311
+ ...(config.validators ? config.validators : []),
312
+ ].filter(x => x)
313
+ const value = await valueGetter()
314
+ const isRequiredValue = config.required
315
+ ? true
316
+ : validators.includes(isRequired)
317
+ if (!value && !isRequiredValue) {
318
+ return []
319
+ }
320
+ const validator = aggregateValidator(value, validators)
321
+ const errors = await validator(instance, instanceData)
322
+ return [...new Set(flatMap(errors))]
323
+ }
324
+ return _propertyValidator
325
+ }
326
+
327
+ const createModelValidator = (
328
+ validators: PropertyValidators,
329
+ modelValidators?: readonly ModelComponentValidator[]
330
+ ) => {
331
+ const _modelValidator = async (
332
+ instance: ModelInstance<any>,
333
+ options: object
334
+ ): Promise<ModelErrors> => {
335
+ return Promise.resolve().then(async () => {
336
+ if (!instance) {
337
+ throw new Error(`Instance cannot be empty`)
338
+ }
339
+ const keysAndFunctions = Object.entries(validators)
340
+ const instanceData = await instance.toObj()
341
+ const propertyValidationErrors = await Promise.all(
342
+ keysAndFunctions.map(async ([key, validator]) => {
343
+ return [key, await validator(instance, instanceData)]
344
+ })
345
+ )
346
+ const modelValidationErrors = (
347
+ await Promise.all(
348
+ modelValidators
349
+ ? modelValidators.map(validator =>
350
+ validator(instance, instanceData, options)
351
+ )
352
+ : []
353
+ )
354
+ ).filter(x => x)
355
+ const propertyErrors = propertyValidationErrors
356
+ .filter(([, errors]) => Boolean(errors) && errors.length > 0)
357
+ .reduce((acc, [key, errors]) => {
358
+ return merge(acc, { [String(key)]: errors })
359
+ }, {})
360
+ return modelValidationErrors.length > 0
361
+ ? merge(propertyErrors, { overall: modelValidationErrors })
362
+ : propertyErrors
363
+ })
364
+ }
365
+ return _modelValidator
366
+ }
367
+
368
+ export {
369
+ isNumber,
370
+ isBoolean,
371
+ isString,
372
+ isInteger,
373
+ isType,
374
+ isDate,
375
+ isArray,
376
+ isRequired,
377
+ maxNumber,
378
+ minNumber,
379
+ choices,
380
+ maxTextLength,
381
+ minTextLength,
382
+ meetsRegex,
383
+ aggregateValidator,
384
+ emptyValidator,
385
+ createPropertyValidator,
386
+ createModelValidator,
387
+ arrayType,
388
+ referenceTypeMatch,
389
+ TYPE_PRIMITIVES,
390
+ }
@@ -1,72 +1,90 @@
1
- const assert = require('chai').assert
2
- const flatMap = require('lodash/flatMap')
3
- const { Given, When, Then } = require('@cucumber/cucumber')
4
- const {
5
- Model,
1
+ import { assert } from 'chai'
2
+ import flatMap from 'lodash/flatMap'
3
+ import { Given, When, Then } from '@cucumber/cucumber'
4
+ import {
5
+ BaseModel,
6
6
  UniqueId,
7
7
  TextProperty,
8
- Function,
9
8
  Property,
9
+ WrapperModelMethod,
10
+ WrapperInstanceMethod,
10
11
  ArrayProperty,
11
12
  validation,
12
- } = require('../../index')
13
+ } from '../src'
14
+ import { ModelInstanceMethod, ModelMethod } from '../src/interfaces'
13
15
 
14
- const instanceToString = Function(modelInstance => {
16
+ const instanceToString = WrapperInstanceMethod(modelInstance => {
15
17
  return `${modelInstance.getModel().getName()}-Instance`
16
18
  })
17
19
 
18
- const instanceToJson = Function(async modelInstance => {
19
- return JSON.stringify(await modelInstance.functions.toObj())
20
+ const instanceToJson = WrapperInstanceMethod(async modelInstance => {
21
+ return JSON.stringify(await modelInstance.toObj())
20
22
  })
21
23
 
22
- const modelToString = Function(model => {
23
- return `${model.getName()}-[${Object.keys(model.getProperties()).join(',')}]`
24
+ const modelToString = WrapperModelMethod(model => {
25
+ return `${model.getName()}-[${Object.keys(
26
+ model.getModelDefinition().properties
27
+ ).join(',')}]`
24
28
  })
25
29
 
26
- const modelWrapper = Function(model => {
30
+ const modelWrapper = WrapperModelMethod(model => {
27
31
  return model
28
32
  })
29
33
 
30
34
  const MODEL_DEFINITIONS = {
31
- FunctionModel1: Model(
32
- 'FunctionModel1',
33
- {
35
+ FunctionModel1: BaseModel<{
36
+ name: string
37
+ modelWrapper: ModelMethod
38
+ toString: ModelInstanceMethod
39
+ toJson: ModelInstanceMethod
40
+ }>('FunctionModel1', {
41
+ properties: {
34
42
  id: UniqueId({ required: true }),
35
43
  name: TextProperty({ required: true }),
36
44
  },
45
+ modelMethods: {
46
+ modelWrapper,
47
+ },
48
+ instanceMethods: {
49
+ toString: instanceToString,
50
+ toJson: instanceToJson,
51
+ },
52
+ }),
53
+ TestModel1: BaseModel<{ name: string; type: string; flag: number }>(
54
+ 'TestModel1',
37
55
  {
38
- modelFunctions: {
39
- modelWrapper,
40
- toString: modelToString,
41
- },
42
- instanceFunctions: {
43
- toString: instanceToString,
44
- toJson: instanceToJson,
56
+ properties: {
57
+ name: Property('Text', { required: true }),
58
+ type: Property('Type', { required: true, isString: true }),
59
+ flag: Property('Flag', { required: true, isNumber: true }),
45
60
  },
46
61
  }
47
62
  ),
48
- TestModel1: Model('TestModel1', {
49
- name: Property({ required: true }),
50
- type: Property({ required: true, isString: true }),
51
- flag: Property({ required: true, isNumber: true }),
52
- }),
53
- ArrayModel1: Model('ArrayModel1', {
54
- ArrayProperty: Property({
55
- isArray: true,
56
- validators: [validation.arrayType(validation.TYPE_PRIMATIVES.integer)],
57
- }),
63
+ ArrayModel1: BaseModel<{ ArrayProperty: readonly number[] }>('ArrayModel1', {
64
+ properties: {
65
+ ArrayProperty: Property('Array', {
66
+ isArray: true,
67
+ validators: [validation.arrayType(validation.TYPE_PRIMITIVES.integer)],
68
+ }),
69
+ },
58
70
  }),
59
- ArrayModel2: Model('ArrayModel2', {
60
- ArrayProperty: Property({ isArray: true }),
71
+ ArrayModel2: BaseModel('ArrayModel2', {
72
+ properties: {
73
+ ArrayProperty: Property('Array', { isArray: true }),
74
+ },
61
75
  }),
62
- ArrayModel3: Model('ArrayModel3', {
63
- ArrayProperty: ArrayProperty({}),
76
+ ArrayModel3: BaseModel('ArrayModel3', {
77
+ properties: {
78
+ ArrayProperty: ArrayProperty({}),
79
+ },
64
80
  }),
65
- ArrayModel4: Model('ArrayModel4', {
66
- ArrayProperty: ArrayProperty({
67
- choices: [4, 5, 6],
68
- validators: [validation.arrayType(validation.TYPE_PRIMATIVES.integer)],
69
- }),
81
+ ArrayModel4: BaseModel('ArrayModel4', {
82
+ properties: {
83
+ ArrayProperty: ArrayProperty({
84
+ choices: [4, 5, 6],
85
+ validators: [validation.arrayType(validation.TYPE_PRIMITIVES.integer)],
86
+ }),
87
+ },
70
88
  }),
71
89
  }
72
90
 
@@ -106,15 +124,17 @@ const MODEL_INPUT_VALUES = {
106
124
  }
107
125
 
108
126
  const EXPECTED_FIELDS = {
109
- TestModel1b: ['getName', 'getType', 'getFlag', 'meta', 'functions'],
127
+ TestModel1b: ['name', 'type', 'flag'],
110
128
  }
111
129
 
112
130
  Given(
113
131
  'the {word} has been created, with {word} inputs provided',
114
132
  function (modelDefinition, modelInputValues) {
133
+ // @ts-ignore
115
134
  const def = MODEL_DEFINITIONS[modelDefinition]
116
135
  this.model = def
117
136
 
137
+ // @ts-ignore
118
138
  const input = MODEL_INPUT_VALUES[modelInputValues]
119
139
  if (!def) {
120
140
  throw new Error(`${modelDefinition} did not result in a definition`)
@@ -127,7 +147,8 @@ Given(
127
147
  )
128
148
 
129
149
  When('functions.validate is called', function () {
130
- return this.instance.functions.validate().then(x => {
150
+ // @ts-ignore
151
+ return this.instance.validate().then(x => {
131
152
  this.errors = x
132
153
  })
133
154
  })
@@ -141,6 +162,7 @@ Then('an array of {int} errors is shown', function (errorCount) {
141
162
  })
142
163
 
143
164
  Given('{word} model is used', function (modelDefinition) {
165
+ // @ts-ignore
144
166
  const def = MODEL_DEFINITIONS[modelDefinition]
145
167
  if (!def) {
146
168
  throw new Error(`${modelDefinition} did not result in a definition`)
@@ -150,6 +172,7 @@ Given('{word} model is used', function (modelDefinition) {
150
172
  })
151
173
 
152
174
  When('{word} data is inserted', function (modelInputValues) {
175
+ // @ts-ignore
153
176
  const input = MODEL_INPUT_VALUES[modelInputValues]
154
177
  if (!input) {
155
178
  throw new Error(`${modelInputValues} did not result in an input`)
@@ -158,36 +181,36 @@ When('{word} data is inserted', function (modelInputValues) {
158
181
  })
159
182
 
160
183
  Then('{word} expected property is found', function (properties) {
184
+ // @ts-ignore
161
185
  const propertyArray = EXPECTED_FIELDS[properties]
162
186
  if (!propertyArray) {
163
187
  throw new Error(`${properties} did not result in properties`)
164
188
  }
189
+ // @ts-ignore
165
190
  propertyArray.forEach(key => {
166
- if (!(key in this.instance)) {
191
+ if (!(key in this.instance.get)) {
167
192
  throw new Error(`Did not find ${key} in model`)
168
193
  }
169
194
  })
170
195
  })
171
196
 
172
197
  Then('the {word} property is called on the model', function (property) {
173
- return this.instance[property]().then(result => {
174
- this.results = result
175
- })
198
+ this.results = this.instance.get[property]()
176
199
  })
177
200
 
178
- Then('the array values match', function (table) {
201
+ Then('the array values match', async function (table) {
179
202
  const expected = JSON.parse(table.rowsHash().array)
180
- assert.deepEqual(this.results, expected)
203
+ assert.deepEqual(await this.results, expected)
181
204
  })
182
205
 
183
206
  Then('{word} property is found', function (propertyKey) {
184
- assert.isFunction(this.instance[propertyKey])
207
+ assert.isFunction(this.instance.get[propertyKey])
185
208
  })
186
209
 
187
210
  Then('{word} instance function is found', function (instanceFunctionKey) {
188
- assert.isFunction(this.instance.functions[instanceFunctionKey])
211
+ assert.isFunction(this.instance.methods[instanceFunctionKey])
189
212
  })
190
213
 
191
214
  Then('{word} model function is found', function (modelFunctionKey) {
192
- assert.isFunction(this.model[modelFunctionKey])
215
+ assert.isFunction(this.model.methods[modelFunctionKey])
193
216
  })