Skip to content

Commit

Permalink
feat: custom class serialization
Browse files Browse the repository at this point in the history
  • Loading branch information
marcus-pousette committed Jan 17, 2024
1 parent b35bf99 commit 1b3477c
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 38 deletions.
40 changes: 39 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ class TestStruct {
```

**Custom serialization and deserialization**
Override how one field is handled

```typescript
class TestStruct {

Expand All @@ -245,12 +247,48 @@ class TestStruct {
}
}

validate(TestStruct);
const serialized = serialize(new TestStruct(3));
const deserialied = deserialize(serialized, TestStruct);
expect(deserialied.number).toEqual(3);
```

Override how one class is serialized

```typescript
import { serializer } from '@dao-xyz/borsh'
class TestStruct {

@field({type: 'u8'})
public number: number;

constructor(number: number) {
this.number = number;
}

cache: Uint8Array | undefined;

@serializer()
override(writer: BinaryWriter, serialize: (obj: this) => Uint8Array) {
if (this.cache) {
writer.set(this.cache)
}
else {
this.cache = serialize(this)
writer.set(this.cache)
}
}
}

const obj = new TestStruct(3);
const serialized = serialize(obj);
const deserialied = deserialize(serialized, TestStruct);
expect(deserialied.number).toEqual(3);
expect(obj.cache).toBeDefined()
```





## Inheritance
Schema generation is supported if deserialization is deterministic. In other words, all classes extending some super class needs to use discriminators/variants of the same type.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dao-xyz/borsh",
"version": "5.1.8",
"version": "5.2.0",
"readme": "README.md",
"homepage": "https://github.com/dao-xyz/borsh-ts#README",
"description": "Binary Object Representation Serializer for Hashing simplified with decorators",
Expand Down
157 changes: 135 additions & 22 deletions src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
getSchema,
BorshError,
string,
serializer,
} from "../index.js";
import crypto from "crypto";

Expand Down Expand Up @@ -1584,34 +1585,146 @@ describe("string", () => {
});
});

describe("bool", () => {
test("field bolean", () => {
describe("options", () => {
class Test {
@field({ type: "u8" })
number: number;
constructor(number: number) {
this.number = number;
}
}
test("pass writer", () => {
const writer = new BinaryWriter();
writer.u8(1);
expect(new Uint8Array(serialize(new Test(123), writer))).toEqual(
new Uint8Array([1, 123])
);
});
});

describe("override", () => {
describe("serializer", () => {
class TestStruct {
@field({ type: "bool" })
public a: boolean;
@serializer()
override(writer: BinaryWriter) {
writer.u8(1);
}
}

constructor(a: boolean) {
this.a = a;
class TestStructMixed {
@field({ type: TestStruct })
nested: TestStruct;

@field({ type: "u8" })
number: number;

cached: Uint8Array;

constructor(number: number) {
this.nested = new TestStruct();
this.number = number;
}

@serializer()
override(writer: BinaryWriter, serialize: (obj: this) => Uint8Array) {
if (this.cached) {
writer.set(this.cached);
} else {
this.cached = serialize(this);
writer.set(this.cached);
}
}
}
validate(TestStruct);
const expectedResult: StructKind = new StructKind({
fields: [
{
key: "a",
type: "bool",
},
],

class TestStructMixedNested {
@field({ type: TestStructMixed })
nested: TestStructMixed;

@field({ type: "u8" })
number: number;

cached: Uint8Array;

constructor(number: number) {
this.nested = new TestStructMixed(number);
this.number = number;
}

@serializer()
override(writer: BinaryWriter, serialize: (obj: this) => Uint8Array) {
if (this.cached) {
writer.set(this.cached);
} else {
this.cached = serialize(this);
writer.set(this.cached);
}
}
}

class TestStructNested {
@field({ type: TestStruct })
nested: TestStruct;

@field({ type: "u8" })
number: number;
constructor() {
this.nested = new TestStruct();
this.number = 2;
}
}

class TestBaseClass {
@serializer()
override(writer: BinaryWriter) {
writer.u8(3);
}
}
@variant(2)
class TestStructInherited extends TestBaseClass {
@field({ type: TestStruct })
struct: TestStruct;

@field({ type: "u8" })
number: number;
constructor() {
super();
this.struct = new TestStruct();
this.number = 0;
}
}

test("struct", () => {
expect(new Uint8Array(serialize(new TestStruct()))).toEqual(
new Uint8Array([1])
);
});
expect(getSchema(TestStruct)).toEqual(expectedResult);
const buf = serialize(new TestStruct(true));
expect(new Uint8Array(buf)).toEqual(new Uint8Array([1]));
const deserializedSome = deserialize(new Uint8Array(buf), TestStruct);
expect(deserializedSome.a).toEqual(true);
});
});
test("recursive call", () => {
const obj = new TestStructMixed(2);
expect(new Uint8Array(serialize(obj))).toEqual(new Uint8Array([1, 2]));
expect(new Uint8Array(obj.cached)).toEqual(new Uint8Array([1, 2]));
expect(new Uint8Array(serialize(obj))).toEqual(new Uint8Array([1, 2]));
});
test("recursive call nested", () => {
const obj = new TestStructMixedNested(2);
expect(new Uint8Array(serialize(obj))).toEqual(new Uint8Array([1, 2, 2]));
expect(new Uint8Array(obj.cached)).toEqual(new Uint8Array([1, 2, 2]));
expect(new Uint8Array(obj.nested.cached)).toEqual(new Uint8Array([1, 2]));

describe("override", () => {
expect(new Uint8Array(serialize(obj))).toEqual(new Uint8Array([1, 2, 2]));
});

test("nested", () => {
expect(new Uint8Array(serialize(new TestStructNested()))).toEqual(
new Uint8Array([1, 2])
);
});

test("inherited", () => {
expect(new Uint8Array(serialize(new TestStructInherited()))).toEqual(
new Uint8Array([3, 2, 1, 0])
);
});
});
test("serialize/deserialize", () => {
/**
* Serialize field with custom serializer and deserializer
Expand Down
7 changes: 7 additions & 0 deletions src/binary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,13 @@ export class BinaryWriter {
writer.totalSize += lengthSize + len;
}

public set(array: Uint8Array) {
let offset = this.totalSize;
this._writes = this._writes.next = () => {
this._buf.set(array, offset);
}
this.totalSize += array.length
}

public uint8Array(array: Uint8Array) {
return BinaryWriter.uint8Array(array, this)
Expand Down
59 changes: 45 additions & 14 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,15 @@ const PROTOTYPE_SCHEMA_OFFSET = PROTOTYPE_DESERIALIZATION_HANDLER_OFFSET + PROTO
* @returns bytes
*/
export function serialize(
obj: any
obj: any,
writer: BinaryWriter = new BinaryWriter()
): Uint8Array {
const writer = new BinaryWriter();
(obj.constructor._borsh_serialize || (obj.constructor._borsh_serialize = serializeStruct(obj.constructor)))(obj, writer)
(obj.constructor._borsh_serialize || (obj.constructor._borsh_serialize = serializeStruct(obj.constructor, true)))(obj, writer)
return writer.finalize();
}

function recursiveSerialize(obj: any, writer: BinaryWriter = new BinaryWriter()) {
(obj.constructor._borsh_serialize_recursive || (obj.constructor._borsh_serialize_recursive = serializeStruct(obj.constructor, false)))(obj, writer)
return writer.finalize();
}

Expand Down Expand Up @@ -196,7 +201,8 @@ function serializeField(


function serializeStruct(
ctor: Function
ctor: Function,
allowCustomSerializer = true
) {
let handle: (obj: any, writer: BinaryWriter) => any = undefined;
var i = 0;
Expand Down Expand Up @@ -236,20 +242,29 @@ function serializeStruct(
} : (_obj, writer) => writer.string(index);
}
}
for (const field of schema.fields) {
if (allowCustomSerializer && schema.serializer) {
let prev = handle;
const fieldHandle = serializeField(field.key, field.type);
if (prev) {
handle = (obj, writer) => {
prev(obj, writer);
fieldHandle(obj[field.key], writer)
handle = prev ? (obj, writer) => {
prev(obj, writer);
schema.serializer(obj, writer, (obj: any) => recursiveSerialize(obj))
} : (obj, writer) => schema.serializer(obj, writer, (obj: any) => recursiveSerialize(obj))
}
else {
for (const field of schema.fields) {
let prev = handle;
const fieldHandle = serializeField(field.key, field.type);
if (prev) {
handle = (obj, writer) => {
prev(obj, writer);
fieldHandle(obj[field.key], writer)
}
}
else {
handle = (obj, writer) => fieldHandle(obj[field.key], writer)
}
}
else {
handle = (obj, writer) => fieldHandle(obj[field.key], writer)
}

}

}

else if (once && !getDependencies(ctor, i)?.length) {
Expand Down Expand Up @@ -740,6 +755,22 @@ export function field(properties: SimpleField | CustomField<any>) {
};
}


/**
* @param properties, the properties of the field mapping to schema
* @returns
*/
export function serializer() {
return function (target: any, propertyKey: string) {
const offset = getOffset(target.constructor);
const schemas = getOrCreateStructMeta(target.constructor, offset);
schemas.serializer = (obj, writer, serialize) => obj[propertyKey](writer, serialize)
};
}




/**
* @param clazzes
* @param validate, run validation?
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ export interface Field {

export class StructKind {
variant?: number | number[] | string
serializer?: (any: any, writer: BinaryWriter, serialize: (obj: any) => Uint8Array) => void
fields: Field[];
constructor(properties?: { variant?: number | number[] | string, fields: Field[] }) {
if (properties) {
Expand Down

0 comments on commit 1b3477c

Please sign in to comment.