Skip to content

Commit

Permalink
feat: added support for z.optional and z.nullable
Browse files Browse the repository at this point in the history
removed the use of Map to make blocks json serializable
added fromBlocks
  • Loading branch information
arumi-s committed Aug 1, 2024
1 parent 2bd456e commit bea4836
Show file tree
Hide file tree
Showing 11 changed files with 362 additions and 32 deletions.
2 changes: 1 addition & 1 deletion .releaserc.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"branches": ["master"]
}
}
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ npm i zod-pbf-binary-serializer
- z.instanceof(Uint8Array)
- z.discriminatedUnion
- z.literal(string | number | boolean)
- z.optional
- z.nullable

## Usage

Expand Down Expand Up @@ -62,6 +64,49 @@ console.log(decoded);
// { a: 'apple', b: 123, c: true, d: ['hello', 'world'] }
```

### Export and import parsed blocks

```typescript
import { fromSchema, fromBlocks } from 'zod-pbf-binary-serializer';

const schema = z.object({
a: z.string(),
b: z.number(),
c: z.boolean(),
d: z.array(z.string()),
});

const serializer = fromSchema(schema);

console.log(serializer.blocks);
/**
* [
* {
* block: 'primitive',
* type: 'string',
* path: ['a'],
* },
* {
* block: 'primitive',
* type: 'float',
* path: ['b'],
* },
* {
* block: 'primitive',
* type: 'boolean',
* path: ['c'],
* },
* {
* block: 'array',
* type: 'string',
* path: ['d'],
* },
* ]
*/

