Skip to content

Commit

Permalink
Dev/get all leaf subclasses (#6)
Browse files Browse the repository at this point in the history
* Added function to retrieve leaf subclasses and prevented inheritance of metadata which causes quite some problematic behavior.

* Bumped version.
  • Loading branch information
efreeti authored Jul 8, 2019
1 parent 29bb3f3 commit fcb5b17
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 58 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ts-runtime-reflection",
"version": "1.2.0",
"version": "1.3.0",
"main": "./dist/index.js",
"typings": "./dist/index.d.ts",
"scripts": {
Expand Down
4 changes: 2 additions & 2 deletions src/transformer.ast.builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export function createParentClassDecorator(parent: ts.Identifier) {
'defineMetadata', SubclassMetadataKey, <ReadonlyArray<ts.Expression>>[
ts.createArrayLiteral([
ts.createSpread(ts.createLogicalOr(
createMetadataCall('getMetadata', SubclassMetadataKey, <ReadonlyArray<ts.Expression>>[parent]),
createMetadataCall('getOwnMetadata', SubclassMetadataKey, <ReadonlyArray<ts.Expression>>[parent]),
ts.createArrayLiteral([])
)),
ts.createIdentifier('target')
Expand All @@ -109,7 +109,7 @@ export function createShorthandPropertyDecorator(propertyName: ts.Identifier, ty
'defineMetadata', ShorthandPropertiesMetadataKey, <ReadonlyArray<ts.Expression>>[
ts.createObjectLiteral([
ts.createSpreadAssignment(ts.createLogicalOr(
createMetadataCall('getMetadata', ShorthandPropertiesMetadataKey, <ReadonlyArray<ts.Expression>>[
createMetadataCall('getOwnMetadata', ShorthandPropertiesMetadataKey, <ReadonlyArray<ts.Expression>>[
ts.createIdentifier('target')
]),
ts.createObjectLiteral([])
Expand Down
21 changes: 17 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,15 +99,28 @@ export const SubclassMetadataKey = "ts-runtime-reflection:subtypes";
export const ShorthandPropertiesMetadataKey = "ts-runtime-reflection:shorthand-properties";

export function getSubclasses(target: Function): Function[] | undefined {
return Reflect.getMetadata(SubclassMetadataKey, target)
return Reflect.getOwnMetadata(SubclassMetadataKey, target)
}

export function getAllLeafSubclasses(type: Function): Function[] {
const subclasses = getSubclasses(type) || [];

return subclasses.reduce(
(result, subclass) => {
const subclasses = getAllLeafSubclasses(subclass);

return result.concat(subclasses.length > 0 ? subclasses : [subclass]);
},
<Function[]>[]
);
}

export function getType(target: Function): Types.Type | undefined {
return Reflect.getMetadata(TypeMetadataKey, target)
return Reflect.getOwnMetadata(TypeMetadataKey, target)
}

export function getPropType(target: Function, propertyKey: string | symbol | number): Types.Type | undefined {
return Reflect.getMetadata(TypeMetadataKey, target.prototype, propertyKey as any) || (
(Reflect.getMetadata(ShorthandPropertiesMetadataKey, target) || {})[propertyKey]
return Reflect.getOwnMetadata(TypeMetadataKey, target.prototype, propertyKey as any) || (
(Reflect.getOwnMetadata(ShorthandPropertiesMetadataKey, target) || {})[propertyKey]
);
}
155 changes: 104 additions & 51 deletions tests/class_decoration.spec.ts
Original file line number Diff line number Diff line change
@@ -1,74 +1,127 @@
import {
Reflective,
Types,
getType,
getPropType,
getSubclasses
Reflective,
Types,
getType,
getPropType,
getSubclasses,
getAllLeafSubclasses
} from '../src';

const TypeKind = Types.TypeKind;


@Reflective
class TestClass extends Array<string> {
'str': string
'str-str': string
42: string
get computed() {
return 'string'
}
[Symbol.toPrimitive](){
return 23
}
method() {
return 'asd'
}
constructor(
public publicShorthandProp: string,
private privateShorthandProp: number,
protected protectedShorthandProp: boolean,
readonly readonlyShorthandProp: Date
) {
super();
}
'str': string
'str-str': string
42: string
get computed() {
return 'string'
}
[Symbol.toPrimitive](){
return 23
}
method() {
return 'asd'
}
constructor(
public publicShorthandProp: string,
private privateShorthandProp: number,
protected protectedShorthandProp: boolean,
readonly readonlyShorthandProp: Date
) {
super();
}
}

@Reflective
class ParentClass {}
class ParentClass {
public parentProperty: string;

constructor(public parentShorthandProp: string) {
}
}

@Reflective
class Subclass1 extends ParentClass {}
class Subclass1 extends ParentClass {
}

@Reflective
class Subclass2 extends ParentClass {}

class Subclass3 extends ParentClass {}

@Reflective
class SubSubclass1 extends Subclass1 {}

@Reflective
class SubSubclass2 extends Subclass1 {}

describe('Class decoration', () => {
it('should decorate null properties', () => {
const ptype = getType(TestClass) as Types.ClassType
expect(ptype.kind).toEqual(TypeKind.Class)
expect(ptype.name).toEqual('TestClass')
expect(ptype.extends).toEqual({kind: TypeKind.Reference, type: Array, arguments: [{kind: TypeKind.String} as any]})

expect(ptype.props).toEqual([
'str', 'str-str', 42, 'publicShorthandProp', 'privateShorthandProp', 'protectedShorthandProp', 'readonlyShorthandProp'
])

expect(getPropType(TestClass, 'str')).toEqual({kind: TypeKind.String})
expect(getPropType(TestClass, 'str-str')).toEqual({kind: TypeKind.String})
expect(getPropType(TestClass, 42)).toEqual({kind: TypeKind.String})
expect(getPropType(TestClass, 'publicShorthandProp')).toEqual({kind: TypeKind.String})
expect(getPropType(TestClass, 'privateShorthandProp')).toEqual({kind: TypeKind.Number})
expect(getPropType(TestClass, 'protectedShorthandProp')).toEqual({kind: TypeKind.Boolean})
expect(getPropType(TestClass, 'readonlyShorthandProp')).toEqual({kind: TypeKind.Reference, type: Date, arguments: []})
});
it('should decorate properties', () => {
const ptype = getType(TestClass) as Types.ClassType;
expect(ptype.kind).toEqual(TypeKind.Class);
expect(ptype.name).toEqual('TestClass');
expect(ptype.extends).toEqual({kind: TypeKind.Reference, type: Array, arguments: [{kind: TypeKind.String} as any]});

expect(ptype.props).toEqual([
'str', 'str-str', 42, 'publicShorthandProp', 'privateShorthandProp', 'protectedShorthandProp', 'readonlyShorthandProp'
]);

expect(getPropType(TestClass, 'str')).toEqual({kind: TypeKind.String});
expect(getPropType(TestClass, 'str-str')).toEqual({kind: TypeKind.String});
expect(getPropType(TestClass, 42)).toEqual({kind: TypeKind.String});
expect(getPropType(TestClass, 'publicShorthandProp')).toEqual({kind: TypeKind.String});
expect(getPropType(TestClass, 'privateShorthandProp')).toEqual({kind: TypeKind.Number});
expect(getPropType(TestClass, 'protectedShorthandProp')).toEqual({kind: TypeKind.Boolean});
expect(getPropType(TestClass, 'readonlyShorthandProp')).toEqual({kind: TypeKind.Reference, type: Date, arguments: []});
});

it('should not inherit properties', () => {
const ptype = getType(Subclass1) as Types.ClassType;

expect(ptype.props.length).toEqual(0);
});

it('should not inherit properties if not reflective', () => {
expect(getType(Subclass3)).toBeUndefined();
});

it('should not inherit property type if not reflective', () => {
expect(getPropType(Subclass3, "parentProperty")).toBeUndefined();
});

it('should not inherit shorthand property type if not reflective', () => {
expect(getPropType(Subclass3, "parentShorthandProp")).toBeUndefined();
});
});

describe('Sub classes registration', () => {
it('should register all subclasses', () => {
const subclasses = getSubclasses(ParentClass)!
expect(subclasses.length).toEqual(2)
expect(subclasses[0]).toEqual(Subclass1)
expect(subclasses[1]).toEqual(Subclass2)
});
it('should register all subclasses', () => {
const subclasses = getSubclasses(ParentClass)!;
expect(subclasses.length).toEqual(2);
expect(subclasses[0]).toEqual(Subclass1);
expect(subclasses[1]).toEqual(Subclass2);
});

it('should not inherit subclasses decorator', () => {
const subclasses = getSubclasses(Subclass2)!;
expect(subclasses).toBeUndefined();
});

it('should not combine subclasses decorator with parent', () => {
const subclasses = getSubclasses(Subclass1)!;
expect(subclasses.length).toEqual(2);
expect(subclasses[0]).toEqual(SubSubclass1);
expect(subclasses[1]).toEqual(SubSubclass2);
});

it('should retrieve all leaf subclasses', () => {
const subclasses = getAllLeafSubclasses(ParentClass)!;
expect(subclasses.length).toEqual(3);
expect(subclasses[0]).toEqual(SubSubclass1);
expect(subclasses[1]).toEqual(SubSubclass2);
expect(subclasses[2]).toEqual(Subclass2);
});
});

0 comments on commit fcb5b17

Please sign in to comment.