Skip to content

Commit

Permalink
Improvement: add interface type serialize support.
Browse files Browse the repository at this point in the history
  • Loading branch information
dpimonov committed Oct 14, 2023
1 parent fe4e55d commit 906e7e4
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 17 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [7.2.0] - 2023-10-14

### Added

- Support for polymorphic serialization based on interface types.

## [7.1.1] - 2023-08-19

### Fixed
Expand Down
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ If you like or are using this project please give it a star. Thanks!
* [Serializer option](#serializer-option)
* [Use default value option](#use-default-value-option)
* [Use implicit conversion option](#use-implicit-conversion-option)
* [Parent type functions option](#parent-type-functions-option)
* [Defining configuration manually](#defining-configuration-manually)
* [Configuring global options](#configuring-global-options)
* [Configuring options per type](#configuring-options-per-type)
Expand Down Expand Up @@ -999,6 +1000,37 @@ export class User

With this any value which can be converted to `String` will be converted properly. Such behaviour works for other built in serializers and supported for custom ones. By default implicit conversion is turned off. You can enable it using `useImplicitConversion` option per type and property or enable globally using `TypeManager` configure method.

### Parent type functions option

When type implements interfaces which represent other classes this information got lost during `TypeScript` compilation process and there is no way to extract it. This option can be used to provide such information for a `TypeManager` to be used during serialization and deserialization.

```typescript
import { Type, Property } from '@dipscope/type-manager';

@Type()
export abstract class Entity
{
@Property(String) public id?: string;
}

@Type()
export abstract class UserStatus extends Entity
{
@Property(String) public title?: string;
}

@Type({
parentTypeFns: [UserStatus]
})
export class ActiveUserStatus extends Entity implements UserStatus
{
@Property(String) public title?: string;
@Property(Boolean) public active?: boolean;
}
```

Note that usually only implemented classes should be specified as direct parents are already known to a `TypeManager`. However even if we specify both - it will work properly.

## Defining configuration manually

There are circumstances when decorators cannot be used or you don't want to. For example you are using a 3rd party package and cannot decorate classes from it. Another use case when you want to configure some options globally. In such scenarios you can define the complete configuration through special configure methods.
Expand Down
7 changes: 5 additions & 2 deletions spec/use-cases/polymorphic-types.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@ class DefaultActiveUserStatus extends UserStatus
@Property(Boolean) public active?: boolean;
}

@Type()
class DefaultInactiveUserStatus extends UserStatus
@Type({
parentTypeFns: [UserStatus]
})
class DefaultInactiveUserStatus implements UserStatus
{
@Property(String) public title?: string;
@Property(Boolean) public inactive?: boolean;
}

Expand Down
3 changes: 2 additions & 1 deletion src/type-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,8 @@ export class TypeManager
const symbol = this.symbol;
const parentPrototype = Object.getPrototypeOf(typeFn.prototype) ?? {};
const parentTypeMetadata = parentPrototype[symbol];
const typeMetadata = new TypeMetadata(this, typeFn, mapTypeOptions, parentTypeMetadata);
const parentTypeMetadatas = isNil(parentTypeMetadata) ? [] : [parentTypeMetadata];
const typeMetadata = new TypeMetadata(this, typeFn, mapTypeOptions, parentTypeMetadatas);

return typeMetadata;
}
Expand Down
90 changes: 76 additions & 14 deletions src/type-metadata.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isFunction, isNil, isUndefined } from 'lodash';
import { first, isEmpty, isFunction, isNil, isUndefined } from 'lodash';
import { Alias } from './alias';
import { CustomContext } from './custom-context';
import { CustomKey } from './custom-key';
Expand Down Expand Up @@ -107,25 +107,25 @@ export class TypeMetadata<TType> extends Metadata
public readonly injectMetadataMap: Map<InjectIndex, InjectMetadata<TType, any>> = new Map<InjectIndex, InjectMetadata<TType, any>>();

/**
* Parent type metadata.
* Parent type metadatas.
*
* @type {TypeMetadata<any>}
* @type {Array<TypeMetadata<any>>}
*/
public readonly parentTypeMetadata?: TypeMetadata<any>;
public readonly parentTypeMetadatas: Array<TypeMetadata<any>>;

/**
* Constructor.
*
* @param {TypeManager} typeManager Type manager.
* @param {TypeFn<any>} typeFn Type function.
* @param {TypeOptions<TType>} typeOptions Type options.
* @param {TypeMetadata<any>} parentTypeMetadata Parent type metadata.
* @param {Array<TypeMetadata<any>>} parentTypeMetadatas Parent type metadatas.
*/
public constructor(
typeManager: TypeManager,
typeFn: TypeFn<TType>,
typeOptions: TypeOptions<TType>,
parentTypeMetadata?: TypeMetadata<any>
parentTypeMetadatas: Array<TypeMetadata<any>>
)
{
super(typeManager);
Expand All @@ -135,7 +135,7 @@ export class TypeMetadata<TType> extends Metadata
this.typeOptionsBase = typeManager.typeOptionsBase;
this.typeOptions = this.constructTypeOptions(typeOptions);
this.typeInternals = this.constructTypeInternals();
this.parentTypeMetadata = parentTypeMetadata;
this.parentTypeMetadatas = parentTypeMetadatas;

this.deriveParentTypeMetadataProperties();
this.hasDiscriminant(this.discriminant);
Expand All @@ -144,6 +144,16 @@ export class TypeMetadata<TType> extends Metadata
return;
}

/**
* Parent type metadata.
*
* @type {TypeMetadata<any>}
*/
public get parentTypeMetadata(): TypeMetadata<any> | undefined
{
return first(this.parentTypeMetadatas);
}

/**
* Gets alias.
*
Expand Down Expand Up @@ -496,6 +506,16 @@ export class TypeMetadata<TType> extends Metadata
return injectOptionsMap;
}

/**
* Gets parent type fns value.
*
* @returns {Array<TypeFn<any>>|undefined} Parent type fns or undefined.
*/
public get parentTypeFns(): Array<TypeFn<any>> | undefined
{
return this.typeOptions.parentTypeFns;
}

/**
* Constructs initial type options by extending passed options
* with default values if they are not overriden. All references are kept.
Expand Down Expand Up @@ -530,17 +550,23 @@ export class TypeMetadata<TType> extends Metadata
*/
private deriveParentTypeMetadataProperties(): this
{
if (isNil(this.parentTypeMetadata))
if (isEmpty(this.parentTypeMetadatas))
{
return this;
}

for (const [propertyName, propertyMetadata] of this.parentTypeMetadata.propertyMetadataMap)
for (const parentTypeMetadata of this.parentTypeMetadatas)
{
this.propertyMetadataMap.set(propertyName, propertyMetadata);
}
for (const [propertyName, propertyMetadata] of parentTypeMetadata.propertyMetadataMap)
{
if (!this.propertyMetadataMap.has(propertyName))
{
this.propertyMetadataMap.set(propertyName, propertyMetadata);
}
}

this.parentTypeMetadata.childrenTypeMetadataMap.set(this.typeFn, this);
parentTypeMetadata.childrenTypeMetadataMap.set(this.typeFn, this);
}

return this;
}
Expand Down Expand Up @@ -741,9 +767,12 @@ export class TypeMetadata<TType> extends Metadata
{
this.discriminantMap.set(typeFn, discriminant);

if (!isNil(this.parentTypeMetadata))
if (!isEmpty(this.parentTypeMetadatas))
{
this.parentTypeMetadata.provideDiscriminant(typeFn, discriminant);
for (const parentTypeMetadata of this.parentTypeMetadatas)
{
parentTypeMetadata.provideDiscriminant(typeFn, discriminant);
}
}

return this;
Expand Down Expand Up @@ -1075,6 +1104,34 @@ export class TypeMetadata<TType> extends Metadata
return typeExtensionMetadata;
}

/**
* Configures parent type fns.
*
* @param {Array<TypeFn<any>>} parentTypeFns Parent type fns.
*
* @returns {this} Current instance of type metadata.
*/
public hasParentTypeFns(parentTypeFns: Array<TypeFn<any>>): this
{
if (isNil(this.typeOptions.parentTypeFns))
{
this.typeOptions.parentTypeFns = new Array<TypeFn<any>>();
}

for (const parentTypeFn of parentTypeFns)
{
const parentTypeMetadata = this.typeManager.extractTypeMetadata(parentTypeFn);

this.parentTypeMetadatas.push(parentTypeMetadata);
this.typeOptions.parentTypeFns.push(parentTypeFn);
}

this.deriveParentTypeMetadataProperties();
this.hasDiscriminant(this.discriminant);

return this;
}

/**
* Configures type metadata based on provided options.
*
Expand All @@ -1084,6 +1141,11 @@ export class TypeMetadata<TType> extends Metadata
*/
public configure(typeOptions: TypeOptions<TType>): this
{
if (!isUndefined(typeOptions.parentTypeFns))
{
this.hasParentTypeFns(typeOptions.parentTypeFns);
}

if (!isUndefined(typeOptions.alias))
{
this.hasAlias(typeOptions.alias);
Expand Down
9 changes: 9 additions & 0 deletions src/type-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { InjectIndex } from './inject-index';
import { InjectOptions } from './inject-options';
import { PropertyName } from './property-name';
import { PropertyOptions } from './property-options';
import { TypeFn } from './type-fn';
import { TypeOptionsBase } from './type-options-base';

/**
Expand Down Expand Up @@ -78,4 +79,12 @@ export type TypeOptions<TType> = Partial<TypeOptionsBase<TType>> &
* @type {Map<PropertyName, PropertyOptions<any>>}
*/
propertyOptionsMap?: Map<PropertyName, PropertyOptions<any>>;

/**
* Explicit array of type fns to specify polymorphic serialization based
* on implemented interfaces.
*
* @type {Array<TypeFn<any>>}
*/
parentTypeFns?: Array<TypeFn<any>>;
}

0 comments on commit 906e7e4

Please sign in to comment.