const reconstructedSerializer = fromBlocks(serializer.blocks);
```

## License

[MIT](https://github.com/arumi-s/zod-pbf-binary-serializer/blob/master/LICENSE)
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "zod-pbf-binary-serializer",
"version": "1.0.0",
"version": "1.1.0",
"description": "Serialize and deserialize zod schemas to and from a compact binary format",
"type": "module",
"sideEffects": false,
Expand Down
16 changes: 12 additions & 4 deletions src/decode.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type Pbf from 'pbf';
import type { Primitive } from 'zod';
import type { Block, PrimitiveBlockType } from './types/block';

/**
Expand Down Expand Up @@ -51,6 +50,10 @@ const decodeBuffer = (pbf: Pbf): Uint8Array => {
return pbf.readBytes();
};

const decodeNull = (pbf: Pbf): null => {
return null;
};

const decodeBooleanArray = (pbf: Pbf, length: number): boolean[] => {
const array: boolean[] = new Array(length);
const bits = pbf.readBytes();
Expand All @@ -73,18 +76,23 @@ const chooseDecoder = (type: PrimitiveBlockType) => {
return decodeBoolean;
} else if (type === 'buffer') {
return decodeBuffer;
} else if (type === 'null') {
return decodeNull;
}

throw new Error(`Unknown type: ${type}`);
};

export function decode(pbf: Pbf, blocks: Block[], data: any = {}) {
for (const block of blocks) {
if (block.block === 'discriminator') {
const decoder = chooseDecoder(block.type);
const value = decoder(pbf);
data = setter(data, [...block.path, block.discriminator], value);
const discriminatorValue = decoder(pbf);
if (block.discriminator !== '') {
data = setter(data, [...block.path, block.discriminator], discriminatorValue);
}

const selected = block.options.get(value as Primitive);
const selected = block.options.find(([key]) => key === discriminatorValue)?.[1];
if (selected) {
data = decode(pbf, selected, data);
}
Expand Down
24 changes: 21 additions & 3 deletions src/encode.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type Pbf from 'pbf';
import type { Primitive } from 'zod';
import type { Block, PrimitiveBlockType } from './types/block';

/**
Expand Down Expand Up @@ -37,6 +36,8 @@ const encodeBuffer = (value: unknown, pbf: Pbf): void => {
pbf.writeBytes(value as Uint8Array);
};

const encodeNull = (value: unknown, pbf: Pbf): void => {};

const encodeBooleanArray = (array: unknown[], pbf: Pbf): void => {
const bits = new Uint8Array(Math.ceil(array.length / 8));
for (let i = 0; i < array.length; i++) {
Expand All @@ -58,18 +59,35 @@ const chooseEncoder = (type: PrimitiveBlockType) => {
return encodeBoolean;
} else if (type === 'buffer') {
return encodeBuffer;
} else if (type === 'null') {
return encodeNull;
}

throw new Error(`Unknown type: ${type}`);
};

export const optional = (value: unknown): number => {
if (typeof value === 'undefined') {
// value not exist or value equals undefined
return 0;
}
if (value === null) {
// value exists and not null
return 2;
}
// value exists and is null
return 1;
};

export function encode(data: any, blocks: Block[], pbf: Pbf) {
for (const block of blocks) {
if (block.block === 'discriminator') {
const discriminatorValue = getter(data, [...block.path, block.discriminator]);
const discriminatorValue =
block.discriminator === '' ? optional(getter(data, block.path)) : getter(data, [...block.path, block.discriminator]);
const encoder = chooseEncoder(block.type);
encoder(discriminatorValue, pbf);

const selected = block.options.get(discriminatorValue as Primitive);
const selected = block.options.find(([key]) => key === discriminatorValue)?.[1];
if (selected) {
encode(data, selected, pbf);
}
Expand Down
8 changes: 7 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@ import type { SerializableSchema } from './types/schema';
import { parseSchema } from './parse';
import { encode } from './encode';
import { decode } from './decode';
import { Block } from './types/block';

export * from './types/block';
export * from './types/schema';

export function fromSchema<T extends SerializableSchema>(schema: T) {
const blocks = parseSchema(schema);
return fromBlocks<T>(parseSchema(schema));
}

export function fromBlocks<T extends SerializableSchema>(blocks: Block[]) {
return {
/**
* get the blocks of the schema
Expand Down
35 changes: 26 additions & 9 deletions src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ export function parseSchema(schema: SerializableSchema, blocks: Block[] = [], pa
// z.object
if (schema._def.typeName === ZodFirstPartyTypeKind.ZodObject) {
const shape = schema._def.shape();
for (const i in shape) {
for (const key in shape) {
// parse schema recursively for each property
parseSchema(shape[i], blocks, [...path, i]);
parseSchema(shape[key], blocks, [...path, key]);
}

return blocks;
Expand All @@ -43,21 +43,38 @@ export function parseSchema(schema: SerializableSchema, blocks: Block[] = [], pa
const type = types.length === 1 ? types[0] : types.every((v) => ['int', 'float'].includes(v)) ? 'float' : null;
if (type === null) {
// throw an error if mixed types are used
throw new Error('Could not determine the type of the discriminated union');
throw new Error('Could not determine the type of the discriminator');
}
const discriminator = schema._def.discriminator;

// add the discriminator block
blocks.unshift({
block: 'discriminator',
type,
options: new Map(
Array.from(schema._def.optionsMap.entries()).map(
// parse schema recursively for each option, but filter out the discriminator because it's already added
([key, option]) => [key, parseSchema(option, [], []).filter((block) => block.path.join('.') !== discriminator)] as const,
),
options: Array.from(schema._def.optionsMap.entries()).map(
// parse schema recursively for each option, but filter out the discriminator because it's already added
([key, option]) => [key, parseSchema(option, [], []).filter((block) => block.path.join('.') !== discriminator)] as const,
),
discriminator: discriminator,
discriminator,
path,
});

return blocks;
}

// z.optional or z.nullable
if (schema._def.typeName === ZodFirstPartyTypeKind.ZodOptional || schema._def.typeName === ZodFirstPartyTypeKind.ZodNullable) {
// we don't distinguish between 'value not exist' (!Object.prototype.hasOwnProperty) and 'value equals undefined' (typeof value === 'undefined') because zod doesn't do so

blocks.push({
block: 'discriminator',
type: 'uint',
options: [
[0, []], // value not exist or value equals undefined
[1, parseSchema(schema._def.innerType, [], path)], // value exists and not null
[2, [{ block: 'primitive', type: 'null', path }]], // value exists and is null
],
discriminator: '',
path,
});

Expand Down
4 changes: 2 additions & 2 deletions src/types/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Primitive } from 'zod';

export type Path = string[];

export type PrimitiveBlockType = 'string' | 'float' | 'int' | 'uint' | 'boolean' | 'buffer';
export type PrimitiveBlockType = 'string' | 'float' | 'int' | 'uint' | 'boolean' | 'buffer' | 'null';

export type PrimitiveBlock = {
block: 'primitive';
Expand All @@ -20,7 +20,7 @@ export type ArrayBlock = {
export type DiscriminatorBlock = {
block: 'discriminator';
type: Exclude<PrimitiveBlockType, 'buffer'>;
options: Map<Primitive, Block[]>;
options: [Primitive, Block[]][];
discriminator: string;
path: Path;
};
Expand Down
2 changes: 2 additions & 0 deletions src/types/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export type SerializableDef =
| z.ZodNumberDef
| z.ZodObjectDef
| z.ZodStringDef
| z.ZodOptionalDef<SerializableSchema>
| z.ZodNullableDef<SerializableSchema>
| z.ZodLiteralDef<SerializableSchema>
| z.ZodEffectsDef<SerializableSchema>
| z.ZodDiscriminatedUnionDef<string>;
Expand Down
Loading

0 comments on commit bea4836

Please sign in to comment.