denyUnknownFields

This commit is contained in:
Rowan 2025-05-17 21:38:01 -05:00
parent 548c206afb
commit 0c654000ec
6 changed files with 94 additions and 18 deletions

View file

@ -16,10 +16,10 @@ export abstract class DefaultMapAccessImpl implements MapAccess {
nextEntrySeed<TK, TV, K extends Deserialize<TK>, V extends Deserialize<TV>>(kseed: K, vseed: V): Nullable<[TK, TV]> { nextEntrySeed<TK, TV, K extends Deserialize<TK>, V extends Deserialize<TV>>(kseed: K, vseed: V): Nullable<[TK, TV]> {
const key = this.nextKeySeed(kseed) as Nullable<TK> const key = this.nextKeySeed(kseed) as Nullable<TK>
if (key) { if (key !== undefined) {
const value = this.nextValueSeed(vseed) as Nullable<TV> const value = this.nextValueSeed(vseed) as Nullable<TV>
if (value) { if (value !== undefined) {
return [key, value] return [key, value]
} }
} }

View file

@ -1,16 +1,42 @@
import { Serde } from '../decorator' import { getMetadata, Serde } from '../decorator'
import { SerdeOptions, Stage } from '../options'
import { Constructor, staticImplements } from '../utils' import { Constructor, staticImplements } from '../utils'
import { GenericVisitor } from './generic' import { GenericVisitor } from './generic'
import { Deserialize, Deserializer } from './interface' import { Deserialize, Deserializer } from './interface'
type DeserializeConstructor<T> = Deserialize<T> & { new(): Deserialize<T> }
function deserializeWith<T, D extends Deserializer, E extends DeserializeConstructor<T>>(deserializer: D, into: E, options: SerdeOptions): T {
const visitor = new GenericVisitor<T>()
const obj = deserializer.deserializeObject(visitor) as any
const target = new into()
const newObject = {} as any
for (const property in target) {
const name = options.getPropertyName(property, Stage.Deserialize)
const value = obj[name] || options.getDefault(property)
newObject[property] = value
delete obj[name]
}
if (options.options.denyUnknownFields && Object.keys(obj).length > 0) {
throw new TypeError(`Unexpected fields: ${Object.keys(obj).join(', ')}`)
}
return Object.assign(target, newObject)
}
export function deserialize<T, C extends Constructor>(constructor: C) { export function deserialize<T, C extends Constructor>(constructor: C) {
@staticImplements<Deserialize<T>>() @staticImplements<Deserialize<T>>()
class Deserializable extends constructor { class Deserializable extends constructor {
static name = constructor.name
static deserialize<D extends Deserializer>(deserializer: D): T { static deserialize<D extends Deserializer>(deserializer: D): T {
const visitor = new GenericVisitor<T>() return deserializeWith(deserializer, this, getMetadata(this))
return deserializer.deserializeAny(visitor)
} }
} }
// @ts-ignore
Deserializable[Serde] = (constructor as any)[Serde] Deserializable[Serde] = (constructor as any)[Serde]
return Deserializable return Deserializable

View file

@ -1,4 +1,4 @@
import { ContainerOptions, PropertyOptions, SerdeOptions } from "./options" import { ContainerOptions, PropertyOptions, SerdeOptions } from './options'
export const Serde = Symbol('Serde') export const Serde = Symbol('Serde')
@ -38,3 +38,7 @@ export function serde(options: ContainerOptions | PropertyOptions) {
} }
} }
} }
export function getMetadata(value: any) {
return value[Serde]
}

View file

@ -244,10 +244,10 @@ class JSONObjectSerializer implements ObjectSerializer<void> {
} }
serializeWith(this.ser, key) serializeWith(this.ser, key)
this.ser.write(':')
} }
serializeValue<U extends Serializable>(value: U): void { serializeValue<U extends Serializable>(value: U): void {
this.ser.write(':')
serializeWith(this.ser, value) serializeWith(this.ser, value)
} }

View file

