From 5258543cf7b6df361d2bfa5a5b0c80f3aece368a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Marty=C5=84ski?= Date: Sat, 16 Feb 2019 20:06:40 +0100 Subject: [PATCH] feat: created collectSchema which recursively can create schema from MessageClass --- src/collectSchema.ts | 106 +++++ src/index.ts | 1 + src/utils.ts | 2 + test/__snapshots__/collectSchema.test.ts.snap | 400 ++++++++++++++++++ test/collectSchema.test.ts | 248 +++++++++++ 5 files changed, 757 insertions(+) create mode 100644 src/collectSchema.ts create mode 100644 test/__snapshots__/collectSchema.test.ts.snap create mode 100644 test/collectSchema.test.ts diff --git a/src/collectSchema.ts b/src/collectSchema.ts new file mode 100644 index 0000000..0384bba --- /dev/null +++ b/src/collectSchema.ts @@ -0,0 +1,106 @@ +import { getMetadataObject, hasMetadataObject } from "./metadataHelpers"; +import { IFieldInfo, IFieldOptions, ProtobufLiteMetadata } from "./ProtobufLiteMetadata"; +import { getPrototypeChain, Omit } from "./utils"; + +export type FieldInfoWithoutPropertyKey = Omit; + +export interface ISchema { + refs: FieldInfoWithoutPropertyKey[][]; + fieldsInfo: FieldInfoWithoutPropertyKey[]; +} + +export const collectSchema = (MessageClass: Function): ISchema => { + /* istanbul ignore next line */ + if (!hasMetadataObject(MessageClass)) { + throw new Error(`MessageClass doesn't have protobuf lite metadata assosiated!`); + } + + const metadata = getMetadataObject(MessageClass); + + const schema: ISchema = { + refs: [], + fieldsInfo: [] + }; + + const mapField = (fieldInfo: IFieldInfo): FieldInfoWithoutPropertyKey => { + if (childTypesByClassName.has(fieldInfo.prototype)) { + const fieldOptions = childTypesByClassName.get(fieldInfo.prototype) as IFieldOptions; + const indexRef = indexedChildTypes.indexOf(fieldOptions); + + return { prototype: `__ref__${indexRef}__`, rule: fieldInfo.rule }; + } + + return { prototype: fieldInfo.prototype, rule: fieldInfo.rule }; + }; + + const childTypesByClassName = new Map(); + const indexedChildTypes: IFieldOptions[] = []; + + const collectChildTypes = (childTypes: IFieldOptions[]) => { + for (let item of childTypes) { + const metadata = getMetadataObject(item.MessageClass); + + if (!childTypesByClassName.has(metadata.getMessageClassName())) { + childTypesByClassName.set(metadata.getMessageClassName(), item); + indexedChildTypes.push(item); + + if (metadata.getChildTypes()) { + collectChildTypes(metadata.getChildTypes()); + } + } + } + }; + + collectChildTypes(metadata.getChildTypes()); + + const alreadyCollectedChildTypes = new Map(); + + const handleChildTypes = (childTypes: IFieldOptions[]) => { + for (let item of childTypes) { + const metadata = getMetadataObject(item.MessageClass); + + if (alreadyCollectedChildTypes.has(metadata)) { + continue; + } + alreadyCollectedChildTypes.set(metadata, true); + + schema.refs.push(metadata.getFieldsInfo().map(mapField)); + + if (metadata.getChildTypes()) { + handleChildTypes(metadata.getChildTypes()); + } + } + }; + + handleChildTypes(metadata.getChildTypes()); + + // const MessageClass = metadata.getMessageClass(); + const prototypes = getPrototypeChain(MessageClass) + .reverse() + .filter(p => p !== MessageClass.prototype); + + const alreadyUsedPropertyKeys: Map = new Map(); + + const addFields = (fields: IFieldInfo[]) => { + for (let field of fields) { + if (alreadyUsedPropertyKeys.has(field.propertyKey)) { + throw new Error(`Parent class field was most likely overwrited by child class!`); + } + + schema.fieldsInfo.push(mapField(field)); + + alreadyUsedPropertyKeys.set(field.propertyKey, true); + } + }; + + for (let prototype of prototypes) { + const mt = getMetadataObject(prototype.constructor); + const fields = mt.getFieldsInfo(); + + addFields(fields); + } + + addFields(metadata.getFieldsInfo()); + + return schema; +}; diff --git a/src/index.ts b/src/index.ts index 0bee9d0..132db5f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,3 @@ +export * from "./collectSchema"; export * from "./encoderDecoderFunctions"; export * from "./ProtobufLiteProperty"; diff --git a/src/utils.ts b/src/utils.ts index 5cd6775..8ba4465 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,5 @@ +export type Omit = Pick>; + export interface Constructable { new (): T; } diff --git a/test/__snapshots__/collectSchema.test.ts.snap b/test/__snapshots__/collectSchema.test.ts.snap new file mode 100644 index 0000000..48d2d93 --- /dev/null +++ b/test/__snapshots__/collectSchema.test.ts.snap @@ -0,0 +1,400 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`collectSchema(MessageClass) should return correct schema for Buffers 1`] = ` +Object { + "fieldsInfo": Array [ + Object { + "prototype": "bytes", + "rule": "required", + }, + ], + "refs": Array [], +} +`; + +exports[`collectSchema(MessageClass) should return correct schema for Buffers 2`] = ` +Object { + "fieldsInfo": Array [ + Object { + "prototype": "bytes", + "rule": "optional", + }, + ], + "refs": Array [], +} +`; + +exports[`collectSchema(MessageClass) should return correct schema for Buffers 3`] = ` +Object { + "fieldsInfo": Array [ + Object { + "prototype": "bytes", + "rule": "repeated", + }, + ], + "refs": Array [], +} +`; + +exports[`collectSchema(MessageClass) should return correct schema for Dates 1`] = ` +Object { + "fieldsInfo": Array [ + Object { + "prototype": "bytes", + "rule": "required", + }, + ], + "refs": Array [], +} +`; + +exports[`collectSchema(MessageClass) should return correct schema for Dates 2`] = ` +Object { + "fieldsInfo": Array [ + Object { + "prototype": "bytes", + "rule": "optional", + }, + ], + "refs": Array [], +} +`; + +exports[`collectSchema(MessageClass) should return correct schema for Dates 3`] = ` +Object { + "fieldsInfo": Array [ + Object { + "prototype": "bytes", + "rule": "repeated", + }, + ], + "refs": Array [], +} +`; + +exports[`collectSchema(MessageClass) should return correct schema for booleans 1`] = ` +Object { + "fieldsInfo": Array [ + Object { + "prototype": "bool", + "rule": "required", + }, + ], + "refs": Array [], +} +`; + +exports[`collectSchema(MessageClass) should return correct schema for booleans 2`] = ` +Object { + "fieldsInfo": Array [ + Object { + "prototype": "bool", + "rule": "optional", + }, + ], + "refs": Array [], +} +`; + +exports[`collectSchema(MessageClass) should return correct schema for booleans 3`] = ` +Object { + "fieldsInfo": Array [ + Object { + "prototype": "bool", + "rule": "repeated", + }, + ], + "refs": Array [], +} +`; + +exports[`collectSchema(MessageClass) should return correct schema for inhreited classes v1 1`] = ` +Object { + "fieldsInfo": Array [ + Object { + "prototype": "string", + "rule": "required", + }, + Object { + "prototype": "string", + "rule": "required", + }, + ], + "refs": Array [], +} +`; + +exports[`collectSchema(MessageClass) should return correct schema for inhreited classes v2 1`] = ` +Object { + "fieldsInfo": Array [ + Object { + "prototype": "string", + "rule": "required", + }, + ], + "refs": Array [], +} +`; + +exports[`collectSchema(MessageClass) should return correct schema for inhreited classes v2 2`] = ` +Object { + "fieldsInfo": Array [ + Object { + "prototype": "string", + "rule": "required", + }, + Object { + "prototype": "string", + "rule": "required", + }, + ], + "refs": Array [], +} +`; + +exports[`collectSchema(MessageClass) should return correct schema for inhreited classes v2 3`] = ` +Object { + "fieldsInfo": Array [ + Object { + "prototype": "string", + "rule": "required", + }, + Object { + "prototype": "string", + "rule": "required", + }, + Object { + "prototype": "string", + "rule": "required", + }, + ], + "refs": Array [], +} +`; + +exports[`collectSchema(MessageClass) should return correct schema for inhreited classes v2 4`] = ` +Object { + "fieldsInfo": Array [ + Object { + "prototype": "string", + "rule": "required", + }, + Object { + "prototype": "string", + "rule": "required", + }, + Object { + "prototype": "string", + "rule": "required", + }, + Object { + "prototype": "string", + "rule": "required", + }, + ], + "refs": Array [], +} +`; + +exports[`collectSchema(MessageClass) should return correct schema for inhreited classes v2 5`] = ` +Object { + "fieldsInfo": Array [ + Object { + "prototype": "string", + "rule": "required", + }, + Object { + "prototype": "string", + "rule": "required", + }, + Object { + "prototype": "string", + "rule": "required", + }, + Object { + "prototype": "string", + "rule": "required", + }, + Object { + "prototype": "string", + "rule": "required", + }, + ], + "refs": Array [], +} +`; + +exports[`collectSchema(MessageClass) should return correct schema for inhreited classes v3 1`] = ` +Object { + "fieldsInfo": Array [ + Object { + "prototype": "string", + "rule": "required", + }, + ], + "refs": Array [], +} +`; + +exports[`collectSchema(MessageClass) should return correct schema for inhreited classes v3 2`] = ` +Object { + "fieldsInfo": Array [ + Object { + "prototype": "string", + "rule": "required", + }, + ], + "refs": Array [], +} +`; + +exports[`collectSchema(MessageClass) should return correct schema for inhreited classes v3 3`] = ` +Object { + "fieldsInfo": Array [ + Object { + "prototype": "string", + "rule": "required", + }, + ], + "refs": Array [], +} +`; + +exports[`collectSchema(MessageClass) should return correct schema for inhreited classes v3 4`] = ` +Object { + "fieldsInfo": Array [ + Object { + "prototype": "string", + "rule": "required", + }, + Object { + "prototype": "string", + "rule": "required", + }, + ], + "refs": Array [], +} +`; + +exports[`collectSchema(MessageClass) should return correct schema for nestedClasses 1`] = ` +Object { + "fieldsInfo": Array [ + Object { + "prototype": "__ref__0__", + "rule": "required", + }, + Object { + "prototype": "__ref__0__", + "rule": "required", + }, + Object { + "prototype": "__ref__0__", + "rule": "required", + }, + Object { + "prototype": "string", + "rule": "required", + }, + ], + "refs": Array [ + Array [ + Object { + "prototype": "__ref__1__", + "rule": "required", + }, + Object { + "prototype": "string", + "rule": "required", + }, + ], + Array [ + Object { + "prototype": "__ref__2__", + "rule": "required", + }, + Object { + "prototype": "string", + "rule": "required", + }, + ], + Array [ + Object { + "prototype": "string", + "rule": "required", + }, + ], + ], +} +`; + +exports[`collectSchema(MessageClass) should return correct schema for numbers 1`] = ` +Object { + "fieldsInfo": Array [ + Object { + "prototype": "int32", + "rule": "required", + }, + ], + "refs": Array [], +} +`; + +exports[`collectSchema(MessageClass) should return correct schema for numbers 2`] = ` +Object { + "fieldsInfo": Array [ + Object { + "prototype": "int32", + "rule": "optional", + }, + ], + "refs": Array [], +} +`; + +exports[`collectSchema(MessageClass) should return correct schema for numbers 3`] = ` +Object { + "fieldsInfo": Array [ + Object { + "prototype": "int32", + "rule": "repeated", + }, + ], + "refs": Array [], +} +`; + +exports[`collectSchema(MessageClass) should return correct schema for strings 1`] = ` +Object { + "fieldsInfo": Array [ + Object { + "prototype": "string", + "rule": "required", + }, + ], + "refs": Array [], +} +`; + +exports[`collectSchema(MessageClass) should return correct schema for strings 2`] = ` +Object { + "fieldsInfo": Array [ + Object { + "prototype": "string", + "rule": "optional", + }, + ], + "refs": Array [], +} +`; + +exports[`collectSchema(MessageClass) should return correct schema for strings 3`] = ` +Object { + "fieldsInfo": Array [ + Object { + "prototype": "string", + "rule": "repeated", + }, + ], + "refs": Array [], +} +`; diff --git a/test/collectSchema.test.ts b/test/collectSchema.test.ts new file mode 100644 index 0000000..e7d2778 --- /dev/null +++ b/test/collectSchema.test.ts @@ -0,0 +1,248 @@ +import "@abraham/reflection"; +import { ProtobufLiteProperty } from "../src"; +import {} from "../src/metadataHelpers"; +import { collectSchema } from "../src/collectSchema"; + +describe("collectSchema(MessageClass)", () => { + it("should return correct schema for strings", () => { + class C1 { + @ProtobufLiteProperty() + name: string; + } + + class C2 { + @ProtobufLiteProperty({ optional: true }) + name?: string; + } + + class C3 { + @ProtobufLiteProperty({ type: () => String }) + names: string[]; + } + + expect(collectSchema(C1)).toMatchSnapshot(); + + expect(collectSchema(C2)).toMatchSnapshot(); + + expect(collectSchema(C3)).toMatchSnapshot(); + }); + + it("should return correct schema for numbers", () => { + class C1 { + @ProtobufLiteProperty() + age: number; + } + + class C2 { + @ProtobufLiteProperty({ optional: true }) + age?: number; + } + + class C3 { + @ProtobufLiteProperty({ type: () => Number }) + ages: number[]; + } + + expect(collectSchema(C1)).toMatchSnapshot(); + + expect(collectSchema(C2)).toMatchSnapshot(); + + expect(collectSchema(C3)).toMatchSnapshot(); + }); + + it("should return correct schema for booleans", () => { + class C1 { + @ProtobufLiteProperty() + isTrue: boolean; + } + + class C2 { + @ProtobufLiteProperty({ optional: true }) + isTrue?: boolean; + } + + class C3 { + @ProtobufLiteProperty({ type: () => Boolean }) + isTrues: boolean[]; + } + + expect(collectSchema(C1)).toMatchSnapshot(); + + expect(collectSchema(C2)).toMatchSnapshot(); + + expect(collectSchema(C3)).toMatchSnapshot(); + }); + + it("should return correct schema for Buffers", () => { + class C1 { + @ProtobufLiteProperty() + buffer: Buffer; + } + + class C2 { + @ProtobufLiteProperty({ optional: true }) + buffer?: Buffer; + } + + class C3 { + @ProtobufLiteProperty({ type: () => Buffer }) + buffers: Buffer[]; + } + + expect(collectSchema(C1)).toMatchSnapshot(); + + expect(collectSchema(C2)).toMatchSnapshot(); + + expect(collectSchema(C3)).toMatchSnapshot(); + }); + + it("should return correct schema for Dates", () => { + class C1 { + @ProtobufLiteProperty() + date: Date; + } + + class C2 { + @ProtobufLiteProperty({ optional: true }) + date?: Date; + } + + class C3 { + @ProtobufLiteProperty({ type: () => Date }) + dates: Date[]; + } + + expect(collectSchema(C1)).toMatchSnapshot(); + + expect(collectSchema(C2)).toMatchSnapshot(); + + expect(collectSchema(C3)).toMatchSnapshot(); + }); + + it("should return correct schema for inhreited classes v1", () => { + class Parent { + @ProtobufLiteProperty() + someField: string; + } + + class Child extends Parent { + @ProtobufLiteProperty() + someOtherField: string; + } + + expect(collectSchema(Child)).toMatchSnapshot(); + }); + + it("should return correct schema for inhreited classes v2", () => { + class C1 { + @ProtobufLiteProperty() + c1: string; + } + + class C2 extends C1 { + @ProtobufLiteProperty() + c2: string; + } + + class C3 extends C2 { + @ProtobufLiteProperty() + c3: string; + } + + class C4 extends C3 { + @ProtobufLiteProperty() + c4: string; + } + + class C5 extends C4 { + @ProtobufLiteProperty() + c5: string; + } + + expect(collectSchema(C1)).toMatchSnapshot(); + + expect(collectSchema(C2)).toMatchSnapshot(); + + expect(collectSchema(C3)).toMatchSnapshot(); + + expect(collectSchema(C4)).toMatchSnapshot(); + + expect(collectSchema(C5)).toMatchSnapshot(); + }); + + it("should return correct schema for inhreited classes v3", () => { + class Parent { + @ProtobufLiteProperty() + someField: string; + } + + class Child extends Parent {} + class Child1 extends Child {} + class Child2 extends Child1 {} + class Child3 extends Child1 { + @ProtobufLiteProperty() + c3: string; + } + + expect(collectSchema(Child)).toMatchSnapshot(); + + expect(collectSchema(Child1)).toMatchSnapshot(); + + expect(collectSchema(Child2)).toMatchSnapshot(); + + expect(collectSchema(Child3)).toMatchSnapshot(); + }); + + it("should return correct schema for nestedClasses", () => { + class C3 { + @ProtobufLiteProperty() + iAmC3: string; + } + + class C2 { + @ProtobufLiteProperty() + c3: C3; + + @ProtobufLiteProperty() + iAmC2: string; + } + + class C1 { + @ProtobufLiteProperty() + c2: C2; + + @ProtobufLiteProperty() + iAmC1: string; + } + + class CMain { + @ProtobufLiteProperty() + c1_0: C1; + + @ProtobufLiteProperty() + c1_1: C1; + + @ProtobufLiteProperty() + c1_2: C1; + + @ProtobufLiteProperty() + cMainYooo: string; + } + + expect(collectSchema(CMain)).toMatchSnapshot(); + }); + + it("should throw if child overwrites parent fields", () => { + class Parent { + @ProtobufLiteProperty() + someField: string; + } + + class Child extends Parent { + @ProtobufLiteProperty() + someField: string; + } + + expect(() => collectSchema(Child)).toThrowError(); + }); +});