Skip to content

Commit

Permalink
fix: optional and nullable handling in discriminatedSchema
Browse files Browse the repository at this point in the history
  • Loading branch information
asadali214 committed Jul 15, 2024
1 parent 42e868d commit 58bd8e1
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 67 deletions.
51 changes: 35 additions & 16 deletions packages/schema/src/types/discriminatedObject.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { Schema, SchemaMappedType, SchemaType } from '../schema';
import {
Schema,
SchemaMappedType,
SchemaType,
SchemaValidationError,
} from '../schema';
import { objectEntries } from '../utils';
import { ObjectXmlOptions } from './object';

Expand All @@ -14,9 +19,11 @@ export function discriminatedObject<
defaultDiscriminator: keyof TDiscrimMap,
xmlOptions?: ObjectXmlOptions
): Schema<any, any> {
const schemaSelector = (
const allSchemas = Object.values(discriminatorMap).reverse();
const selectSchema = (
value: unknown,
discriminatorProp: string | TDiscrimProp | TDiscrimMappedProp,
checker: (schema: TSchema) => SchemaValidationError[],
isAttr: boolean = false
) => {
if (
Expand All @@ -39,41 +46,53 @@ export function discriminatedObject<
return discriminatorMap[discriminatorValue];
}
}
for (const key in allSchemas) {
if (checker(allSchemas[key]).length === 0) {
return allSchemas[key];
}
}
return discriminatorMap[defaultDiscriminator];
};

return {
type: () =>
`DiscriminatedUnion<${discriminatorPropName},[${objectEntries(
`DiscriminatedUnion<${discriminatorPropName as string},[${objectEntries(
discriminatorMap
)
.map(([_, v]) => v.type)
.join(',')}]>`,
map: (value, ctxt) =>
schemaSelector(value, discriminatorPropName).map(value, ctxt),
selectSchema(value, discriminatorPropName, (schema) =>
schema.validateBeforeMap(value, ctxt)
).map(value, ctxt),
unmap: (value, ctxt) =>
schemaSelector(value, discriminatorMappedPropName).unmap(value, ctxt),
selectSchema(value, discriminatorMappedPropName, (schema) =>
schema.validateBeforeUnmap(value, ctxt)
).unmap(value, ctxt),
validateBeforeMap: (value, ctxt) =>
schemaSelector(value, discriminatorPropName).validateBeforeMap(
value,
ctxt
),
selectSchema(value, discriminatorPropName, (schema) =>
schema.validateBeforeMap(value, ctxt)
).validateBeforeMap(value, ctxt),
validateBeforeUnmap: (value, ctxt) =>
schemaSelector(value, discriminatorMappedPropName).validateBeforeUnmap(
value,
ctxt
),
selectSchema(value, discriminatorMappedPropName, (schema) =>
schema.validateBeforeUnmap(value, ctxt)
).validateBeforeUnmap(value, ctxt),
mapXml: (value, ctxt) =>
schemaSelector(
selectSchema(
value,
xmlOptions?.xmlName ?? discriminatorPropName,
(schema) => schema.validateBeforeMapXml(value, ctxt),
xmlOptions?.isAttr
).mapXml(value, ctxt),
unmapXml: (value, ctxt) =>
schemaSelector(value, discriminatorMappedPropName).unmapXml(value, ctxt),
selectSchema(value, discriminatorMappedPropName, (schema) =>
schema.validateBeforeUnmap(value, ctxt)
).unmapXml(value, ctxt),
validateBeforeMapXml: (value, ctxt) =>
schemaSelector(
selectSchema(
value,
xmlOptions?.xmlName ?? discriminatorPropName,
(schema) => schema.validateBeforeMapXml(value, ctxt),
xmlOptions?.isAttr
).validateBeforeMapXml(value, ctxt),
};
Expand Down
5 changes: 4 additions & 1 deletion packages/schema/src/types/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,10 @@ function validateValueObject({
ctxt.createChild(propTypePrefix + key, valueObject[key], schema)
)
);
} else if (schema.type().indexOf('Optional<') !== 0) {
} else if (
!schema.type().startsWith('Optional<') &&
!schema.type().startsWith('Nullable<')
) {
// Add to missing keys if it is not an optional property
missingProps.add(key);
}
Expand Down
181 changes: 131 additions & 50 deletions packages/schema/test/types/discriminatedObject.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import {
boolean,
discriminatedObject,
extendStrictObject,
literal,
nullable,
number,
optional,
strictObject,
string,
validateAndMap,
Expand All @@ -12,21 +13,21 @@ import {

describe('Discriminated Object', () => {
const baseType = strictObject({
type: ['type mapped', string()],
type: ['type mapped', optional(string())],
baseField: ['base field', number()],
});

const childType1 = extendStrictObject(baseType, {
type: ['type mapped', literal('child1')],
type: ['type mapped', optional(string())],
child1Field: ['child1 field', boolean()],
});

const childType2 = extendStrictObject(baseType, {
type: ['type mapped', literal('child2')],
type: ['type mapped', optional(string())],
child2Field: ['child2 field', boolean()],
});

const schema = discriminatedObject(
const discriminatedSchema = discriminatedObject(
'type',
'type mapped',
{
Expand All @@ -37,14 +38,18 @@ describe('Discriminated Object', () => {
'base'
);

const nestedDiscriminatedObject = strictObject({
innerType: ['inner type', nullable(discriminatedSchema)],
});

describe('Mapping', () => {
it('should map to child type on discriminator match', () => {
const input = {
'type mapped': 'child1',
'base field': 123123,
'child1 field': true,
};
const output = validateAndMap(input, schema);
const output = validateAndMap(input, discriminatedSchema);
expect(output.errors).toBeFalsy();
expect((output as any).result).toStrictEqual({
type: 'child1',
Expand All @@ -53,13 +58,67 @@ describe('Discriminated Object', () => {
});
});

it('should map to child type without discriminator match', () => {
const input = {
'type mapped': 'hello world',
'base field': 123123,
'child1 field': true,
};
const output = validateAndMap(input, discriminatedSchema);
expect(output.errors).toBeFalsy();
expect((output as any).result).toStrictEqual({
type: 'hello world',
baseField: 123123,
child1Field: true,
});
});

it('should map to child type with missing discriminator', () => {
const input = {
'base field': 123123,
'child1 field': true,
};
const output = validateAndMap(input, discriminatedSchema);
expect(output.errors).toBeFalsy();
expect((output as any).result).toStrictEqual({
baseField: 123123,
child1Field: true,
});
});

it('should map to base type on discriminator match', () => {
const input = {
'type mapped': 'base',
'base field': 123123,
};
const output = validateAndMap(input, discriminatedSchema);
expect(output.errors).toBeFalsy();
expect((output as any).result).toStrictEqual({
type: 'base',
baseField: 123123,
});
});

it('should map to base type without discriminator match', () => {
const input = {
'type mapped': 'hello world',
'base field': 123123,
};
const output = validateAndMap(input, discriminatedSchema);
expect(output.errors).toBeFalsy();
expect((output as any).result).toStrictEqual({
type: 'hello world',
baseField: 123123,
});
});

it('should fail on schema invalidation', () => {
const input = {
'type mapped': 'child1',
'base field': 123123,
'child1 field': 101,
};
const output = validateAndMap(input, schema);
const output = validateAndMap(input, discriminatedSchema);
expect(output.errors).toBeTruthy();
expect(output.errors).toMatchInlineSnapshot(`
Array [
Expand Down Expand Up @@ -88,45 +147,93 @@ describe('Discriminated Object', () => {
`);
});

it('should map to base type on discriminator match', () => {
it('should map to nestedDiscriminatedObject with null', () => {
const input = {};
const output = validateAndMap(input, nestedDiscriminatedObject);
expect(output.errors).toBeFalsy();
expect((output as any).result).toStrictEqual({
innerType: null,
});
});
});
describe('Unmapping', () => {
it('should unmap child type on discriminator match', () => {
const input = {
'type mapped': 'base',
'base field': 123123,
type: 'child1',
baseField: 123123,
child1Field: true,
};
const output = validateAndMap(input, schema);
const output = validateAndUnmap(input, discriminatedSchema);
expect(output.errors).toBeFalsy();
expect((output as any).result).toStrictEqual({
type: 'base',
baseField: 123123,
'type mapped': 'child1',
'base field': 123123,
'child1 field': true,
});
});

it('should map to base type on no discriminator match', () => {
it('should unmap child type without discriminator match', () => {
const input = {
type: 'hello world',
baseField: 123123,
child1Field: true,
};
const output = validateAndUnmap(input, discriminatedSchema);
expect(output.errors).toBeFalsy();
expect((output as any).result).toStrictEqual({
'type mapped': 'hello world',
'base field': 123123,
'child1 field': true,
});
});

it('should unmap child type with missing discriminator', () => {
const input = {
baseField: 123123,
child1Field: true,
};
const output = validateAndMap(input, schema);
const output = validateAndUnmap(input, discriminatedSchema);
expect(output.errors).toBeFalsy();
expect((output as any).result).toStrictEqual({
'base field': 123123,
'child1 field': true,
});
});

it('should unmap base type on discriminator match', () => {
const input = {
type: 'base',
baseField: 123123,
};
const output = validateAndUnmap(input, discriminatedSchema);
expect(output.errors).toBeFalsy();
expect((output as any).result).toStrictEqual({
'type mapped': 'base',
'base field': 123123,
});
});

it('should unmap base type without discriminator match', () => {
const input = {
type: 'hello world',
baseField: 123123,
};
const output = validateAndUnmap(input, discriminatedSchema);
expect(output.errors).toBeFalsy();
expect((output as any).result).toStrictEqual({
'type mapped': 'hello world',
'base field': 123123,
});
});
});
describe('Unmapping', () => {
it('should map to child type on discriminator match', () => {

it('should unmap base type with missing discriminator', () => {
const input = {
type: 'child1',
baseField: 123123,
child1Field: true,
};
const output = validateAndUnmap(input, schema);
const output = validateAndUnmap(input, discriminatedSchema);
expect(output.errors).toBeFalsy();
expect((output as any).result).toStrictEqual({
'type mapped': 'child1',
'base field': 123123,
'child1 field': true,
});
});

Expand All @@ -136,7 +243,7 @@ describe('Discriminated Object', () => {
baseField: 123123,
child1Field: 101,
};
const output = validateAndUnmap(input, schema);
const output = validateAndUnmap(input, discriminatedSchema);
expect(output.errors).toBeTruthy();
expect(output.errors).toMatchInlineSnapshot(`
Array [
Expand Down Expand Up @@ -164,31 +271,5 @@ describe('Discriminated Object', () => {
]
`);
});

it('should map to base type on discriminator match', () => {
const input = {
type: 'base',
baseField: 123123,
};
const output = validateAndUnmap(input, schema);
expect(output.errors).toBeFalsy();
expect((output as any).result).toStrictEqual({
'type mapped': 'base',
'base field': 123123,
});
});

it('should map to base type on no discriminator match', () => {
const input = {
type: 'hello world',
baseField: 123123,
};
const output = validateAndUnmap(input, schema);
expect(output.errors).toBeFalsy();
expect((output as any).result).toStrictEqual({
'type mapped': 'hello world',
'base field': 123123,
});
});
});
});

0 comments on commit 58bd8e1

Please sign in to comment.