diff --git a/README.md b/README.md index f042b4c..f982f77 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ const serialization = new Searilie(new CSVCompressor()); console.log(serialization.encode([{a: "kick", b: 51}, {a: "cat", b: 92}])); // Bkick,51;cat,92 ``` -## deserialization +## Deserialization the first character on encoded payload denotes which compressor was used, we need to use the same compressor to ensure we don't load everything at once, we don't import everything and check it for you. ```typescript import {Searilie, ValueType} from "./src/Searilie" @@ -45,3 +45,14 @@ const serialization = new Searilie(new CSVCompressor()); console.log(serialization.decode("Bkick,51;cat,92", {a: ValueType.String, b: ValueType.Number})); // [{a: "kick", b: 51}, {a: "cat", b: 92}] console.log(serialization.decode("Bkick,51;cat,92", {myKey: ValueType.String, newKey: ValueType.Number})); // [{myKey: "kick", newKey: 51}, {myKey: "cat", newKey: 92}] ``` + +## With headers +by trading off some character spaces, we can also encode data with keys so we don't need to provide schema while decoding + +```typescript +import {Searilie} from "./src/Searilie" +import {CSVCompressor} from "./src/adapters/CSVCompressor"; +const serialization = new Searilie(new CSVCompressor()); +console.log(serialization.encodeWithHeaders([{a: "kick", b: 51}, {a: "cat", b: 92}])); // Ba,|b:kick,51;cat,92 +console.log(serialization.decodeUsingHeaders("Ba,|b:kick,51;cat,92")); // [{a: "kick", b: 51}, {a: "cat", b: 92}] +``` diff --git a/src/Searilie.spec.ts b/src/Searilie.spec.ts index 242509a..7600afe 100644 --- a/src/Searilie.spec.ts +++ b/src/Searilie.spec.ts @@ -1,4 +1,5 @@ -import {TinyCompressor} from "./adapters/TinyCompressor"; +import {CSVCompressor, TinyCompressor} from "./adapters"; +import {HEADER_SEPARATOR, INTEGER_IDENTIFIER, PAYLOAD_SEPARATOR} from "./constants"; import {IAdapter, IObject, Searilie, ValueType} from "./Searilie"; describe("Searilie", () => { @@ -8,10 +9,9 @@ describe("Searilie", () => { }); it("should throw error if identifier mismatches", () => { const mockAdapter: IAdapter = { - deserialize: (): IObject[] => [{a: "2"}], getIdentifier: () => "A", serialize: () => "something" - }; + } as any; const serializer = new Searilie(mockAdapter); expect(serializer.encode([{a: "s"}])).toBe("Asomething"); expect(() => serializer.decode("Bs", {a: ValueType.String})).toThrowError("adapter mismatched"); @@ -19,9 +19,8 @@ describe("Searilie", () => { it("should deserialize using adapter", () => { const mockAdapter: IAdapter = { deserialize: (): IObject[] => [{a: 2}], - getIdentifier: () => "Z", - serialize: () => "something" - }; + getIdentifier: () => "Z" + } as any; const serializer = new Searilie(mockAdapter); expect(serializer.decode("Z23", {a: ValueType.Number})).toStrictEqual([{a: 2}]); }); @@ -30,4 +29,35 @@ describe("Searilie", () => { expect(serializer.encode([{a: "h", b: 2}])).toEqual("Ah2"); expect(serializer.decode("Ah2", {a: ValueType.String, b: ValueType.Number})).toStrictEqual([{a: "h", b: 2}]); }); + describe("encode with headers", () => { + it("should have headers while encoding", () => { + const serializer = new Searilie(new TinyCompressor()); + expect(serializer.encodeWithHeaders([{a: "h", b: 2}])).toBe(`Aa${HEADER_SEPARATOR}${INTEGER_IDENTIFIER}b${PAYLOAD_SEPARATOR}h2`); + }); + }); + describe("decode using headers", () => { + it("should throw error if wrong adapter", () => { + const adapter: IAdapter = { + getIdentifier: jest.fn(() => "Z") + } as any; + const serializer = new Searilie(adapter); + expect(() => serializer.decodeUsingHeaders(`Aa,${INTEGER_IDENTIFIER}b:h2`)).toThrow("adapter mismatched"); + }); + it("should be able to decode properly", () => { + const adapter: IAdapter = { + deserialize: jest.fn(() => [{a: 2}]), + getIdentifier: jest.fn(() => "A") + } as any; + const serializer = new Searilie(adapter); + expect(serializer.decodeUsingHeaders(`Aa,${INTEGER_IDENTIFIER}b:h2`)).toStrictEqual([{a: 2}]); + }); + }); + describe("decode and encode with headers", () => { + it("should decode and encode properly", () => { + const tinySerializer = new Searilie(new TinyCompressor()); + const csv = new Searilie(new CSVCompressor()); + expect(tinySerializer.encodeWithHeaders([{a: 2, b: 5}, {a: 3, b: 8}])).toBe(`A${INTEGER_IDENTIFIER}a${HEADER_SEPARATOR}${INTEGER_IDENTIFIER}b${PAYLOAD_SEPARATOR}2538`); + expect(csv.encodeWithHeaders([{a: "23", b: 2}, {a: "35", b: 100}])).toBe(`Ba${HEADER_SEPARATOR}${INTEGER_IDENTIFIER}b${PAYLOAD_SEPARATOR}23,2;35,100`); + }); + }); }); diff --git a/src/Searilie.ts b/src/Searilie.ts index c9e901c..e76bcf2 100644 --- a/src/Searilie.ts +++ b/src/Searilie.ts @@ -1,3 +1,5 @@ +import {HEADER_SEPARATOR, INTEGER_IDENTIFIER, PAYLOAD_SEPARATOR} from "./constants"; + export interface IObject { [key: string]: number | string; } @@ -20,20 +22,66 @@ export interface IAdapter { deserialize(text: string, schema: ISchema): IObject[]; getIdentifier(): TIdentifier; } +interface IPayloadInfo { + identifier: TIdentifier; + payload: string; +} export class Searilie { constructor(private adapter: IAdapter) {} + private static getIdentifierAndPayload(text: string): IPayloadInfo { + if (text.length === 0) { + throw new Error("Invalid payload"); + } + const [firstCharacter, ...rest] = text; + return { + identifier: firstCharacter.toUpperCase() as TIdentifier, + payload: rest.join("") + }; + } + public encodeWithHeaders(object: IObject[]): string { + const headers = this.getHeaders(object); + const encodedData = this.adapter.serialize(object); + return `${this.adapter.getIdentifier()}${headers}${PAYLOAD_SEPARATOR}${encodedData}`; + } + + public getSchemaFromHeaders(text: string): ISchema { + const schema: ISchema = {}; + const headers = text.split(":")[0]; + headers.split(",").forEach((x) => { + const [first, ...rest] = x; + if (first === INTEGER_IDENTIFIER) { + schema[rest.join("")] = ValueType.Number; + } else { + schema[x] = ValueType.String; + } + }); + return schema; + } + public decodeUsingHeaders(value: string): IObject[] { + const schema = this.getSchemaFromHeaders(value.split(":")[0]); + const {payload, identifier} = Searilie.getIdentifierAndPayload(value); + if (this.adapter.getIdentifier() !== identifier) { + throw new Error("adapter mismatched"); + } + return this.adapter.deserialize(payload, schema); + } public encode(object: IObject[]): string { return `${this.adapter.getIdentifier()}${this.adapter.serialize(object)}`; } + public decode(value: string, schema: ISchema): IObject[] { - if (value.length === 0) { - throw new Error("Invalid payload"); - } - const [firstCharacter, ...rest] = value; - if (this.adapter.getIdentifier() !== firstCharacter) { + const {payload, identifier} = Searilie.getIdentifierAndPayload(value); + if (this.adapter.getIdentifier() !== identifier) { throw new Error("adapter mismatched"); } - return this.adapter.deserialize(rest.join(""), schema); + return this.adapter.deserialize(payload, schema); + } + + private getHeaders(objects: IObject[]): string { + return Object.keys(objects[0]) + .sort() + .map((x) => typeof objects[0][x] === "number" ? `${INTEGER_IDENTIFIER}${x}` : x) + .join(HEADER_SEPARATOR); } } diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..3b17087 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,3 @@ +export const INTEGER_IDENTIFIER = "|"; +export const HEADER_SEPARATOR = ","; +export const PAYLOAD_SEPARATOR = "_";