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]> {
const key = this.nextKeySeed(kseed) as Nullable<TK>
if (key) {
if (key !== undefined) {
const value = this.nextValueSeed(vseed) as Nullable<TV>
if (value) {
if (value !== undefined) {
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 { GenericVisitor } from './generic'
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) {
@staticImplements<Deserialize<T>>()
class Deserializable extends constructor {
static name = constructor.name
static deserialize<D extends Deserializer>(deserializer: D): T {
const visitor = new GenericVisitor<T>()
return deserializer.deserializeAny(visitor)
return deserializeWith(deserializer, this, getMetadata(this))
}
}
// @ts-ignore
Deserializable[Serde] = (constructor as any)[Serde]
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')
@ -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)
this.ser.write(':')
}
serializeValue<U extends Serializable>(value: U): void {
this.ser.write(':')
serializeWith(this.ser, value)
}

View file

@ -1,5 +1,7 @@
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 {
@ -14,7 +16,7 @@ export interface RenameAllOptions {
export interface ContainerOptions {
// deserialization only
default?: <T>() => T
default?: () => any
rename?: RenameOptions | string
renameAll?: RenameAllOptions | CaseConvention
// deserialization only
@ -34,12 +36,17 @@ export interface SkipOptions {
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 {
alias?: string
// deserialization only
default?: () => any
flatten?: boolean
rename?: RenameOptions | string
serializeWith?: Morphism
deserializeWith: Morphism
//serializeWith?: CustomSerializer
//deserializeWith: CustomDeserializer
skip?: SkipOptions | boolean
}
@ -108,4 +115,41 @@ export class SerdeOptions {
const name = options != null ? this.getPropertyRename(property, stage, options) : property
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 { SerdeOptions, Stage } from '../options'
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}'`)
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
const objectSerializer = serializer.serializeObject!()
for (const [key, val] of value) {
const name = options?.getPropertyName(key as string, Stage.Serialize) ?? key
state = serializer.serializeKey(name)
state = serializer.serializeValue(val)
state = objectSerializer.serializeKey(name)
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)
}
@ -58,7 +60,7 @@ export function serializeWith<T>(serializer: Serializer<T>, value: Serializable,
if (isIterable(value) && isFunction(serializer.serializeIterable)) {
return serializeIter(serializer.serializeIterable(), value)
} 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
default: return serializeAny(value)