From 89f5e6917f1e9d570e2a1d101a0d32649c2d88b0 Mon Sep 17 00:00:00 2001 From: woksin Date: Thu, 29 Oct 2026 12:00:20 +0100 Subject: [PATCH] Use Unicode White_Space for empty/notEmpty string validation (#188) --- Source/Core/validation/evaluateRule.ts | 4 +++- .../when_empty/with_byte_order_mark.ts | 14 ++++++++++++++ .../when_empty/with_empty_string.ts | 14 ++++++++++++++ .../when_empty/with_next_line_character.ts | 14 ++++++++++++++ .../when_empty/with_padded_content.ts | 14 ++++++++++++++ .../when_empty/with_whitespace_only.ts | 14 ++++++++++++++ .../when_notEmpty/with_byte_order_mark.ts | 14 ++++++++++++++ .../when_notEmpty/with_empty_string.ts | 14 ++++++++++++++ .../when_notEmpty/with_next_line_character.ts | 14 ++++++++++++++ .../when_notEmpty/with_padded_content.ts | 14 ++++++++++++++ .../when_notEmpty/with_whitespace_only.ts | 14 ++++++++++++++ 11 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 Source/Core/validation/for_evaluateRule/when_empty/with_byte_order_mark.ts create mode 100644 Source/Core/validation/for_evaluateRule/when_empty/with_empty_string.ts create mode 100644 Source/Core/validation/for_evaluateRule/when_empty/with_next_line_character.ts create mode 100644 Source/Core/validation/for_evaluateRule/when_empty/with_padded_content.ts create mode 100644 Source/Core/validation/for_evaluateRule/when_empty/with_whitespace_only.ts create mode 100644 Source/Core/validation/for_evaluateRule/when_notEmpty/with_byte_order_mark.ts create mode 100644 Source/Core/validation/for_evaluateRule/when_notEmpty/with_empty_string.ts create mode 100644 Source/Core/validation/for_evaluateRule/when_notEmpty/with_next_line_character.ts create mode 100644 Source/Core/validation/for_evaluateRule/when_notEmpty/with_padded_content.ts create mode 100644 Source/Core/validation/for_evaluateRule/when_notEmpty/with_whitespace_only.ts diff --git a/Source/Core/validation/evaluateRule.ts b/Source/Core/validation/evaluateRule.ts index 4bdb9622..fef54770 100644 --- a/Source/Core/validation/evaluateRule.ts +++ b/Source/Core/validation/evaluateRule.ts @@ -4,8 +4,10 @@ import { DateOnly, Guid, TimeOnly, TimeSpan } from '@cratis/fundamentals'; import type { Rule } from './Rule.js'; import { compareValues } from './compareValues.js'; +const whiteSpaceOnly = /^\p{White_Space}*$/u; + const empty = (value: unknown): boolean => value == null || value === '' || value === 0 || value === false || - Array.isArray(value) && !value.length || typeof value === 'string' && !value.trim() || + Array.isArray(value) && !value.length || typeof value === 'string' && whiteSpaceOnly.test(value) || value instanceof Guid && (value as Guid).equals(Guid.empty); function equal(left: unknown, right: unknown): boolean { diff --git a/Source/Core/validation/for_evaluateRule/when_empty/with_byte_order_mark.ts b/Source/Core/validation/for_evaluateRule/when_empty/with_byte_order_mark.ts new file mode 100644 index 00000000..05890ccb --- /dev/null +++ b/Source/Core/validation/for_evaluateRule/when_empty/with_byte_order_mark.ts @@ -0,0 +1,14 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +import { BaseValidator } from '../../BaseValidator.js'; +import { evaluateRule } from '../../evaluateRule.js'; + +describe('when evaluating empty with byte order mark', () => { + let result: boolean; + beforeEach(async () => { + const validator = new BaseValidator<{ value: unknown }>(); + validator.ruleFor(model => model.value).empty(); + result = await evaluateRule(validator.rules[0]!, '\uFEFF', { value: '\uFEFF' }, new AbortController().signal); + }); + it('should reject the value', () => { result.should.equal(false); }); +}); diff --git a/Source/Core/validation/for_evaluateRule/when_empty/with_empty_string.ts b/Source/Core/validation/for_evaluateRule/when_empty/with_empty_string.ts new file mode 100644 index 00000000..3ff36630 --- /dev/null +++ b/Source/Core/validation/for_evaluateRule/when_empty/with_empty_string.ts @@ -0,0 +1,14 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +import { BaseValidator } from '../../BaseValidator.js'; +import { evaluateRule } from '../../evaluateRule.js'; + +describe('when evaluating empty with empty string', () => { + let result: boolean; + beforeEach(async () => { + const validator = new BaseValidator<{ value: unknown }>(); + validator.ruleFor(model => model.value).empty(); + result = await evaluateRule(validator.rules[0]!, '', { value: '' }, new AbortController().signal); + }); + it('should accept the value', () => { result.should.equal(true); }); +}); diff --git a/Source/Core/validation/for_evaluateRule/when_empty/with_next_line_character.ts b/Source/Core/validation/for_evaluateRule/when_empty/with_next_line_character.ts new file mode 100644 index 00000000..bf3c78ef --- /dev/null +++ b/Source/Core/validation/for_evaluateRule/when_empty/with_next_line_character.ts @@ -0,0 +1,14 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +import { BaseValidator } from '../../BaseValidator.js'; +import { evaluateRule } from '../../evaluateRule.js'; + +describe('when evaluating empty with next line character', () => { + let result: boolean; + beforeEach(async () => { + const validator = new BaseValidator<{ value: unknown }>(); + validator.ruleFor(model => model.value).empty(); + result = await evaluateRule(validator.rules[0]!, '\u0085', { value: '\u0085' }, new AbortController().signal); + }); + it('should accept the value', () => { result.should.equal(true); }); +}); diff --git a/Source/Core/validation/for_evaluateRule/when_empty/with_padded_content.ts b/Source/Core/validation/for_evaluateRule/when_empty/with_padded_content.ts new file mode 100644 index 00000000..a5687536 --- /dev/null +++ b/Source/Core/validation/for_evaluateRule/when_empty/with_padded_content.ts @@ -0,0 +1,14 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +import { BaseValidator } from '../../BaseValidator.js'; +import { evaluateRule } from '../../evaluateRule.js'; + +describe('when evaluating empty with padded content', () => { + let result: boolean; + beforeEach(async () => { + const validator = new BaseValidator<{ value: unknown }>(); + validator.ruleFor(model => model.value).empty(); + result = await evaluateRule(validator.rules[0]!, ' a ', { value: ' a ' }, new AbortController().signal); + }); + it('should reject the value', () => { result.should.equal(false); }); +}); diff --git a/Source/Core/validation/for_evaluateRule/when_empty/with_whitespace_only.ts b/Source/Core/validation/for_evaluateRule/when_empty/with_whitespace_only.ts new file mode 100644 index 00000000..fecaf958 --- /dev/null +++ b/Source/Core/validation/for_evaluateRule/when_empty/with_whitespace_only.ts @@ -0,0 +1,14 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +import { BaseValidator } from '../../BaseValidator.js'; +import { evaluateRule } from '../../evaluateRule.js'; + +describe('when evaluating empty with whitespace only', () => { + let result: boolean; + beforeEach(async () => { + const validator = new BaseValidator<{ value: unknown }>(); + validator.ruleFor(model => model.value).empty(); + result = await evaluateRule(validator.rules[0]!, ' \t\n', { value: ' \t\n' }, new AbortController().signal); + }); + it('should accept the value', () => { result.should.equal(true); }); +}); diff --git a/Source/Core/validation/for_evaluateRule/when_notEmpty/with_byte_order_mark.ts b/Source/Core/validation/for_evaluateRule/when_notEmpty/with_byte_order_mark.ts new file mode 100644 index 00000000..e8cdfc92 --- /dev/null +++ b/Source/Core/validation/for_evaluateRule/when_notEmpty/with_byte_order_mark.ts @@ -0,0 +1,14 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +import { BaseValidator } from '../../BaseValidator.js'; +import { evaluateRule } from '../../evaluateRule.js'; + +describe('when evaluating notEmpty with byte order mark', () => { + let result: boolean; + beforeEach(async () => { + const validator = new BaseValidator<{ value: unknown }>(); + validator.ruleFor(model => model.value).notEmpty(); + result = await evaluateRule(validator.rules[0]!, '\uFEFF', { value: '\uFEFF' }, new AbortController().signal); + }); + it('should accept the value', () => { result.should.equal(true); }); +}); diff --git a/Source/Core/validation/for_evaluateRule/when_notEmpty/with_empty_string.ts b/Source/Core/validation/for_evaluateRule/when_notEmpty/with_empty_string.ts new file mode 100644 index 00000000..353cfd3b --- /dev/null +++ b/Source/Core/validation/for_evaluateRule/when_notEmpty/with_empty_string.ts @@ -0,0 +1,14 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +import { BaseValidator } from '../../BaseValidator.js'; +import { evaluateRule } from '../../evaluateRule.js'; + +describe('when evaluating notEmpty with empty string', () => { + let result: boolean; + beforeEach(async () => { + const validator = new BaseValidator<{ value: unknown }>(); + validator.ruleFor(model => model.value).notEmpty(); + result = await evaluateRule(validator.rules[0]!, '', { value: '' }, new AbortController().signal); + }); + it('should reject the value', () => { result.should.equal(false); }); +}); diff --git a/Source/Core/validation/for_evaluateRule/when_notEmpty/with_next_line_character.ts b/Source/Core/validation/for_evaluateRule/when_notEmpty/with_next_line_character.ts new file mode 100644 index 00000000..27949bae --- /dev/null +++ b/Source/Core/validation/for_evaluateRule/when_notEmpty/with_next_line_character.ts @@ -0,0 +1,14 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +import { BaseValidator } from '../../BaseValidator.js'; +import { evaluateRule } from '../../evaluateRule.js'; + +describe('when evaluating notEmpty with next line character', () => { + let result: boolean; + beforeEach(async () => { + const validator = new BaseValidator<{ value: unknown }>(); + validator.ruleFor(model => model.value).notEmpty(); + result = await evaluateRule(validator.rules[0]!, '\u0085', { value: '\u0085' }, new AbortController().signal); + }); + it('should reject the value', () => { result.should.equal(false); }); +}); diff --git a/Source/Core/validation/for_evaluateRule/when_notEmpty/with_padded_content.ts b/Source/Core/validation/for_evaluateRule/when_notEmpty/with_padded_content.ts new file mode 100644 index 00000000..9a2d9c26 --- /dev/null +++ b/Source/Core/validation/for_evaluateRule/when_notEmpty/with_padded_content.ts @@ -0,0 +1,14 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +import { BaseValidator } from '../../BaseValidator.js'; +import { evaluateRule } from '../../evaluateRule.js'; + +describe('when evaluating notEmpty with padded content', () => { + let result: boolean; + beforeEach(async () => { + const validator = new BaseValidator<{ value: unknown }>(); + validator.ruleFor(model => model.value).notEmpty(); + result = await evaluateRule(validator.rules[0]!, ' a ', { value: ' a ' }, new AbortController().signal); + }); + it('should accept the value', () => { result.should.equal(true); }); +}); diff --git a/Source/Core/validation/for_evaluateRule/when_notEmpty/with_whitespace_only.ts b/Source/Core/validation/for_evaluateRule/when_notEmpty/with_whitespace_only.ts new file mode 100644 index 00000000..2a802948 --- /dev/null +++ b/Source/Core/validation/for_evaluateRule/when_notEmpty/with_whitespace_only.ts @@ -0,0 +1,14 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +import { BaseValidator } from '../../BaseValidator.js'; +import { evaluateRule } from '../../evaluateRule.js'; + +describe('when evaluating notEmpty with whitespace only', () => { + let result: boolean; + beforeEach(async () => { + const validator = new BaseValidator<{ value: unknown }>(); + validator.ruleFor(model => model.value).notEmpty(); + result = await evaluateRule(validator.rules[0]!, ' \t\n', { value: ' \t\n' }, new AbortController().signal); + }); + it('should reject the value', () => { result.should.equal(false); }); +});