From 538bd929ae3169f91cddbb02bb2a54cec3ec4e7c Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Mon, 24 Jan 2022 19:37:50 +0100 Subject: [PATCH] Changed the 'subType' field to 'type' to be more consise. Updated the README.md --- README.md | 295 ++++++++++++++++++++++++++++++++++- package.json | 2 +- src/parsed.test.ts | 1 - src/structure/object.test.ts | 4 +- src/structure/object.ts | 20 +-- 5 files changed, 302 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 3fe05b1..96f4392 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,42 @@ # Typed Binary +Created by Iwo Plaza +License +npm +stars + Gives tools to describe binary structures with full TypeScript support. Encodes and decodes into pure JavaScript objects, while giving type context for the parsed data. -## Features: +# Table of contents +- [Features](#features) +- [Installation](#installation) +- [Basic usage](#basic-usage) +- [Defining schemas](#defining-schemas) + - [Primitives](#primitives) + - [Objects](#objects) + - [Arrays](#arrays) + - [Tuples](#tuples) + - [Optionals](#optionals) +- [Serialization and Deserialization](#serialization-and-deserialization) + +# Features: - Type-safe schemas (your IDE will know what structure the parsed binary is in). - Generic objects - Estimating the size of any resulting binary object (helpful for creating buffered storage) -## Workflow example +### Why Typed Binary over other libraries? +- It's one of the few libraries (if not the only one) with fully staticly-typed binary schemas. +- It has **zero-dependencies**. +- It's platform independent (use it in Node.js as well as in in Browsers) +- While being made with TypeScript in mind, it also works with plain JavaScript. + +# Instalation +Using NPM: +```sh +$ npm i --save typed-binary +``` + + +# Basic usage ```ts import { object, arrayOf, INT, STRING, BOOL, Parsed } from 'typed-binary'; @@ -73,8 +103,261 @@ async function saveGameState(state: GameState): Promise { } ``` -## Instalation -Using NPM: -```sh -$ npm i --save typed-binary +# Defining schemas +## Primitives +There's a couple primitives to choose from: +- `BOOL` - an 8-bit value representing either `true` or `false`. + - Encoded as `1` if true, and as `0` if false. +- `BYTE` - an 8-bit value representing an unsigned number between 0 and 255. + - Encoded as-is +- `INT` - a 32-bit signed integer number container. + - Encoded as-is +- `FLOAT` - a 32-bit signed floating-point number container. + - Encoded as-is +- `STRING` - a variable-length string of ASCII characters. + - A string of characters followed by a '\0' terminal character. +```ts +import { BufferWriter, BufferReader, BYTE, STRING } from 'typed-binary'; + +const buffer = Buffer.alloc(16); +const writer = new BufferWriter(buffer); +const reader = new BufferReader(buffer); + +// Writing four bytes into the buffer +BYTE.write(writer, 'W'.charCodeAt(0)); +BYTE.write(writer, 'o'.charCodeAt(0)); +BYTE.write(writer, 'w'.charCodeAt(0)); +BYTE.write(writer, 0); + +console.log(STRING.read(reader)); // Wow ``` + +## Objects +Objects store their properties in key-ascending-alphabetical order, one next to another. +### Simple objects +```ts +import { BufferWriter, BufferReader, INT, STRING, object } from 'typed-binary'; + +const buffer = Buffer.alloc(16); +const writer = new BufferWriter(buffer); +const reader = new BufferReader(buffer); + +// Simple object schema +const Person = object({ + firstName: STRING, + lastName: STRING, + age: INT, +}); + +// Writing a Person +Person.write(writer, { + firstName: "John", + lastName: "Doe", + age: 43, +}); + +console.log(JSON.stringify(Person.read(reader).address)); // { "firstName": "John", ... } +``` +### Generic objects +This feature allows for the parsing of a type that contains different fields depending on it's previous values. For example, if you want to store an animal description, certain animal types might have differing features from one another. + +**Keyed by strings:** +```ts +import { BufferWriter, BufferReader, INT, STRING, generic, object } from 'typed-binary'; + +// Generic object schema +const Animal = generic({ + nickname: STRING, + age: INT, +}, { + 'dog': object({ // Animal can be a dog + breed: STRING, + }), + 'cat': object({ // Animal can be a cat + striped: BOOL, + }), +}); + +// A buffer to serialize into/out of +const buffer = Buffer.alloc(16); +const writer = new BufferWriter(buffer); +const reader = new BufferReader(buffer); + +// Writing an Animal +Animal.write(writer, { + type: 'cat', // We're specyfing which concrete type we want this object to be. + + // Base properties + nickname: 'James', + age: 5, + + // Concrete type specific properties + striped: true, +}); + +// Deserializing the animal +const animal = Animal.read(reader); + +console.log(JSON.stringify(animal)); // { "age": 5, "striped": true ... } + +// -- Type checking works here! -- +// animal.type => 'cat' | 'dog' +if (animal.type === 'cat') { + // animal.type => 'cat' + console.log("It's a cat!"); + // animal.striped => bool + console.log(animal.striped ? "Striped" : "Not striped"); +} +else { + // animal.type => 'dog' + console.log("It's a dog!"); + // animal.breed => string + console.log(`More specifically, a ${animal.breed}`); + + // This would result in a type error (Static typing FTW!) + // console.log(`Striped: ${animal.striped}`); +} +``` + +**Keyed by an enum (byte):** +```ts +import { BufferWriter, BufferReader, INT, STRING, genericEnum, object } from 'typed-binary'; + +enum AnimalType = { + DOG = 0, + CAT = 1, +}; + +// Generic (enum) object schema +const Animal = genericEnum({ + nickname: STRING, + age: INT, +}, { + [AnimalType.DOG]: object({ // Animal can be a dog + breed: STRING, + }), + [AnimalType.CAT]: object({ // Animal can be a cat + striped: BOOL, + }), +}); + +// ... +// Same as for the string keyed case +// ... + +// -- Type checking works here! -- +// animal.type => AnimalType +if (animal.type === AnimalType.CAT) { + // animal.type => AnimalType.CAT + console.log("It's a cat!"); + // animal.striped => bool + console.log(animal.striped ? "Striped" : "Not striped"); +} +else { + // animal.type => AnimalType.DOG + console.log("It's a dog!"); + // animal.breed => string + console.log(`More specifically, a ${animal.breed}`); + + // This would result in a type error (Static typing FTW!) + // console.log(`Striped: ${animal.striped}`); +} +``` + +## Arrays +First 4 bytes of encoding are the length of the array, then it's items next to one another. +``` +import { INT, arrayOf } from 'typed-binary'; + +const IntArray = arrayOf(INT); +``` + +## Tuples +The items are encoded right next to each other. No need to store length information, as that's constant (built into the tuple). +``` +import { FLOAT, tupleOf } from 'typed-binary'; + +const Vector2 = tupleOf(FLOAT, 2); +const Vector3 = tupleOf(FLOAT, 3); +const Vector4 = tupleOf(FLOAT, 4); +``` + +## Optionals +Optionals are a good way of ensuring that no excessive data is stored as binary. + +They are encoded as: +- `0` given `value === undefined`. +- `1 encoded(value)` given `value !== undefined`. + +```ts +import { BufferWriter, BufferReader, INT, STRING, object, optional } from 'typed-binary'; + +const buffer = Buffer.alloc(16); +const writer = new BufferWriter(buffer); +const reader = new BufferReader(buffer); + +// Simple object schema +const Address = object({ + city: STRING, + street: STRING, + postalCode: STRING, +}); + +// Simple object schema (with optional field) +const Person = object({ + firstName: STRING, + lastName: STRING, + age: INT, + address: optional(Address), +}); + +// Writing a Person (no address) +Person.write(writer, { + firstName: "John", + lastName: "Doe", + age: 43, +}); + +// Writing a Person (with an address) +Person.write(writer, { + firstName: "Jesse", + lastName: "Doe", + age: 38, + address: { + city: "New York", + street: "Binary St.", + postalCode: "11-111", + }, +}); + +console.log(JSON.stringify(Person.read(reader).address)); // undefined +console.log(JSON.stringify(Person.read(reader).address)); // { "city": "New York", ... } +``` + +# Serialization and Deserialization +Each schema has the following methods: +```ts +/** + * Writes the value (according to the schema's structure) to the output. + */ +write(output: ISerialOutput, value: T): void; +/** + * Reads a value (according to the schema's structure) from the input. + */ +read(input: ISerialInput): T; +/** + * Estimates the size of the value (according to the schema's structure) + */ +sizeOf(value: T): number; +``` + +The `ISerialInput/Output` interfaces have a basic built-in implementation that reads/writes to a buffer: +```ts +import { BufferReader, BufferWriter } from 'typed-binary'; + +// Creating a fixed-length buffer of arbitrary size (64 bytes). +const buffer = Buffer.alloc(64); // Or new ArrayBuffer(64); on browsers. + +const reader = new BufferReader(buffer); // Implements ISerialInput +const writer = new BufferWriter(buffer); // Implements ISerialOutput +``` \ No newline at end of file diff --git a/package.json b/package.json index cb86390..7439b23 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "dist" ], "author": "Iwo Plaza ", - "license": "ISC", + "license": "MIT", "devDependencies": { "@rollup/plugin-commonjs": "^21.0.1", "@rollup/plugin-node-resolve": "^13.1.3", diff --git a/src/parsed.test.ts b/src/parsed.test.ts index f9e2293..83a0f72 100644 --- a/src/parsed.test.ts +++ b/src/parsed.test.ts @@ -41,4 +41,3 @@ export const KeyframeNodeTemplate = type KeyframeNodeTemplate = Parsed; const s = {} as KeyframeNodeTemplate; - diff --git a/src/structure/object.test.ts b/src/structure/object.test.ts index 04f8d52..6cf0d21 100644 --- a/src/structure/object.test.ts +++ b/src/structure/object.test.ts @@ -34,7 +34,7 @@ describe('(read/write)Object', () => { }); const value = { - subType: 'concrete' as const, + type: 'concrete' as const, sharedValue: 100, extraValue: 10, }; @@ -56,7 +56,7 @@ describe('(read/write)Object', () => { }); const value = { - subType: 0 as const, + type: 0 as const, sharedValue: 100, extraValue: 10, }; diff --git a/src/structure/object.ts b/src/structure/object.ts index 1abb55c..e1f1fbf 100644 --- a/src/structure/object.ts +++ b/src/structure/object.ts @@ -1,5 +1,5 @@ import type { ISerialInput, ISerialOutput } from '../io'; -import { ValueOrProvider } from '../utilityTypes'; +import type { ValueOrProvider } from '../utilityTypes'; import { STRING } from './baseTypes'; import { Schema, InferedProperties, SchemaProperties @@ -46,7 +46,7 @@ export class ObjectSchema}> = { - [Key in keyof T]: T[Key]['_infered'] & { subType: Key } + [Key in keyof T]: T[Key]['_infered'] & { type: Key } }; export type ObjectSchemaMap = {[key in keyof S]: ObjectSchema}; @@ -70,17 +70,17 @@ export class GenericObjectSchema< write(output: ISerialOutput, value: InferedProperties & InferedSubTypes[keyof S]): void { // Figuring out sub-types - const subTypeDescription = this.getSubTypeMap()[value.subType] || null; + const subTypeDescription = this.getSubTypeMap()[value.type] || null; if (subTypeDescription === null) { - throw new Error(`Unknown sub-type '${value.subType}' in among '${JSON.stringify(Object.keys(this.subTypeMap))}'`); + throw new Error(`Unknown sub-type '${value.type}' in among '${JSON.stringify(Object.keys(this.subTypeMap))}'`); } // Writing the sub-type out. if (this.keyedBy === SubTypeKey.ENUM) { - output.writeByte(value.subType as number); + output.writeByte(value.type as number); } else { - output.writeString(value.subType as string); + output.writeString(value.type as string); } // Writing the base properties @@ -108,7 +108,7 @@ export class GenericObjectSchema< let result: any = super.read(input); // Making the sub type key available to the result object. - result.subType = subTypeKey; + result.type = subTypeKey; if (subTypeDescription !== null) { const extraKeys: string[] = Object.keys(subTypeDescription.properties).sort(); @@ -127,12 +127,12 @@ export class GenericObjectSchema< let size = super.sizeOf(value); // We're a generic object trying to encode a concrete value. - size += this.keyedBy === SubTypeKey.ENUM ? 1 : STRING.sizeOf(value.subType as string); + size += this.keyedBy === SubTypeKey.ENUM ? 1 : STRING.sizeOf(value.type as string); // Extra sub-type fields - const subTypeDescription = this.getSubTypeMap()[value.subType] || null; + const subTypeDescription = this.getSubTypeMap()[value.type] || null; if (subTypeDescription === null) { - throw new Error(`Unknown sub-type '${value.subType}' in among '${JSON.stringify(Object.keys(this.subTypeMap))}'`); + throw new Error(`Unknown sub-type '${value.type}' in among '${JSON.stringify(Object.keys(this.subTypeMap))}'`); } size += Object.keys(subTypeDescription.properties) // Going through extra property keys