Skip to content

Commit 9cd6336

Browse files
committed
feat: add support for convertEmptyStringsToNull
1 parent 0a03cd4 commit 9cd6336

File tree

10 files changed

+587
-6
lines changed

10 files changed

+587
-6
lines changed

factories/output.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,18 @@
88
*/
99

1010
import { EOL } from 'node:os'
11+
import type { CompilerOptions } from '../src/types.js'
1112
import { defineInlineErrorMessages } from '../src/scripts/define_error_messages.js'
1213
import { defineInlineFunctions } from '../src/scripts/define_inline_functions.js'
1314

1415
/**
1516
* Returns code for the initial output
1617
*/
17-
export function getInitialOutput() {
18+
export function getInitialOutput(options?: CompilerOptions) {
1819
return [
1920
`async function anonymous(root,meta,refs,errorReporter) {`,
2021
...defineInlineErrorMessages().split(EOL),
21-
...defineInlineFunctions().split(EOL),
22+
...defineInlineFunctions(options || { convertEmptyStringsToNull: false }).split(EOL),
2223
'let out;',
2324
]
2425
}

src/compiler/main.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import type {
2727
CompilerField,
2828
CompilerNodes,
2929
CompilerParent,
30+
CompilerOptions,
3031
ErrorReporterContract,
3132
} from '../types.js'
3233
import { TupleNodeCompiler } from './nodes/tuple.js'
@@ -52,21 +53,27 @@ export class Compiler {
5253
*/
5354
#rootNode: RootNode
5455

56+
/**
57+
* Options to configure the compiler behavior
58+
*/
59+
#options: CompilerOptions
60+
5561
/**
5662
* Buffer for collection the JS output string
5763
*/
5864
#buffer: CompilerBuffer = new CompilerBuffer()
5965

60-
constructor(rootNode: RootNode) {
66+
constructor(rootNode: RootNode, options?: CompilerOptions) {
6167
this.#rootNode = rootNode
68+
this.#options = options || { convertEmptyStringsToNull: false }
6269
}
6370

6471
/**
6572
* Initiates the JS output
6673
*/
6774
#initiateJSOutput() {
6875
this.#buffer.writeStatement(defineInlineErrorMessages())
69-
this.#buffer.writeStatement(defineInlineFunctions())
76+
this.#buffer.writeStatement(defineInlineFunctions(this.#options))
7077
this.#buffer.writeStatement('let out;')
7178
}
7279

src/scripts/define_inline_functions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@
1111
* Returns JS fragment for inline function needed by the
1212
* validation runtime code.
1313
*/
14-
export function defineInlineFunctions() {
14+
export function defineInlineFunctions(options: { convertEmptyStringsToNull: boolean }) {
1515
return `function report(message, ctx) {
1616
ctx.isValid = false;
1717
errorReporter.report(message, ctx.fieldPath);
1818
};
1919
function defineValue(value, ctx) {
20+
${options.convertEmptyStringsToNull ? `if (value === '') { value = null; }` : ''}
2021
ctx.value = value;
2122
ctx.isDefined = value !== undefined && value !== null;
2223
return ctx;

src/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,3 +485,14 @@ export interface ErrorReporterContract {
485485
*/
486486
report(message: string, ctx: FieldContext): any
487487
}
488+
489+
/**
490+
* Options accepted by the compiler
491+
*/
492+
export type CompilerOptions = {
493+
/**
494+
* Convert empty string values to null for sake of
495+
* normalization
496+
*/
497+
convertEmptyStringsToNull: boolean
498+
}

tests/integration/compiler/array_node.spec.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,48 @@ test.group('Array node', () => {
693693
assert.deepEqual(error.messages, ['ref://2 validation failed'])
694694
}
695695
})
696+
697+
test('convert empty string to null', async ({ assert }) => {
698+
assert.plan(2)
699+
700+
const compiler = new Compiler(
701+
{
702+
type: 'root',
703+
schema: {
704+
type: 'array',
705+
bail: true,
706+
fieldName: '*',
707+
validations: [],
708+
propertyName: '*',
709+
allowNull: false,
710+
isOptional: false,
711+
each: {
712+
type: 'literal',
713+
allowNull: false,
714+
bail: true,
715+
isOptional: false,
716+
fieldName: '*',
717+
propertyName: '*',
718+
validations: [],
719+
},
720+
},
721+
},
722+
{ convertEmptyStringsToNull: true }
723+
)
724+
725+
const data: any = ''
726+
const meta = {}
727+
const refs = {}
728+
const errorReporter = new ErrorReporterFactory().create()
729+
730+
const fn = compiler.compile()
731+
try {
732+
await fn(data, meta, refs, errorReporter)
733+
} catch (error) {
734+
assert.equal(error.message, 'Validation failure')
735+
assert.deepEqual(error.messages, ['value is required'])
736+
}
737+
})
696738
})
697739

698740
test.group('Array node | optional: true', () => {
@@ -837,6 +879,42 @@ test.group('Array node | optional: true', () => {
837879
assert.deepEqual(error.messages, ['value is not a valid array'])
838880
}
839881
})
882+
883+
test('convert empty string to null', async ({ assert }) => {
884+
const compiler = new Compiler(
885+
{
886+
type: 'root',
887+
schema: {
888+
type: 'array',
889+
bail: true,
890+
fieldName: '*',
891+
validations: [],
892+
propertyName: '*',
893+
allowNull: false,
894+
isOptional: true,
895+
each: {
896+
type: 'literal',
897+
allowNull: false,
898+
bail: true,
899+
isOptional: false,
900+
fieldName: '*',
901+
propertyName: '*',
902+
validations: [],
903+
},
904+
},
905+
},
906+
{ convertEmptyStringsToNull: true }
907+
)
908+
909+
const data: any = ''
910+
const meta = {}
911+
const refs = {}
912+
const errorReporter = new ErrorReporterFactory().create()
913+
914+
const fn = compiler.compile()
915+
const output = await fn(data, meta, refs, errorReporter)
916+
assert.isUndefined(output)
917+
})
840918
})
841919

842920
test.group('Array node | allowNull: true', () => {
@@ -981,4 +1059,40 @@ test.group('Array node | allowNull: true', () => {
9811059
assert.deepEqual(error.messages, ['value is not a valid array'])
9821060
}
9831061
})
1062+
1063+
test('convert empty string to null', async ({ assert }) => {
1064+
const compiler = new Compiler(
1065+
{
1066+
type: 'root',
1067+
schema: {
1068+
type: 'array',
1069+
bail: true,
1070+
fieldName: '*',
1071+
validations: [],
1072+
propertyName: '*',
1073+
allowNull: true,
1074+
isOptional: false,
1075+
each: {
1076+
type: 'literal',
1077+
allowNull: false,
1078+
bail: true,
1079+
isOptional: false,
1080+
fieldName: '*',
1081+
propertyName: '*',
1082+
validations: [],
1083+
},
1084+
},
1085+
},
1086+
{ convertEmptyStringsToNull: true }
1087+
)
1088+
1089+
const data: any = ''
1090+
const meta = {}
1091+
const refs = {}
1092+
const errorReporter = new ErrorReporterFactory().create()
1093+
1094+
const fn = compiler.compile()
1095+
const output = await fn(data, meta, refs, errorReporter)
1096+
assert.isNull(output)
1097+
})
9841098
})

tests/integration/compiler/literal_node.spec.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,39 @@ test.group('Literal node', () => {
460460
const fn = compiler.compile()
461461
assert.deepEqual(await fn(data, meta, refs, errorReporter), 'VIRK')
462462
})
463+
464+
test('convert empty string to null', async ({ assert }) => {
465+
assert.plan(2)
466+
467+
const compiler = new Compiler(
468+
{
469+
type: 'root',
470+
schema: {
471+
type: 'literal',
472+
bail: true,
473+
fieldName: '*',
474+
validations: [],
475+
propertyName: '*',
476+
allowNull: false,
477+
isOptional: false,
478+
},
479+
},
480+
{ convertEmptyStringsToNull: true }
481+
)
482+
483+
const data = ''
484+
const meta = {}
485+
const refs = {}
486+
const errorReporter = new ErrorReporterFactory().create()
487+
488+
const fn = compiler.compile()
489+
try {
490+
await fn(data, meta, refs, errorReporter)
491+
} catch (error) {
492+
assert.equal(error.message, 'Validation failure')
493+
assert.deepEqual(error.messages, ['value is required'])
494+
}
495+
})
463496
})
464497

465498
test.group('Literal node | optional: true', () => {
@@ -738,6 +771,33 @@ test.group('Literal node | optional: true', () => {
738771
const output = await fn(data, meta, refs, errorReporter)
739772
assert.deepEqual(output, undefined)
740773
})
774+
775+
test('convert empty string to null', async ({ assert }) => {
776+
const compiler = new Compiler(
777+
{
778+
type: 'root',
779+
schema: {
780+
type: 'literal',
781+
bail: true,
782+
fieldName: '*',
783+
validations: [],
784+
propertyName: '*',
785+
allowNull: false,
786+
isOptional: true,
787+
},
788+
},
789+
{ convertEmptyStringsToNull: true }
790+
)
791+
792+
const data = ''
793+
const meta = {}
794+
const refs = {}
795+
const errorReporter = new ErrorReporterFactory().create()
796+
797+
const fn = compiler.compile()
798+
const output = await fn(data, meta, refs, errorReporter)
799+
assert.isUndefined(output)
800+
})
741801
})
742802

743803
test.group('Literal node | allowNull: true', () => {
@@ -1023,4 +1083,31 @@ test.group('Literal node | allowNull: true', () => {
10231083
const output = await fn(data, meta, refs, errorReporter)
10241084
assert.deepEqual(output, null)
10251085
})
1086+
1087+
test('convert empty string to null', async ({ assert }) => {
1088+
const compiler = new Compiler(
1089+
{
1090+
type: 'root',
1091+
schema: {
1092+
type: 'literal',
1093+
bail: true,
1094+
fieldName: '*',
1095+
validations: [],
1096+
propertyName: '*',
1097+
allowNull: true,
1098+
isOptional: false,
1099+
},
1100+
},
1101+
{ convertEmptyStringsToNull: true }
1102+
)
1103+
1104+
const data = ''
1105+
const meta = {}
1106+
const refs = {}
1107+
const errorReporter = new ErrorReporterFactory().create()
1108+
1109+
const fn = compiler.compile()
1110+
const output = await fn(data, meta, refs, errorReporter)
1111+
assert.isNull(output)
1112+
})
10261113
})

0 commit comments

Comments
 (0)