@ -1,5 +1,7 @@
import { CaseConvention, convertCase } from './case' import { CaseConvention, convertCase } from './case'
import { isNumber, isString, Morphism } from './utils' import { Deserializer } from './de'
import { Serializer } from './ser'
import { isFunction, isNumber, isString, Morphism, Nullable } from './utils'
export interface RenameOptions { export interface RenameOptions {
@ -14,7 +16,7 @@ export interface RenameAllOptions {
export interface ContainerOptions { export interface ContainerOptions {
// deserialization only // deserialization only
default?: <T>() => T default?: () => any
rename?: RenameOptions | string rename?: RenameOptions | string
renameAll?: RenameAllOptions | CaseConvention renameAll?: RenameAllOptions | CaseConvention
// deserialization only // deserialization only
@ -34,12 +36,17 @@ export interface SkipOptions {
deserializing?: ConditionalSkipOptions | boolean deserializing?: ConditionalSkipOptions | boolean
} }
export type CustomSerializer = <T, V, S extends Serializer<T>>(value: V, serializer: S) => T
export type CustomDeserializer = <T, D extends Deserializer>(deserializer: D) => T
export interface PropertyOptions { export interface PropertyOptions {
alias?: string alias?: string
// deserialization only
default?: () => any
flatten?: boolean flatten?: boolean
rename?: RenameOptions | string rename?: RenameOptions | string
serializeWith?: Morphism //serializeWith?: CustomSerializer
deserializeWith: Morphism //deserializeWith: CustomDeserializer
skip?: SkipOptions | boolean skip?: SkipOptions | boolean
} }
@ -108,4 +115,41 @@ export class SerdeOptions {
const name = options != null ? this.getPropertyRename(property, stage, options) : property const name = options != null ? this.getPropertyRename(property, stage, options) : property
return this.getPropertyCase(name, stage) return this.getPropertyCase(name, stage)
} }
getSerializationName(property: string) {
return this.getPropertyName(property, Stage.Serialize)
}
getDeserializationName(property: string) {
return this.getPropertyName(property, Stage.Deserialize)
}
getDefault(property: string) {
const options = this.properties.get(property)
if (options && isFunction(options.default)) {
return options.default()
} else if (isFunction(this.options.default)) {
return this.options.default()
}
}
getCustomImpl(property: string, stage: Stage) {
const options = this.properties.get(property)
if (options != null) {
if (stage === Stage.Serialize && isFunction(options.serializeWith)) {
return options.serializeWith
} else if (stage === Stage.Deserialize && isFunction(options.deserializeWith)) {
return options.deserializeWith
}
}
}
getSerializer(property: string): Nullable<CustomSerializer> {
return this.getCustomImpl(property, Stage.Serialize) as CustomSerializer
}
getDeserializer(property: string): Nullable<CustomDeserializer> {
return this.getCustomImpl(property, Stage.Deserialize) as CustomDeserializer
}
} }

View file

@ -1,23 +1,25 @@
import { Serde } from '../decorator' import { Serde } from '../decorator'
import { SerdeOptions, Stage } from '../options' import { SerdeOptions, Stage } from '../options'
import { ifNull, isFunction, isIterable, Nullable, orElse } from '../utils' import { ifNull, isFunction, isIterable, Nullable, orElse } from '../utils'
import { IterableSerializer, ObjectSerializer, Serializable, Serializer } from './interface' import { IterableSerializer, Serializable, Serializer } from './interface'
const unhandledType = (serializer: any, value: any) => new TypeError(`'${serializer.constructor.name}' has no method for value type '${typeof value}'`) const unhandledType = (serializer: any, value: any) => new TypeError(`'${serializer.constructor.name}' has no method for value type '${typeof value}'`)
function serializeEntries<T, K extends Serializable, V extends Serializable, E extends Iterable<[K, V]>>(serializer: ObjectSerializer<T>, value: E, options?: SerdeOptions) { function serializeEntries<T, K extends string, V extends Serializable, E extends Iterable<[K, V]>>(serializer: Serializer<T>, value: E, options?: SerdeOptions) {
let state let state
const objectSerializer = serializer.serializeObject!()
for (const [key, val] of value) { for (const [key, val] of value) {
const name = options?.getPropertyName(key as string, Stage.Serialize) ?? key const name = options?.getPropertyName(key as string, Stage.Serialize) ?? key
state = serializer.serializeKey(name) state = objectSerializer.serializeKey(name)
state = serializer.serializeValue(val) state = objectSerializer.serializeValue(val)
} }
return serializer.end() return objectSerializer.end()
} }
function serializeObject<T, K extends PropertyKey, V extends Serializable, R extends Record<K, V>>(serializer: ObjectSerializer<T>, value: R, options?: SerdeOptions) { function serializeObject<T, K extends string, V extends Serializable, R extends Record<K, V>>(serializer: Serializer<T>, value: R, options?: SerdeOptions) {
return serializeEntries(serializer, Object.entries(value) as Iterable<[K, V]>, options) return serializeEntries(serializer, Object.entries(value) as Iterable<[K, V]>, options)
} }
@ -58,7 +60,7 @@ export function serializeWith<T>(serializer: Serializer<T>, value: Serializable,
if (isIterable(value) && isFunction(serializer.serializeIterable)) { if (isIterable(value) && isFunction(serializer.serializeIterable)) {
return serializeIter(serializer.serializeIterable(), value) return serializeIter(serializer.serializeIterable(), value)
} else if (isFunction(serializer.serializeObject)) { } else if (isFunction(serializer.serializeObject)) {
return serializeObject(serializer.serializeObject!(), value as Record<PropertyKey, any>, optionsGetter(value)) return serializeObject(serializer, value as Record<PropertyKey, any>, optionsGetter(value))
} // deliberate fallthrough when the above fail } // deliberate fallthrough when the above fail
default: return serializeAny(value) default: return serializeAny(value)