From 339323a0c8e1486a0c90aefb5473570791b2befb Mon Sep 17 00:00:00 2001 From: Chirill Ceban Date: Wed, 9 Jun 2021 14:45:09 +0300 Subject: [PATCH] feat: add case, singularize, pluralize and condenseWhitespace options to string - Add case (lowerCase, upperCase, camelCase, snakeCase, dashCase, pascalCase, capitalCase, sentenceCase, dotCase, dotCase, titleCase, noCase) options. - Add pluralize, singularize and condenseWhitespace options --- adonis-typings/validator.ts | 27 ++- src/Schema/index.ts | 8 +- src/Validations/primitives/string.ts | 74 +++++++- test/schema.spec.ts | 108 ++++++++++-- test/validations/string.spec.ts | 241 ++++++++++++++++++++++++++- 5 files changed, 436 insertions(+), 22 deletions(-) diff --git a/adonis-typings/validator.ts b/adonis-typings/validator.ts index 5f16d20..b9ceadb 100644 --- a/adonis-typings/validator.ts +++ b/adonis-typings/validator.ts @@ -262,12 +262,12 @@ declare module '@ioc:Adonis/Core/Validator' { * Signature to define a string or optional string type */ export interface StringType { - (options?: { escape?: boolean; trim?: boolean }, rules?: Rule[]): { + (options?: StringOptions, rules?: Rule[]): { t: string getTree(): SchemaLiteral } optional( - options?: { escape?: boolean; trim?: boolean }, + options?: StringOptions, rules?: Rule[] ): { t?: string @@ -577,6 +577,29 @@ declare module '@ioc:Adonis/Core/Validator' { stripWWW?: boolean } + /** + * String validation options + */ + export type StringOptions = { + case?: + | 'lowerCase' + | 'upperCase' + | 'camelCase' + | 'snakeCase' + | 'dashCase' + | 'pascalCase' + | 'capitalCase' + | 'sentenceCase' + | 'dotCase' + | 'titleCase' + | 'noCase' + pluralize?: boolean + singularize?: boolean + condenseWhitespace?: boolean + escape?: boolean + trim?: boolean + } + /** * List of available validation rules. The rules are not the validation * implementations, but instead a pointer to the validation implementation diff --git a/src/Schema/index.ts b/src/Schema/index.ts index 7182184..49f49f3 100644 --- a/src/Schema/index.ts +++ b/src/Schema/index.ts @@ -24,18 +24,16 @@ import { BooleanType, TypedSchema, ParsedSchemaTree, + StringOptions, } from '@ioc:Adonis/Core/Validator' /** * String schema type */ -function string(options?: { escape?: boolean; trim?: boolean }, rules?: Rule[]) { +function string(options?: StringOptions, rules?: Rule[]) { return getLiteralType('string', false, options, rules || []) as ReturnType } -string.optional = function optionalString( - options?: { escape?: boolean; trim?: boolean }, - rules?: Rule[] -) { +string.optional = function optionalString(options?: StringOptions, rules?: Rule[]) { return getLiteralType('string', true, options, rules || []) as ReturnType } diff --git a/src/Validations/primitives/string.ts b/src/Validations/primitives/string.ts index 5896e32..237be0d 100644 --- a/src/Validations/primitives/string.ts +++ b/src/Validations/primitives/string.ts @@ -8,7 +8,8 @@ */ import escape from 'validator/lib/escape' -import { SyncValidation } from '@ioc:Adonis/Core/Validator' +import { string as stringHelpers } from '@poppinss/utils/build/helpers' +import { StringOptions, SyncValidation } from '@ioc:Adonis/Core/Validator' import { wrapCompile } from '../../Validator/helpers' const DEFAULT_MESSAGE = 'string validation failed' @@ -18,10 +19,14 @@ const RULE_NAME = 'string' * Ensure value is a valid string * @type {SyncValidation} */ -export const string: SyncValidation<{ escape: boolean; trim: boolean }> = { +export const string: SyncValidation = { compile: wrapCompile(RULE_NAME, [], ([options]) => { return { compiledOptions: { + case: options && options.case, + singularize: !!(options && options.singularize), + pluralize: !!(options && options.pluralize), + condenseWhitespace: !!(options && options.condenseWhitespace), escape: !!(options && options.escape), trim: !!(options && options.trim), }, @@ -35,6 +40,71 @@ export const string: SyncValidation<{ escape: boolean; trim: boolean }> = { let mutated = false + /** + * Change string case + */ + if (compiledOptions.case) { + /** + * Using poppinss.utils + */ + if ( + [ + 'camelCase', + 'snakeCase', + 'dashCase', + 'pascalCase', + 'capitalCase', + 'sentenceCase', + 'dotCase', + 'titleCase', + 'noCase', + ].includes(compiledOptions.case) + ) { + mutated = true + value = stringHelpers[compiledOptions.case](value) + } + + /** + * To lowerCase + */ + if (compiledOptions.case === 'lowerCase') { + mutated = true + value = value.toLowerCase() + } + + /** + * To upperCase + */ + if (compiledOptions.case === 'upperCase') { + mutated = true + value = value.toUpperCase() + } + } + + /** + * Pluralize string + */ + if (compiledOptions.pluralize) { + mutated = true + value = stringHelpers.pluralize(value) + } + + /** + * Singularize string + */ + if (compiledOptions.singularize) { + mutated = true + value = stringHelpers.singularize(value) + } + + /** + * Condense white space + */ + if (compiledOptions.condenseWhitespace) { + mutated = true + value = stringHelpers.condenseWhitespace(value) + } + /** * Escape string */ diff --git a/test/schema.spec.ts b/test/schema.spec.ts index 04fe6bb..7422945 100644 --- a/test/schema.spec.ts +++ b/test/schema.spec.ts @@ -32,7 +32,14 @@ test.group('Schema | String', () => { name: 'string', allowUndefineds: false, async: false, - compiledOptions: { escape: false, trim: false }, + compiledOptions: { + escape: false, + trim: false, + case: undefined, + pluralize: false, + singularize: false, + condenseWhitespace: false, + }, }, ], }, @@ -54,7 +61,14 @@ test.group('Schema | String', () => { name: 'string', allowUndefineds: false, async: false, - compiledOptions: { escape: false, trim: false }, + compiledOptions: { + escape: false, + trim: false, + case: undefined, + pluralize: false, + singularize: false, + condenseWhitespace: false, + }, }, ], }, @@ -82,7 +96,14 @@ test.group('Schema | String', () => { name: 'string', allowUndefineds: false, async: false, - compiledOptions: { escape: false, trim: false }, + compiledOptions: { + escape: false, + trim: false, + case: undefined, + pluralize: false, + singularize: false, + condenseWhitespace: false, + }, }, { name: 'alpha', @@ -118,7 +139,14 @@ test.group('Schema | String', () => { name: 'string', allowUndefineds: false, async: false, - compiledOptions: { escape: true, trim: false }, + compiledOptions: { + escape: true, + trim: false, + case: undefined, + pluralize: false, + singularize: false, + condenseWhitespace: false, + }, }, ], }, @@ -146,7 +174,14 @@ test.group('Schema | String', () => { name: 'string', allowUndefineds: false, async: false, - compiledOptions: { escape: true, trim: true }, + compiledOptions: { + escape: true, + trim: true, + case: undefined, + pluralize: false, + singularize: false, + condenseWhitespace: false, + }, }, ], }, @@ -543,7 +578,14 @@ test.group('Schema | Object', () => { name: 'string', allowUndefineds: false, async: false, - compiledOptions: { escape: false, trim: false }, + compiledOptions: { + escape: false, + trim: false, + case: undefined, + pluralize: false, + singularize: false, + condenseWhitespace: false, + }, }, ], }, @@ -586,7 +628,14 @@ test.group('Schema | Object', () => { name: 'string', allowUndefineds: false, async: false, - compiledOptions: { escape: false, trim: false }, + compiledOptions: { + escape: false, + trim: false, + case: undefined, + pluralize: false, + singularize: false, + condenseWhitespace: false, + }, }, ], }, @@ -682,7 +731,14 @@ test.group('Schema | Array', () => { name: 'string', allowUndefineds: false, async: false, - compiledOptions: { escape: false, trim: false }, + compiledOptions: { + escape: false, + trim: false, + case: undefined, + pluralize: false, + singularize: false, + condenseWhitespace: false, + }, }, ], }, @@ -744,7 +800,14 @@ test.group('Schema | Array', () => { name: 'string', allowUndefineds: false, async: false, - compiledOptions: { escape: false, trim: false }, + compiledOptions: { + escape: false, + trim: false, + case: undefined, + pluralize: false, + singularize: false, + condenseWhitespace: false, + }, }, ], }, @@ -818,7 +881,14 @@ test.group('Schema | Array', () => { name: 'string', allowUndefineds: false, async: false, - compiledOptions: { escape: false, trim: false }, + compiledOptions: { + escape: false, + trim: false, + case: undefined, + pluralize: false, + singularize: false, + condenseWhitespace: false, + }, }, ], }, @@ -913,7 +983,14 @@ test.group('Schema | Array', () => { name: 'string', allowUndefineds: false, async: false, - compiledOptions: { escape: false, trim: false }, + compiledOptions: { + escape: false, + trim: false, + case: undefined, + pluralize: false, + singularize: false, + condenseWhitespace: false, + }, }, ], }, @@ -952,7 +1029,14 @@ test.group('Schema | Array', () => { name: 'string', allowUndefineds: false, async: false, - compiledOptions: { escape: false, trim: false }, + compiledOptions: { + escape: false, + trim: false, + case: undefined, + pluralize: false, + singularize: false, + condenseWhitespace: false, + }, }, ], }, diff --git a/test/validations/string.spec.ts b/test/validations/string.spec.ts index 5f11c33..a8b1ade 100644 --- a/test/validations/string.spec.ts +++ b/test/validations/string.spec.ts @@ -13,8 +13,9 @@ import { validate } from '../fixtures/rules/index' import { MessagesBag } from '../../src/MessagesBag' import { ApiErrorReporter } from '../../src/ErrorReporter' import { string } from '../../src/Validations/primitives/string' +import { StringOptions } from '@ioc:Adonis/Core/Validator' -function compile(options?: { escape: true }) { +function compile(options?: StringOptions) { return string.compile('literal', 'string', rules['string'](options).options, {}) } @@ -82,6 +83,244 @@ test.group('String', () => { assert.deepEqual(reporter.toJSON(), { errors: [] }) }) + test('lowerCase string when enabled', (assert) => { + const reporter = new ApiErrorReporter(new MessagesBag({}), false) + let value = 'Hello-World' + + string.validate(value, compile({ case: 'lowerCase' }).compiledOptions, { + errorReporter: reporter, + field: 'username', + pointer: 'username', + tip: {}, + root: {}, + refs: {}, + mutate: (newValue) => (value = newValue), + }) + + assert.equal(value, 'hello-world') + }) + + test('upperCase string when enabled', (assert) => { + const reporter = new ApiErrorReporter(new MessagesBag({}), false) + let value = 'Hello-World' + + string.validate(value, compile({ case: 'upperCase' }).compiledOptions, { + errorReporter: reporter, + field: 'username', + pointer: 'username', + tip: {}, + root: {}, + refs: {}, + mutate: (newValue) => (value = newValue), + }) + + assert.equal(value, 'HELLO-WORLD') + }) + + test('camelCase string when enabled', (assert) => { + const reporter = new ApiErrorReporter(new MessagesBag({}), false) + let value = 'Hello-World' + + string.validate(value, compile({ case: 'camelCase' }).compiledOptions, { + errorReporter: reporter, + field: 'username', + pointer: 'username', + tip: {}, + root: {}, + refs: {}, + mutate: (newValue) => (value = newValue), + }) + + assert.equal(value, 'helloWorld') + }) + + test('snakeCase string when enabled', (assert) => { + const reporter = new ApiErrorReporter(new MessagesBag({}), false) + let value = 'Hello-World' + + string.validate(value, compile({ case: 'snakeCase' }).compiledOptions, { + errorReporter: reporter, + field: 'username', + pointer: 'username', + tip: {}, + root: {}, + refs: {}, + mutate: (newValue) => (value = newValue), + }) + + assert.equal(value, 'hello_world') + }) + + test('dashCase string when enabled', (assert) => { + const reporter = new ApiErrorReporter(new MessagesBag({}), false) + let value = 'Hello World' + + string.validate(value, compile({ case: 'dashCase' }).compiledOptions, { + errorReporter: reporter, + field: 'username', + pointer: 'username', + tip: {}, + root: {}, + refs: {}, + mutate: (newValue) => (value = newValue), + }) + + assert.equal(value, 'hello-world') + }) + + test('pascalCase string when enabled', (assert) => { + const reporter = new ApiErrorReporter(new MessagesBag({}), false) + let value = 'hello world' + + string.validate(value, compile({ case: 'pascalCase' }).compiledOptions, { + errorReporter: reporter, + field: 'username', + pointer: 'username', + tip: {}, + root: {}, + refs: {}, + mutate: (newValue) => (value = newValue), + }) + + assert.equal(value, 'HelloWorld') + }) + + test('capitalCase string when enabled', (assert) => { + const reporter = new ApiErrorReporter(new MessagesBag({}), false) + let value = 'hello world' + + string.validate(value, compile({ case: 'capitalCase' }).compiledOptions, { + errorReporter: reporter, + field: 'username', + pointer: 'username', + tip: {}, + root: {}, + refs: {}, + mutate: (newValue) => (value = newValue), + }) + + assert.equal(value, 'Hello World') + }) + + test('sentenceCase string when enabled', (assert) => { + const reporter = new ApiErrorReporter(new MessagesBag({}), false) + let value = 'Hello World' + + string.validate(value, compile({ case: 'sentenceCase' }).compiledOptions, { + errorReporter: reporter, + field: 'username', + pointer: 'username', + tip: {}, + root: {}, + refs: {}, + mutate: (newValue) => (value = newValue), + }) + + assert.equal(value, 'Hello world') + }) + + test('noCase string when enabled', (assert) => { + const reporter = new ApiErrorReporter(new MessagesBag({}), false) + let value = 'hello.world' + + string.validate(value, compile({ case: 'noCase' }).compiledOptions, { + errorReporter: reporter, + field: 'username', + pointer: 'username', + tip: {}, + root: {}, + refs: {}, + mutate: (newValue) => (value = newValue), + }) + + assert.equal(value, 'hello world') + }) + + test('titleCase string when enabled', (assert) => { + const reporter = new ApiErrorReporter(new MessagesBag({}), false) + let value = 'Here is a fox' + + string.validate(value, compile({ case: 'titleCase' }).compiledOptions, { + errorReporter: reporter, + field: 'username', + pointer: 'username', + tip: {}, + root: {}, + refs: {}, + mutate: (newValue) => (value = newValue), + }) + + assert.equal(value, 'Here Is a Fox') + }) + + test('pluralize string when enabled', (assert) => { + const reporter = new ApiErrorReporter(new MessagesBag({}), false) + let value = 'post' + + string.validate(value, compile({ pluralize: true }).compiledOptions, { + errorReporter: reporter, + field: 'username', + pointer: 'username', + tip: {}, + root: {}, + refs: {}, + mutate: (newValue) => (value = newValue), + }) + + assert.equal(value, 'posts') + }) + + test('singularize string when enabled', (assert) => { + const reporter = new ApiErrorReporter(new MessagesBag({}), false) + let value = 'posts' + + string.validate(value, compile({ singularize: true }).compiledOptions, { + errorReporter: reporter, + field: 'username', + pointer: 'username', + tip: {}, + root: {}, + refs: {}, + mutate: (newValue) => (value = newValue), + }) + + assert.equal(value, 'post') + }) + + test('trim string when enabled', (assert) => { + const reporter = new ApiErrorReporter(new MessagesBag({}), false) + let value = ' untrimmed string ' + + string.validate(value, compile({ trim: true }).compiledOptions, { + errorReporter: reporter, + field: 'username', + pointer: 'username', + tip: {}, + root: {}, + refs: {}, + mutate: (newValue) => (value = newValue), + }) + + assert.equal(value, 'untrimmed string') + }) + + test('condense white space when enabled', (assert) => { + const reporter = new ApiErrorReporter(new MessagesBag({}), false) + let value = 'uncondensed string' + + string.validate(value, compile({ condenseWhitespace: true }).compiledOptions, { + errorReporter: reporter, + field: 'username', + pointer: 'username', + tip: {}, + root: {}, + refs: {}, + mutate: (newValue) => (value = newValue), + }) + + assert.equal(value, 'uncondensed string') + }) + test('escape string when enabled', (assert) => { const reporter = new ApiErrorReporter(new MessagesBag({}), false) let value = '

hello world

'