From 93f078f3850474ac1cd372b9991ddb945b38b70f Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Sun, 10 Sep 2023 15:07:55 +0200 Subject: [PATCH] Made base types more explicit in terms of precision and sign. --- .prettierrc | 6 + README.md | 435 +++++++++++-------- examples/binaryMesh/schemas.ts | 12 +- examples/genericEnumTypes/index.ts | 76 ++-- examples/genericTypes/index.ts | 70 +-- examples/inferredShowcase/index.ts | 14 +- examples/recursiveTypes/index.ts | 103 +++-- examples/stateMachine/connection.ts | 38 +- examples/stateMachine/graph.ts | 12 +- examples/stateMachine/node.ts | 18 +- examples/stateMachine/triggerCondition.ts | 34 +- package-lock.json | 4 +- src/describe/index.ts | 71 +-- src/io/bufferReaderWriter.test.ts | 112 ++--- src/io/types.ts | 26 +- src/parsed.test.ts | 71 +-- src/structure/array.ts | 69 +-- src/structure/baseTypes.ts | 122 +++--- src/structure/index.ts | 37 +- src/structure/object.ts | 298 +++++++------ src/test/array.test.ts | 48 +- src/test/bool.test.ts | 14 +- src/test/byte.test.ts | 14 +- src/test/float.test.ts | 14 +- src/test/int.test.ts | 14 +- src/test/keyed.test.ts | 506 ++++++++++++---------- src/test/object.test.ts | 207 ++++----- src/test/optional.test.ts | 30 +- src/test/string.test.ts | 40 +- 29 files changed, 1369 insertions(+), 1146 deletions(-) create mode 100644 .prettierrc diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..c03e51d --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "trailingComma": "all", + "arrowParens": "always", + "endOfLine": "auto" +} diff --git a/README.md b/README.md index c43879a..12d9796 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ 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. ## Prioritising Developer Experience + Serialise and deserialise typed schemas without the need for redundant interfaces or an external DSL. Schemas themselves define what type they encode and decode, and **the IDE knows it**! ![Basic Type and Documentation Inferrence](/docs/media/basic-type-and-doc-inferrence.gif) @@ -15,9 +16,11 @@ Serialise and deserialise typed schemas without the need for redundant interface Above is a self-contained code snippet using typed-binary. The IDE can properly infer what `Dog` is. What's even more interesting, is that the "parsed" properties inherit the schema's **JSDocs** (seen on the gif above). ## Highlight feature + The feature I am most proud of would have to be [recursive types](#recursive-types). I wasn't sure it it would be possible to achieve without additional tooling, but pushing the TypeScript type inference engine to it's extremes paid off. # Table of contents + - [Features](#features) - [Installation](#installation) - [Basic usage](#basic-usage) @@ -33,11 +36,13 @@ The feature I am most proud of would have to be [recursive types](#recursive-typ - [Serialization and Deserialization](#serialization-and-deserialization) # Features: + - [Type-safe schema definition system](#defining-schemas) (your IDE will know what structure the parsed binary is in). - [JSDoc inheritance](#prioritising-developer-experience) - [Estimating the size of any resulting binary object (helpful for creating buffered storage)](#serialization-and-deserialization) ### Why Typed Binary over other libraries? + - It's one of the few libraries (if not the only one) with fully staticly-typed binary schemas. - Since value types are inferred from the schemas themselves, there is a **single source-of-truth**. - No external DSL necessary to define the schemas, meaning you have instant feedback without the need to compile the interface definitions. @@ -46,31 +51,32 @@ The feature I am most proud of would have to be [recursive types](#recursive-typ - While being made with TypeScript in mind, it also works with plain JavaScript. # Instalation + Using NPM: + ```sh $ npm i --save typed-binary ``` # Requirements + To properly enable type inference, **TypeScript 4.5** and up is required because of it's newly added [Tail-Recursion Elimination on Conditional Types](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-5.html#tail-recursion-elimination-on-conditional-types) feature, # Basic usage + ```ts -import { - Parsed, - object, arrayOf, - INT, STRING, BOOL, -} from 'typed-binary'; +import { Parsed, object, arrayOf, i32, string, bool } from "typed-binary"; const GameState = object({ - nickname: STRING, // Variable-length string - stage: INT, // 32-bit integer - newGamePlus: BOOL, // byte-encoded boolean - collectables: arrayOf(STRING), // Variable-length string array - powerUpgrades: object({ // Nested object - health: BOOL, - strength: BOOL, - }), + nickname: string, // Variable-length string + stage: i32, // 32-bit integer + newGamePlus: bool, // byte-encoded boolean + collectables: arrayOf(string), // Variable-length string array + powerUpgrades: object({ + // Nested object + health: bool, + strength: bool, + }), }); // Type alias for ease-of-use @@ -78,32 +84,31 @@ type GameState = Parsed; //... -import { BufferReader, BufferWriter } from 'typed-binary'; +import { BufferReader, BufferWriter } from "typed-binary"; /** * Responsible for retrieving the saved game state. * If none can be found, returns a default starting state. */ async function loadGameState(): Promise { - try { - const buffer = await fs.readFile('./savedState.bin'); - const reader = new BufferReader(buffer); - - return GameState.read(reader); - } - catch (e) { - // Returning the default state if no saved state found. - return { - nickname: 'Default', - stage: 1, - newGamePlus: false, - collectables: [], - powerUpgrades: { - health: false, - strength: false, - }, - }; - } + try { + const buffer = await fs.readFile("./savedState.bin"); + const reader = new BufferReader(buffer); + + return GameState.read(reader); + } catch (e) { + // Returning the default state if no saved state found. + return { + nickname: "Default", + stage: 1, + newGamePlus: false, + collectables: [], + powerUpgrades: { + health: false, + strength: false, + }, + }; + } } /** @@ -111,59 +116,66 @@ async function loadGameState(): Promise { * @param state The state to save. */ async function saveGameState(state: GameState): Promise { - try { - const buffer = Buffer.alloc(GameState.sizeOf(state)); - const writer = new BufferWriter(buffer); - - GameState.write(writer, state); - await fs.writeFile('./savedState.bin', buffer); - } - catch (e) { - console.error(`Error occurred during the saving process.`); - console.error(e); - } + try { + const buffer = Buffer.alloc(GameState.sizeOf(state)); + const writer = new BufferWriter(buffer); + + GameState.write(writer, state); + await fs.writeFile("./savedState.bin", buffer); + } catch (e) { + console.error(`Error occurred during the saving process.`); + console.error(e); + } } ``` # Running examples + There are a handful of examples provided. To run any one of them make sure to clone the [typed-binary](https://github.com/iwoplaza/typed-binary) repository first, then go into the `examples/` directory. To setup the examples environment, run `npm run link`, which will build the parent project and link it to dependencies of the child 'examples' project. Pick an example that peaks interest, and run `npm run example:exampleName`. # 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. + +- `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 +- `i32` - a 32-bit signed integer number container. + - Encoded as-is +- `f32` - 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'; +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); +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 +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'; +import { BufferWriter, BufferReader, i32, string, object } from "typed-binary"; const buffer = Buffer.alloc(16); const writer = new BufferWriter(buffer); @@ -171,39 +183,55 @@ const reader = new BufferReader(buffer); // Simple object schema const Person = object({ - firstName: STRING, - lastName: STRING, - age: INT, + firstName: string, + lastName: string, + age: i32, }); // Writing a Person Person.write(writer, { - firstName: "John", - lastName: "Doe", - age: 43, + 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, BOOL, generic, object } from 'typed-binary'; +import { + BufferWriter, + BufferReader, + i32, + string, + bool, + generic, + object, +} from "typed-binary"; // Generic object schema -const Animal = generic({ - nickname: STRING, - age: INT, -}, { - 'dog': object({ // Animal can be a dog - breed: STRING, +const Animal = generic( + { + nickname: string, + age: i32, + }, + { + dog: object({ + // Animal can be a dog + breed: string, }), - 'cat': object({ // Animal can be a cat - striped: BOOL, + cat: object({ + // Animal can be a cat + striped: bool, }), -}); + } +); // A buffer to serialize into/out of const buffer = Buffer.alloc(16); @@ -212,14 +240,14 @@ 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. + type: "cat", // We're specyfing which concrete type we want this object to be. - // Base properties - nickname: 'James', - age: 5, + // Base properties + nickname: "James", + age: 5, - // Concrete type specific properties - striped: true, + // Concrete type specific properties + striped: true, }); // Deserializing the animal @@ -229,27 +257,26 @@ 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"); +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}`); } -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'; +import { BufferWriter, BufferReader, i32, string, genericEnum, object } from 'typed-binary'; enum AnimalType = { DOG = 0, @@ -258,14 +285,14 @@ enum AnimalType = { // Generic (enum) object schema const Animal = genericEnum({ - nickname: STRING, - age: INT, + nickname: string, + age: i32, }, { [AnimalType.DOG]: object({ // Animal can be a dog - breed: STRING, + breed: string, }), [AnimalType.CAT]: object({ // Animal can be a cat - striped: BOOL, + striped: bool, }), }); @@ -293,32 +320,45 @@ else { ``` ## 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'; +import { i32, arrayOf } from 'typed-binary'; -const IntArray = arrayOf(INT); +const IntArray = arrayOf(i32); ``` ## 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'; +import { f32, tupleOf } from 'typed-binary'; -const Vector2 = tupleOf(FLOAT, 2); -const Vector3 = tupleOf(FLOAT, 3); -const Vector4 = tupleOf(FLOAT, 4); +const Vector2 = tupleOf(f32, 2); +const Vector3 = tupleOf(f32, 3); +const Vector4 = tupleOf(f32, 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`. + +- `0` given `value === undefined`. +- `1 encoded(value)` given `value !== undefined`. ```ts -import { BufferWriter, BufferReader, INT, STRING, object, optional } from 'typed-binary'; +import { + BufferWriter, + BufferReader, + i32, + string, + object, + optional, +} from "typed-binary"; const buffer = Buffer.alloc(16); const writer = new BufferWriter(buffer); @@ -326,36 +366,36 @@ const reader = new BufferReader(buffer); // Simple object schema const Address = object({ - city: STRING, - street: STRING, - postalCode: STRING, + city: string, + street: string, + postalCode: string, }); // Simple object schema (with optional field) const Person = object({ - firstName: STRING, - lastName: STRING, - age: INT, - address: optional(Address), + firstName: string, + lastName: string, + age: i32, + address: optional(Address), }); // Writing a Person (no address) Person.write(writer, { - firstName: "John", - lastName: "Doe", - age: 43, + 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", - }, + 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 @@ -363,6 +403,7 @@ console.log(JSON.stringify(Person.read(reader).address)); // { "city": "New York ``` ## Recursive types + If you want an object type to be able to contain one of itself (recursion), then you have to start using **keyed** types. The basic pattern is this: ```ts @@ -370,98 +411,111 @@ If you want an object type to be able to contain one of itself (recursion), then * Wrapping a schema with a 'keyed' call allows the inner code to * use a reference to the type we're currently creating, instead * of the type itself. - * + * * The reference variable 'Recursive' doesn't have to be called * the same as the actual variable we're storing the schema in, * but it's a neat trick that makes the schema code more readable. - * + * * The 'recursive-key' has to uniquely identify this type in this tree. * There may be other distinct types using the same key, as long as they do * not interact with each other (one doesn't contain the other). * This is because references are resolved recursively once the method * passed as the 2nd argument to 'keyed' returns the schema. */ -const Recursive = keyed('recursive-key', (Recursive) => object({ - value: INT, +const Recursive = keyed("recursive-key", (Recursive) => + object({ + value: i32, next: optional(Recursive), -})) + }) +); ``` ### Recursive types alongside generics + ```ts -import { INT, STRING, object, keyed } from 'typed-binary'; +import { i32, string, object, keyed } from "typed-binary"; type Expression = Parsed; -const Expression = keyed('expression', (Expression) => generic({}, { - 'multiply': object({ +const Expression = keyed("expression", (Expression) => + generic( + {}, + { + multiply: object({ a: Expression, b: Expression, - }), - 'negate': object({ + }), + negate: object({ inner: Expression, - }), - 'int_literal': object({ - value: INT, - }), -})); + }), + int_literal: object({ + value: i32, + }), + } + ) +); const expr: Parsed = { - type: 'multiply', - a: { - type: 'negate', - inner: { - type: 'int_literal', - value: 15, - } - }, - b: { - type: 'int_literal', - value: 2, + type: "multiply", + a: { + type: "negate", + inner: { + type: "int_literal", + value: 15, }, + }, + b: { + type: "int_literal", + value: 2, + }, }; - ``` # Custom schema types + Custom schema types can be defined. They are, under the hood, classes that extend the `Schema` base class. The generic `T` type represents what kind of data this schema serializes from and deserializes into. ```ts -import { ISerialInput, ISerialOutput, Schema, IRefResolver } from 'typed-binary'; +import { + ISerialInput, + ISerialOutput, + Schema, + IRefResolver, +} from "typed-binary"; /** * A schema storing radians with 2 bytes of precision. */ class RadiansSchema extends Schema { - resolve(ctx: IRefResolver): void { - // No inner references to resolve - } - - read(input: ISerialInput): number { - const low = input.readByte(); - const high = input.readByte(); - - const discrete = (high << 8) | low; - return discrete / 65535 * Math.PI; - } - - write(output: ISerialOutput, value: number): void { - // The value will be wrapped to be in range of [0, Math.PI) - const wrapped = ((value % Math.PI) + Math.PI) % Math.PI; - // Discretising the value to be ints in range of [0, 65535] - const discrete = Math.min(Math.floor(wrapped / Math.PI * 65535), 65535); - - const low = discrete & 0xFF; - const high = (discrete >> 8) & 0xFF; - - output.writeByte(low); - output.writeByte(high); - } - - sizeOf(_: number): number { - // The size of the data serialized by this schema - // doesn't depend on the actual value. It's always 2 bytes. - return 2; - } + resolve(ctx: IRefResolver): void { + // No inner references to resolve + } + + read(input: ISerialInput): number { + const low = input.readByte(); + const high = input.readByte(); + + const discrete = (high << 8) | low; + return (discrete / 65535) * Math.PI; + } + + write(output: ISerialOutput, value: number): void { + // The value will be wrapped to be in range of [0, Math.PI) + const wrapped = ((value % Math.PI) + Math.PI) % Math.PI; + // Discretising the value to be ints in range of [0, 65535] + const discrete = Math.min(Math.floor((wrapped / Math.PI) * 65535), 65535); + + const low = discrete & 0xff; + const high = (discrete >> 8) & 0xff; + + output.writeByte(low); + output.writeByte(high); + } + + sizeOf(_: number): number { + // The size of the data serialized by this schema + // doesn't depend on the actual value. It's always 2 bytes. + return 2; + } } // Creating a singleton instance of the schema, @@ -470,7 +524,9 @@ export const RADIANS = new RadiansSchema(); ``` # Serialization and Deserialization + Each schema has the following methods: + ```ts /** * Writes the value (according to the schema's structure) to the output. @@ -487,8 +543,9 @@ 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'; +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. diff --git a/examples/binaryMesh/schemas.ts b/examples/binaryMesh/schemas.ts index 345dfc1..7191b87 100644 --- a/examples/binaryMesh/schemas.ts +++ b/examples/binaryMesh/schemas.ts @@ -1,18 +1,18 @@ import { Parsed } from 'typed-binary'; -import { object, arrayOf, tupleOf, INT } from 'typed-binary'; +import { object, arrayOf, tupleOf, i32 } from 'typed-binary'; export const Vertex = object({ - x: INT, - y: INT, - z: INT, + x: i32, + y: i32, + z: i32, }); export const Polygon = object({ - vertices: tupleOf(Vertex, 3), + vertices: tupleOf(Vertex, 3), }); export const Mesh = object({ - faces: arrayOf(Polygon), + faces: arrayOf(Polygon), }); // Helpful for the top-most level element diff --git a/examples/genericEnumTypes/index.ts b/examples/genericEnumTypes/index.ts index 80aa6a3..05fd949 100644 --- a/examples/genericEnumTypes/index.ts +++ b/examples/genericEnumTypes/index.ts @@ -2,25 +2,38 @@ // Run with `npm run example:genericEnumTypes` // -import { BufferWriter, BufferReader, INT, STRING, BOOL, genericEnum, object } from 'typed-binary'; +import { + BufferWriter, + BufferReader, + i32, + string, + bool, + genericEnum, + object, +} from 'typed-binary'; enum AnimalType { - DOG = 0, - CAT = 1, -}; + 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, +const Animal = genericEnum( + { + nickname: string, + age: i32, + }, + { + [AnimalType.DOG]: object({ + // Animal can be a dog + breed: string, }), - [AnimalType.CAT]: object({ // Animal can be a cat - striped: BOOL, + [AnimalType.CAT]: object({ + // Animal can be a cat + striped: bool, }), -}); + }, +); // A buffer to serialize into/out of const buffer = Buffer.alloc(16); @@ -29,14 +42,14 @@ const reader = new BufferReader(buffer); // Writing an Animal Animal.write(writer, { - type: AnimalType.CAT, // We're specyfing which concrete type we want this object to be. + type: AnimalType.CAT, // We're specyfing which concrete type we want this object to be. - // Base properties - nickname: 'James', - age: 5, + // Base properties + nickname: 'James', + age: 5, - // Concrete type specific properties - striped: true, + // Concrete type specific properties + striped: true, }); // Deserializing the animal @@ -45,17 +58,16 @@ const animal = Animal.read(reader); // -- 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}`); + // 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}`); } diff --git a/examples/genericTypes/index.ts b/examples/genericTypes/index.ts index 979ba52..b7fd5eb 100644 --- a/examples/genericTypes/index.ts +++ b/examples/genericTypes/index.ts @@ -2,20 +2,33 @@ // Run with `npm run example:genericTypes` // -import { BufferWriter, BufferReader, INT, STRING, BOOL, generic, object } from 'typed-binary'; +import { + BufferWriter, + BufferReader, + i32, + string, + bool, + generic, + object, +} from 'typed-binary'; // Generic object schema -const Animal = generic({ - nickname: STRING, - age: INT, -}, { - 'dog': object({ // Animal can be a dog - breed: STRING, +const Animal = generic( + { + nickname: string, + age: i32, + }, + { + dog: object({ + // Animal can be a dog + breed: string, }), - 'cat': object({ // Animal can be a cat - striped: BOOL, + cat: object({ + // Animal can be a cat + striped: bool, }), -}); + }, +); // A buffer to serialize into/out of const buffer = Buffer.alloc(16); @@ -24,14 +37,14 @@ 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. + type: 'cat', // We're specyfing which concrete type we want this object to be. - // Base properties - nickname: 'James', - age: 5, + // Base properties + nickname: 'James', + age: 5, - // Concrete type specific properties - striped: true, + // Concrete type specific properties + striped: true, }); // Deserializing the animal @@ -42,17 +55,16 @@ 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}`); + // 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}`); } diff --git a/examples/inferredShowcase/index.ts b/examples/inferredShowcase/index.ts index 93cdb9e..31db2e3 100644 --- a/examples/inferredShowcase/index.ts +++ b/examples/inferredShowcase/index.ts @@ -1,11 +1,11 @@ -import { object, STRING, INT, Parsed } from 'typed-binary'; +import { object, string, i32, Parsed } from 'typed-binary'; // Describing the Dog schema. const Dog = object({ - /** The name of the doggy. */ - name: STRING, - /** The dog's age in dog years. */ - age: INT, + /** The name of the doggy. */ + name: string, + /** The dog's age in dog years. */ + age: i32, }); // Creating a type-alias for ease-of-use. @@ -13,6 +13,6 @@ type Dog = Parsed; // Creating a 'Dog' object. const dog: Dog = { - name: 'Sammy', - age: 15, + name: 'Sammy', + age: 15, }; diff --git a/examples/recursiveTypes/index.ts b/examples/recursiveTypes/index.ts index e96117c..312ebf6 100644 --- a/examples/recursiveTypes/index.ts +++ b/examples/recursiveTypes/index.ts @@ -3,77 +3,86 @@ // import { Parsed } from 'typed-binary'; -import { INT, STRING, keyed, generic, object, optional } from 'typed-binary'; +import { i32, string, keyed, generic, object, optional } from 'typed-binary'; type Expression = Parsed; -const Expression = keyed('expression', (Expression) => generic({}, { - 'multiply': object({ +const Expression = keyed('expression', (Expression) => + generic( + {}, + { + multiply: object({ a: Expression, b: Expression, - }), - 'negate': object({ + }), + negate: object({ inner: Expression, - }), - 'int_literal': object({ - value: INT, - }), -})); + }), + int_literal: object({ + value: i32, + }), + }, + ), +); const expr: Parsed = { - type: 'multiply', - a: { - type: 'negate', - inner: { - type: 'int_literal', - value: 15, - } - }, - b: { - type: 'int_literal', - value: 2, + type: 'multiply', + a: { + type: 'negate', + inner: { + type: 'int_literal', + value: 15, }, + }, + b: { + type: 'int_literal', + value: 2, + }, }; type Instruction = Parsed; const Instruction = object({ - target_variable: STRING, - expression: optional(Expression), + target_variable: string, + expression: optional(Expression), }); type Complex = Parsed; -const Complex = keyed('complex' as const, (Complex) => object({ - label: STRING, +const Complex = keyed('complex' as const, (Complex) => + object({ + label: string, inner: optional(Complex), - cycle: keyed('cycle' as const, (Cycle) => object({ - value: STRING, + cycle: keyed('cycle' as const, (Cycle) => + object({ + value: string, next: optional(Cycle), - })), -})); + }), + ), + }), +); const inst: Instruction = { - target_variable: 'firstlevel', - expression: undefined, + target_variable: 'firstlevel', + expression: undefined, }; const complex: Complex = { - label: '1', - inner: { - label: '1->2', - inner: undefined, - cycle: { - value: '1->2: A', - next: undefined, - }, - }, + label: '1', + inner: { + label: '1->2', + inner: undefined, cycle: { - value: '1: B', - next: { - value: '1: B->C', - next: undefined, - }, + value: '1->2: A', + next: undefined, + }, + }, + cycle: { + value: '1: B', + next: { + value: '1: B->C', + next: undefined, }, -} + }, +}; console.log(expr); console.log(inst); -console.log(complex); \ No newline at end of file +console.log(complex); diff --git a/examples/stateMachine/connection.ts b/examples/stateMachine/connection.ts index f731234..649aff6 100644 --- a/examples/stateMachine/connection.ts +++ b/examples/stateMachine/connection.ts @@ -1,24 +1,22 @@ -import { BYTE, FLOAT, INT, object } from 'typed-binary'; +import { byte, f32, i32, object } from 'typed-binary'; import { TriggerCondition } from './triggerCondition'; - -export const ConnectionTemplate = - object({ - targetNodeIndex: INT, - /** - * The duration of the transition in Minecraft ticks - */ - transitionDuration: FLOAT, - transitionEasing: BYTE, - triggerCondition: TriggerCondition, - }); +export const ConnectionTemplate = object({ + targetNodeIndex: i32, + /** + * The duration of the transition in Minecraft ticks + */ + transitionDuration: f32, + transitionEasing: byte, + triggerCondition: TriggerCondition, +}); export enum Easing { - LINEAR = 0, - EASE_IN_QUAD = 1, - EASE_OUT_QUAD = 2, - EASE_IN_OUT_QUAD = 3, - EASE_IN_CUBIC = 4, - EASE_OUT_CUBIC = 5, - EASE_IN_OUT_SINE = 6, -} \ No newline at end of file + LINEAR = 0, + EASE_IN_QUAD = 1, + EASE_OUT_QUAD = 2, + EASE_IN_OUT_QUAD = 3, + EASE_IN_CUBIC = 4, + EASE_OUT_CUBIC = 5, + EASE_IN_OUT_SINE = 6, +} diff --git a/examples/stateMachine/graph.ts b/examples/stateMachine/graph.ts index fc9049b..299470d 100644 --- a/examples/stateMachine/graph.ts +++ b/examples/stateMachine/graph.ts @@ -1,9 +1,7 @@ -import { INT, arrayOf, object } from 'typed-binary'; +import { i32, arrayOf, object } from 'typed-binary'; import { NodeTemplate } from './node'; - -export const Graph = - object({ - entryNode: INT, - nodes: arrayOf(NodeTemplate), - }); +export const Graph = object({ + entryNode: i32, + nodes: arrayOf(NodeTemplate), +}); diff --git a/examples/stateMachine/node.ts b/examples/stateMachine/node.ts index 8dd6024..4dabafe 100644 --- a/examples/stateMachine/node.ts +++ b/examples/stateMachine/node.ts @@ -1,12 +1,10 @@ -import { BOOL, FLOAT, INT, STRING, arrayOf, object } from 'typed-binary'; +import { bool, f32, i32, string, arrayOf, object } from 'typed-binary'; import { ConnectionTemplate } from './connection'; - -export const NodeTemplate = - object({ - animationKey: STRING, - startFrame: INT, - playbackSpeed: FLOAT, - looping: BOOL, - connections: arrayOf(ConnectionTemplate), - }); +export const NodeTemplate = object({ + animationKey: string, + startFrame: i32, + playbackSpeed: f32, + looping: bool, + connections: arrayOf(ConnectionTemplate), +}); diff --git a/examples/stateMachine/triggerCondition.ts b/examples/stateMachine/triggerCondition.ts index 929c7e6..59c3eec 100644 --- a/examples/stateMachine/triggerCondition.ts +++ b/examples/stateMachine/triggerCondition.ts @@ -1,23 +1,25 @@ -import { BYTE, generic, keyed, object } from 'typed-binary'; +import { byte, generic, keyed, object } from 'typed-binary'; import type { Parsed } from 'typed-binary'; - type TriggerCondition = Parsed; -export const TriggerCondition = keyed('trigger-condition', (TriggerCondition) => generic({}, { - 'core:state': object({ - state: BYTE, - }), - 'core:animation_finished': object({}), - 'core:not': object({ +export const TriggerCondition = keyed('trigger-condition', (TriggerCondition) => + generic( + {}, + { + 'core:state': object({ + state: byte, + }), + 'core:animation_finished': object({}), + 'core:not': object({ condition: TriggerCondition, - }), -})); - + }), + }, + ), +); export enum MobState { - ON_GROUND = 0, - AIRBORNE = 1, - STANDING_STILL = 2, - MOVING_HORIZONTALLY = 3, + ON_GROUND = 0, + AIRBORNE = 1, + STANDING_STILL = 2, + MOVING_HORIZONTALLY = 3, } - diff --git a/package-lock.json b/package-lock.json index 38d66df..b2625d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "typed-binary", - "version": "1.2.0", + "version": "1.2.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "typed-binary", - "version": "1.2.0", + "version": "1.2.2", "license": "MIT", "devDependencies": { "@rollup/plugin-commonjs": "^21.0.1", diff --git a/src/describe/index.ts b/src/describe/index.ts index 2be1706..e700700 100644 --- a/src/describe/index.ts +++ b/src/describe/index.ts @@ -3,38 +3,55 @@ import { ArraySchema } from '../structure/array'; import { OptionalSchema } from '../structure/optional'; import { GenericObjectSchema } from '../structure/object'; import { TupleSchema } from '../structure/tuple'; -import { ISchema, ISchemaWithProperties, IStableSchema, Ref, SchemaMap } from '../structure/types'; +import { + ISchema, + ISchemaWithProperties, + IStableSchema, + Ref, + SchemaMap, +} from '../structure/types'; import { KeyedSchema } from '../structure/keyed'; - -export const chars = (length: T) => - new CharsSchema(length); - -export const object =

= Record>(properties: SchemaMap

) => - new ObjectSchema(properties); - -export const generic =

= Record, S extends {[Key in keyof S]: ISchemaWithProperties>} = Record>(properties: SchemaMap

, subTypeMap: S) => - new GenericObjectSchema( - SubTypeKey.STRING, - properties, - subTypeMap - ); - -export const genericEnum =

, S extends {[Key in keyof S]: ISchemaWithProperties>}>(properties: SchemaMap

, subTypeMap: S) => - new GenericObjectSchema( - SubTypeKey.ENUM, - properties, - subTypeMap - ); +export const chars = (length: T) => new CharsSchema(length); + +export const object = < + P extends Record = Record, +>( + properties: SchemaMap

, +) => new ObjectSchema(properties); + +export const generic = < + P extends Record = Record, + S extends { + [Key in keyof S]: ISchemaWithProperties>; + } = Record, +>( + properties: SchemaMap

, + subTypeMap: S, +) => new GenericObjectSchema(SubTypeKey.STRING, properties, subTypeMap); + +export const genericEnum = < + P extends Record, + S extends { + [Key in keyof S]: ISchemaWithProperties>; + }, +>( + properties: SchemaMap

, + subTypeMap: S, +) => new GenericObjectSchema(SubTypeKey.ENUM, properties, subTypeMap); export const arrayOf = >(elementType: T) => - new ArraySchema(elementType); + new ArraySchema(elementType); -export const tupleOf = >(elementType: T, length: number) => - new TupleSchema(elementType, length); +export const tupleOf = >( + elementType: T, + length: number, +) => new TupleSchema(elementType, length); export const optional = (innerType: ISchema) => - new OptionalSchema(innerType); + new OptionalSchema(innerType); -export const keyed = >(key: K, inner: (ref: ISchema>) => P) => - new KeyedSchema(key, inner); +export const keyed = >( + key: K, + inner: (ref: ISchema>) => P, +) => new KeyedSchema(key, inner); diff --git a/src/io/bufferReaderWriter.test.ts b/src/io/bufferReaderWriter.test.ts index da2dbd6..96db638 100644 --- a/src/io/bufferReaderWriter.test.ts +++ b/src/io/bufferReaderWriter.test.ts @@ -5,59 +5,59 @@ import { randBetween, randIntBetween } from '../test/random'; const expect = chai.expect; describe('BufferWriter/BufferReader', () => { - it('should encode and decode int sequence', () => { - // Generating random int sequence - const intList = []; - for (let i = 0; i < randIntBetween(1, 10); ++i) { - intList.push(randIntBetween(-100, 100)); - } - - // Creating appropriate buffer - const buffer = Buffer.alloc(intList.length * 4); - - // Writer - const writer = new BufferWriter(buffer); - - // Writing the ints - for (const int of intList) { - writer.writeInt(int); - } - - // Expecting specific buffer offset - expect(writer.currentByteOffset).to.equal(intList.length * 4); - - // Reading the ints - const reader = new BufferReader(buffer); - for (let i = 0; i < intList.length; ++i) { - expect(reader.readInt()).to.equal(intList[i]); - } - }); - - it('should encode and decode float sequence', () => { - // Generating random int sequence - const floatList = []; - for (let i = 0; i < randIntBetween(1, 10); ++i) { - floatList.push(randBetween(-100, 100)); - } - - // Creating appropriate buffer - const buffer = Buffer.alloc(floatList.length * 4); - - // Writer - const writer = new BufferWriter(buffer); - - // Writing the ints - for (const float of floatList) { - writer.writeFloat(float); - } - - // Expecting specific buffer offset - expect(writer.currentByteOffset).to.equal(floatList.length * 4); - - // Reading the ints - const reader = new BufferReader(buffer); - for (let i = 0; i < floatList.length; ++i) { - expect(reader.readFloat()).to.be.closeTo(floatList[i], 0.001); - } - }); -}); \ No newline at end of file + it('should encode and decode int sequence', () => { + // Generating random int sequence + const intList = []; + for (let i = 0; i < randIntBetween(1, 10); ++i) { + intList.push(randIntBetween(-100, 100)); + } + + // Creating appropriate buffer + const buffer = Buffer.alloc(intList.length * 4); + + // Writer + const writer = new BufferWriter(buffer); + + // Writing the ints + for (const int of intList) { + writer.writeInt(int); + } + + // Expecting specific buffer offset + expect(writer.currentByteOffset).to.equal(intList.length * 4); + + // Reading the ints + const reader = new BufferReader(buffer); + for (let i = 0; i < intList.length; ++i) { + expect(reader.readInt()).to.equal(intList[i]); + } + }); + + it('should encode and decode f32 sequence', () => { + // Generating random int sequence + const floatList = []; + for (let i = 0; i < randIntBetween(1, 10); ++i) { + floatList.push(randBetween(-100, 100)); + } + + // Creating appropriate buffer + const buffer = Buffer.alloc(floatList.length * 4); + + // Writer + const writer = new BufferWriter(buffer); + + // Writing the ints + for (const float of floatList) { + writer.writeFloat(float); + } + + // Expecting specific buffer offset + expect(writer.currentByteOffset).to.equal(floatList.length * 4); + + // Reading the ints + const reader = new BufferReader(buffer); + for (let i = 0; i < floatList.length; ++i) { + expect(reader.readFloat()).to.be.closeTo(floatList[i], 0.001); + } + }); +}); diff --git a/src/io/types.ts b/src/io/types.ts index 39f7460..1bca29c 100644 --- a/src/io/types.ts +++ b/src/io/types.ts @@ -1,17 +1,17 @@ export interface ISerialInput { - readBool(): boolean; - readByte(): number; - readInt(): number; - readFloat(): number; - readString(): string; - readonly currentByteOffset: number; + readBool(): boolean; + readByte(): number; + readInt(): number; + readFloat(): number; + readString(): string; + readonly currentByteOffset: number; } export interface ISerialOutput { - writeBool(value: boolean): void; - writeByte(value: number): void; - writeInt(value: number): void; - writeFloat(value: number): void; - writeString(value: string): void; - readonly currentByteOffset: number; -} \ No newline at end of file + writeBool(value: boolean): void; + writeByte(value: number): void; + writeInt(value: number): void; + writeFloat(value: number): void; + writeString(value: string): void; + readonly currentByteOffset: number; +} diff --git a/src/parsed.test.ts b/src/parsed.test.ts index 40720ff..c0d33e2 100644 --- a/src/parsed.test.ts +++ b/src/parsed.test.ts @@ -1,43 +1,52 @@ // This file doesn't actually need to be run by a test-runner // It's mostly just to check if structure descriptions can be properly parsed. -import { generic, arrayOf, INT, STRING, BOOL, object, Parsed, genericEnum } from '.'; +import { + generic, + arrayOf, + i32, + string, + bool, + object, + Parsed, + genericEnum, +} from '.'; const enum ExpressionType { - ADD = 0, - NEGATE = 1, + ADD = 0, + NEGATE = 1, } -export const Expression = - genericEnum({}, () => ({ - [ExpressionType.ADD]: object({ - leftHandSizeId: INT, - rightHandSizeId: INT, - }), - [ExpressionType.NEGATE]: object({ - innerExpressionId: INT, - }), - })); +export const Expression = genericEnum({}, () => ({ + [ExpressionType.ADD]: object({ + leftHandSizeId: i32, + rightHandSizeId: i32, + }), + [ExpressionType.NEGATE]: object({ + innerExpressionId: i32, + }), +})); type Expression = Parsed; // const expr = {} as Expression; - -export const KeyframeNodeTemplate = - generic({ - connections: arrayOf(INT), - }, { - 'core:standard': object({ - animationKey: STRING, - startFrame: INT, - playbackSpeed: INT, - looping: BOOL, - }), - 'core:movement': object({ - animationKey: STRING, - startFrame: INT, - playbackSpeed: INT, - }), - }); +export const KeyframeNodeTemplate = generic( + { + connections: arrayOf(i32), + }, + { + 'core:standard': object({ + animationKey: string, + startFrame: i32, + playbackSpeed: i32, + looping: bool, + }), + 'core:movement': object({ + animationKey: string, + startFrame: i32, + playbackSpeed: i32, + }), + }, +); type KeyframeNodeTemplate = Parsed; -// const s = {} as KeyframeNodeTemplate; \ No newline at end of file +// const s = {} as KeyframeNodeTemplate; diff --git a/src/structure/array.ts b/src/structure/array.ts index 0a278db..7f4dfa7 100644 --- a/src/structure/array.ts +++ b/src/structure/array.ts @@ -1,49 +1,50 @@ import type { ISerialInput, ISerialOutput } from '../io'; -import { INT } from './baseTypes'; +import { i32 } from './baseTypes'; import { IRefResolver, ISchema, IStableSchema, Schema } from './types'; - export class ArraySchema extends Schema { - public elementType: IStableSchema + public elementType: IStableSchema; - constructor(private readonly _unstableElementType: ISchema) { - super(); + constructor(private readonly _unstableElementType: ISchema) { + super(); - // In case this array isn't part of a keyed chain, - // let's assume the inner type is stable. - this.elementType = _unstableElementType as IStableSchema; - } + // In case this array isn't part of a keyed chain, + // let's assume the inner type is stable. + this.elementType = _unstableElementType as IStableSchema; + } - resolve(ctx: IRefResolver): void { - this.elementType = ctx.resolve(this._unstableElementType); - } + resolve(ctx: IRefResolver): void { + this.elementType = ctx.resolve(this._unstableElementType); + } - write(output: ISerialOutput, values: T[]): void { - output.writeInt(values.length); + write(output: ISerialOutput, values: T[]): void { + output.writeInt(values.length); - for (const value of values) { - this.elementType.write(output, value); - } + for (const value of values) { + this.elementType.write(output, value); } + } - read(input: ISerialInput): T[] { - const array = []; + read(input: ISerialInput): T[] { + const array = []; - const len = input.readInt(); - - for (let i = 0; i < len; ++i) { - array.push(this.elementType.read(input)); - } - - return array; - } + const len = input.readInt(); - sizeOf(values: T[]): number { - // Length encoding - let size = INT.sizeOf(); - // Values encoding - size += values.map((v) => this.elementType.sizeOf(v)).reduce((a, b) => a + b, 0); - - return size; + for (let i = 0; i < len; ++i) { + array.push(this.elementType.read(input)); } + + return array; + } + + sizeOf(values: T[]): number { + // Length encoding + let size = i32.sizeOf(); + // Values encoding + size += values + .map((v) => this.elementType.sizeOf(v)) + .reduce((a, b) => a + b, 0); + + return size; + } } diff --git a/src/structure/baseTypes.ts b/src/structure/baseTypes.ts index 126414a..a660285 100644 --- a/src/structure/baseTypes.ts +++ b/src/structure/baseTypes.ts @@ -6,107 +6,117 @@ import { Schema } from './types'; //// export class BoolSchema extends Schema { - resolve(): void { /* Nothing to resolve */ } + resolve(): void { + /* Nothing to resolve */ + } - read(input: ISerialInput): boolean { - return input.readBool(); - } + read(input: ISerialInput): boolean { + return input.readBool(); + } - write(output: ISerialOutput, value: boolean): void { - output.writeBool(value); - } + write(output: ISerialOutput, value: boolean): void { + output.writeBool(value); + } - sizeOf(): number { - return 1; - } + sizeOf(): number { + return 1; + } } -export const BOOL = new BoolSchema(); +export const bool = new BoolSchema(); //// // STRING //// export class StringSchema extends Schema { - resolve(): void { /* Nothing to resolve */ } + resolve(): void { + /* Nothing to resolve */ + } - read(input: ISerialInput): string { - return input.readString(); - } + read(input: ISerialInput): string { + return input.readString(); + } - write(output: ISerialOutput, value: T): void { - output.writeString(value); - } + write(output: ISerialOutput, value: T): void { + output.writeString(value); + } - sizeOf(value: T): number { - return value.length + 1; - } + sizeOf(value: T): number { + return value.length + 1; + } } -export const STRING = new StringSchema(); +export const string = new StringSchema(); //// // BYTE //// export class ByteSchema extends Schema { - resolve(): void { /* Nothing to resolve */ } + resolve(): void { + /* Nothing to resolve */ + } - read(input: ISerialInput): number { - return input.readByte(); - } + read(input: ISerialInput): number { + return input.readByte(); + } - write(output: ISerialOutput, value: number): void { - output.writeByte(value); - } + write(output: ISerialOutput, value: number): void { + output.writeByte(value); + } - sizeOf(): number { - return 1; - } + sizeOf(): number { + return 1; + } } -export const BYTE = new ByteSchema(); +export const byte = new ByteSchema(); //// -// INT +// i32 //// export class IntSchema extends Schema { - resolve(): void { /* Nothing to resolve */ } + resolve(): void { + /* Nothing to resolve */ + } - read(input: ISerialInput): number { - return input.readInt(); - } + read(input: ISerialInput): number { + return input.readInt(); + } - write(output: ISerialOutput, value: number): void { - output.writeInt(value); - } + write(output: ISerialOutput, value: number): void { + output.writeInt(value); + } - sizeOf(): number { - return 4; - } + sizeOf(): number { + return 4; + } } -export const INT = new IntSchema(); +export const i32 = new IntSchema(); //// // FLOAT //// export class FloatSchema extends Schema { - resolve(): void { /* Nothing to resolve */ } + resolve(): void { + /* Nothing to resolve */ + } - read(input: ISerialInput): number { - return input.readFloat(); - } + read(input: ISerialInput): number { + return input.readFloat(); + } - write(output: ISerialOutput, value: number): void { - output.writeFloat(value); - } + write(output: ISerialOutput, value: number): void { + output.writeFloat(value); + } - sizeOf(): number { - return 4; - } + sizeOf(): number { + return 4; + } } -export const FLOAT = new FloatSchema(); \ No newline at end of file +export const f32 = new FloatSchema(); diff --git a/src/structure/index.ts b/src/structure/index.ts index 9830d80..54b0c5b 100644 --- a/src/structure/index.ts +++ b/src/structure/index.ts @@ -1,20 +1,19 @@ export { - BOOL, - BYTE, - INT, - FLOAT, - STRING, - - Ref, - IRefResolver, - Schema, - ISchemaWithProperties, - Keyed, - KeyedSchema, - CharsSchema, - ArraySchema, - ObjectSchema, - GenericObjectSchema, - OptionalSchema, - SubTypeKey, -} from './_internal'; + bool, + byte, + i32, + f32, + string, + Ref, + IRefResolver, + Schema, + ISchemaWithProperties, + Keyed, + KeyedSchema, + CharsSchema, + ArraySchema, + ObjectSchema, + GenericObjectSchema, + OptionalSchema, + SubTypeKey, +} from "./_internal"; diff --git a/src/structure/object.ts b/src/structure/object.ts index fd66620..b4d796a 100644 --- a/src/structure/object.ts +++ b/src/structure/object.ts @@ -1,161 +1,211 @@ import type { ISerialInput, ISerialOutput } from '../io'; -import { STRING } from './baseTypes'; +import { string } from './baseTypes'; import { - Schema, IRefResolver, ISchemaWithProperties, SchemaMap, StableSchemaMap + Schema, + IRefResolver, + ISchemaWithProperties, + SchemaMap, + StableSchemaMap, } from './types'; import { SubTypeKey } from './types'; -export function exactEntries>(record: T): [keyof T, T[keyof T]][] { - return Object.entries(record) as [keyof T, T[keyof T]][]; +export function exactEntries>( + record: T, +): [keyof T, T[keyof T]][] { + return Object.entries(record) as [keyof T, T[keyof T]][]; } -export function resolveMap>}>(ctx: IRefResolver, refs: T): StabilizedMap; -export function resolveMap(ctx: IRefResolver, refs: SchemaMap): StableSchemaMap; -export function resolveMap(ctx: IRefResolver, refs: SchemaMap): StableSchemaMap { - const props = {} as StableSchemaMap; - - for (const [key, ref] of exactEntries(refs)) { - props[key] = ctx.resolve(ref); - } - - return props; +export function resolveMap< + T extends { [K in keyof T]: ISchemaWithProperties> }, +>(ctx: IRefResolver, refs: T): StabilizedMap; +export function resolveMap( + ctx: IRefResolver, + refs: SchemaMap, +): StableSchemaMap; +export function resolveMap( + ctx: IRefResolver, + refs: SchemaMap, +): StableSchemaMap { + const props = {} as StableSchemaMap; + + for (const [key, ref] of exactEntries(refs)) { + props[key] = ctx.resolve(ref); + } + + return props; } -export type StableObjectSchemaMap>> = {[key in keyof T]: ObjectSchema}; +export type StableObjectSchemaMap< + T extends Record>, +> = { [key in keyof T]: ObjectSchema }; -export class ObjectSchema extends Schema implements ISchemaWithProperties { - public properties: StableSchemaMap; +export class ObjectSchema + extends Schema + implements ISchemaWithProperties +{ + public properties: StableSchemaMap; - constructor(private readonly _properties: SchemaMap) { - super(); + constructor(private readonly _properties: SchemaMap) { + super(); - // In case this object isn't part of a keyed chain, - // let's assume properties are stable. - this.properties = _properties as StableSchemaMap; - } + // In case this object isn't part of a keyed chain, + // let's assume properties are stable. + this.properties = _properties as StableSchemaMap; + } - resolve(ctx: IRefResolver): void { - this.properties = resolveMap(ctx, this._properties); - } + resolve(ctx: IRefResolver): void { + this.properties = resolveMap(ctx, this._properties); + } - write(output: ISerialOutput, value: I): void { - for (const [key, property] of exactEntries(this.properties)) { - property.write(output, value[key]); - } + write(output: ISerialOutput, value: I): void { + for (const [key, property] of exactEntries(this.properties)) { + property.write(output, value[key]); } + } - read(input: ISerialInput): T { - const result = {} as T; + read(input: ISerialInput): T { + const result = {} as T; - for (const [key, property] of exactEntries(this.properties)) { - result[key] = property.read(input); - } - - return result; + for (const [key, property] of exactEntries(this.properties)) { + result[key] = property.read(input); } - sizeOf(value: I): number { - return exactEntries(this.properties) - .map(([key, property]) => property.sizeOf(value[key])) // Mapping properties into their sizes. - .reduce((a, b) => a + b, 0); // Summing them up - } -} + return result; + } -export type AsSubTypes = ({[K in keyof T]: T[K] extends ISchemaWithProperties ? P & { type: K } : never})[keyof T]; -export type StabilizedMap = ({[K in keyof T]: T[K] extends ISchemaWithProperties ? ObjectSchema

: never}); + sizeOf(value: I): number { + return exactEntries(this.properties) + .map(([key, property]) => property.sizeOf(value[key])) // Mapping properties into their sizes. + .reduce((a, b) => a + b, 0); // Summing them up + } +} -type GenericInfered = T extends Record ? AsSubTypes : T & AsSubTypes; +export type AsSubTypes = { + [K in keyof T]: T[K] extends ISchemaWithProperties + ? P & { type: K } + : never; +}[keyof T]; +export type StabilizedMap = { + [K in keyof T]: T[K] extends ISchemaWithProperties + ? ObjectSchema

+ : never; +}; + +type GenericInfered = T extends Record + ? AsSubTypes + : T & AsSubTypes; export class GenericObjectSchema< - T extends Record, // Base properties - E extends {[key in keyof E]: ISchemaWithProperties>}, // Sub type map + T extends Record, // Base properties + E extends { + [key in keyof E]: ISchemaWithProperties>; + }, // Sub type map > extends Schema> { - private _baseObject: ObjectSchema; - public subTypeMap: StabilizedMap; - - constructor( - public readonly keyedBy: SubTypeKey, - properties: SchemaMap, - private readonly _subTypeMap: E - ) { - super(); - - this._baseObject = new ObjectSchema(properties); - - // In case this object isn't part of a keyed chain, - // let's assume sub types are stable. - this.subTypeMap = _subTypeMap as unknown as typeof this.subTypeMap; + private _baseObject: ObjectSchema; + public subTypeMap: StabilizedMap; + + constructor( + public readonly keyedBy: SubTypeKey, + properties: SchemaMap, + private readonly _subTypeMap: E, + ) { + super(); + + this._baseObject = new ObjectSchema(properties); + + // In case this object isn't part of a keyed chain, + // let's assume sub types are stable. + this.subTypeMap = _subTypeMap as unknown as typeof this.subTypeMap; + } + + resolve(ctx: IRefResolver): void { + this._baseObject.resolve(ctx); + this.subTypeMap = resolveMap(ctx, this._subTypeMap); + } + + write(output: ISerialOutput, value: GenericInfered): void { + // Figuring out sub-types + + const subTypeDescription = this.subTypeMap[value.type] || null; + if (subTypeDescription === null) { + throw new Error( + `Unknown sub-type '${value.type.toString()}' in among '${JSON.stringify( + Object.keys(this.subTypeMap), + )}'`, + ); } - resolve(ctx: IRefResolver): void { - this._baseObject.resolve(ctx); - this.subTypeMap = resolveMap(ctx, this._subTypeMap); + // Writing the sub-type out. + if (this.keyedBy === SubTypeKey.ENUM) { + output.writeByte(value.type as number); + } else { + output.writeString(value.type as string); } - write(output: ISerialOutput, value: GenericInfered): void { - // Figuring out sub-types - - const subTypeDescription = this.subTypeMap[value.type] || null; - if (subTypeDescription === null) { - throw new Error(`Unknown sub-type '${value.type.toString()}' in among '${JSON.stringify(Object.keys(this.subTypeMap))}'`); - } - - // Writing the sub-type out. - if (this.keyedBy === SubTypeKey.ENUM) { - output.writeByte(value.type as number); - } - else { - output.writeString(value.type as string); - } - - // Writing the base properties - this._baseObject.write(output, value as T); - - // Extra sub-type fields - for (const [key, extraProp] of exactEntries(subTypeDescription.properties)) { - extraProp.write(output, value[key]); - } - } - - read(input: ISerialInput): GenericInfered { - const subTypeKey = this.keyedBy === SubTypeKey.ENUM ? input.readByte() : input.readString(); - - const subTypeDescription = this.subTypeMap[subTypeKey as keyof E] || null; - if (subTypeDescription === null) { - throw new Error(`Unknown sub-type '${subTypeKey}' in among '${JSON.stringify(Object.keys(this.subTypeMap))}'`); - } + // Writing the base properties + this._baseObject.write(output, value as T); - const result = this._baseObject.read(input) as GenericInfered; + // Extra sub-type fields + for (const [key, extraProp] of exactEntries( + subTypeDescription.properties, + )) { + extraProp.write(output, value[key]); + } + } + + read(input: ISerialInput): GenericInfered { + const subTypeKey = + this.keyedBy === SubTypeKey.ENUM ? input.readByte() : input.readString(); + + const subTypeDescription = this.subTypeMap[subTypeKey as keyof E] || null; + if (subTypeDescription === null) { + throw new Error( + `Unknown sub-type '${subTypeKey}' in among '${JSON.stringify( + Object.keys(this.subTypeMap), + )}'`, + ); + } - // Making the sub type key available to the result object. - result.type = subTypeKey as keyof E; + const result = this._baseObject.read(input) as GenericInfered; - if (subTypeDescription !== null) { - for (const [key, extraProp] of exactEntries(subTypeDescription.properties)) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - (result as any)[key] = extraProp.read(input); - } - } + // Making the sub type key available to the result object. + result.type = subTypeKey as keyof E; - return result; + if (subTypeDescription !== null) { + for (const [key, extraProp] of exactEntries( + subTypeDescription.properties, + )) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + (result as any)[key] = extraProp.read(input); + } } - sizeOf(value: GenericInfered): number { - let size = this._baseObject.sizeOf(value as T); - - // We're a generic object trying to encode a concrete value. - size += this.keyedBy === SubTypeKey.ENUM ? 1 : STRING.sizeOf(value.type as string); + return result; + } + + sizeOf(value: GenericInfered): number { + let size = this._baseObject.sizeOf(value as T); + + // We're a generic object trying to encode a concrete value. + size += + this.keyedBy === SubTypeKey.ENUM + ? 1 + : string.sizeOf(value.type as string); + + // Extra sub-type fields + const subTypeDescription = this.subTypeMap[value.type] || null; + if (subTypeDescription === null) { + throw new Error( + `Unknown sub-type '${value.type.toString()}' in among '${JSON.stringify( + Object.keys(this.subTypeMap), + )}'`, + ); + } - // Extra sub-type fields - const subTypeDescription = this.subTypeMap[value.type] || null; - if (subTypeDescription === null) { - throw new Error(`Unknown sub-type '${value.type.toString()}' in among '${JSON.stringify(Object.keys(this.subTypeMap))}'`); - } + size += exactEntries(subTypeDescription.properties) // Going through extra property keys + .map(([key, prop]) => prop.sizeOf(value[key])) // Mapping extra properties into their sizes + .reduce((a, b) => a + b, 0); // Summing them up - size += exactEntries(subTypeDescription.properties) // Going through extra property keys - .map(([key, prop]) => prop.sizeOf(value[key])) // Mapping extra properties into their sizes - .reduce((a, b) => a + b, 0); // Summing them up - - return size; - } + return size; + } } diff --git a/src/test/array.test.ts b/src/test/array.test.ts index a2d7594..76b8e1b 100644 --- a/src/test/array.test.ts +++ b/src/test/array.test.ts @@ -1,35 +1,37 @@ import * as chai from 'chai'; import { randIntBetween } from './random'; import { makeIO } from './_mock.test'; -import { ArraySchema, INT } from '../structure'; +import { ArraySchema, i32 } from '../structure'; import { arrayOf } from '..'; const expect = chai.expect; -describe('ArrayScheme', () => { - it('should estimate an int-array encoding size', () => { - const IntArray = arrayOf(INT); +describe('ArraySchema', () => { + it('should estimate an int-array encoding size', () => { + const IntArray = arrayOf(i32); - const length = randIntBetween(0, 200); - const values = []; - for (let i = 0; i < length; ++i) { - values.push(randIntBetween(-10000, 10000)); - } + const length = randIntBetween(0, 200); + const values = []; + for (let i = 0; i < length; ++i) { + values.push(randIntBetween(-10000, 10000)); + } - expect(IntArray.sizeOf(values)).to.equal(INT.sizeOf() + length * INT.sizeOf()); - }); + expect(IntArray.sizeOf(values)).to.equal( + i32.sizeOf() + length * i32.sizeOf(), + ); + }); - it('should encode and decode a simple int array', () => { - const length = randIntBetween(0, 5); - const value = []; - for (let i = 0; i < length; ++i) { - value.push(randIntBetween(-10000, 10000)); - } + it('should encode and decode a simple int array', () => { + const length = randIntBetween(0, 5); + const value = []; + for (let i = 0; i < length; ++i) { + value.push(randIntBetween(-10000, 10000)); + } - const description = new ArraySchema(INT); + const description = new ArraySchema(i32); - const { output, input } = makeIO(length * 4 + 4); // Extra 4 bytes for the length of the array - description.write(output, value); - expect(description.read(input)).to.deep.equal(value); - }); -}); \ No newline at end of file + const { output, input } = makeIO(length * 4 + 4); // Extra 4 bytes for the length of the array + description.write(output, value); + expect(description.read(input)).to.deep.equal(value); + }); +}); diff --git a/src/test/bool.test.ts b/src/test/bool.test.ts index 6b7e55d..7cba9ed 100644 --- a/src/test/bool.test.ts +++ b/src/test/bool.test.ts @@ -1,13 +1,13 @@ import * as chai from 'chai'; -import { BOOL} from '../structure'; +import { bool } from '../structure'; import { encodeAndDecode } from './_mock.test'; const expect = chai.expect; describe('BoolSchema', () => { - it('should encode and decode a bool value', () => { - const value = Math.random() < 0.5; - const decoded = encodeAndDecode(BOOL, value); + it('should encode and decode a bool value', () => { + const value = Math.random() < 0.5; + const decoded = encodeAndDecode(bool, value); - expect(decoded).to.equal(value); - }); -}); \ No newline at end of file + expect(decoded).to.equal(value); + }); +}); diff --git a/src/test/byte.test.ts b/src/test/byte.test.ts index e2456b8..b8fa37b 100644 --- a/src/test/byte.test.ts +++ b/src/test/byte.test.ts @@ -1,14 +1,14 @@ import * as chai from 'chai'; import { randIntBetween } from './random'; -import { BYTE } from '../structure'; +import { byte } from '../structure'; import { encodeAndDecode } from './_mock.test'; const expect = chai.expect; describe('ByteSchema', () => { - it('should encode and decode a byte value', () => { - const value = randIntBetween(0, 256); - const decoded = encodeAndDecode(BYTE, value); + it('should encode and decode a byte value', () => { + const value = randIntBetween(0, 256); + const decoded = encodeAndDecode(byte, value); - expect(decoded).to.equal(value); - }); -}); \ No newline at end of file + expect(decoded).to.equal(value); + }); +}); diff --git a/src/test/float.test.ts b/src/test/float.test.ts index 4f76426..c7d1ff4 100644 --- a/src/test/float.test.ts +++ b/src/test/float.test.ts @@ -1,14 +1,14 @@ import * as chai from 'chai'; import { randBetween } from './random'; -import { FLOAT } from '../structure'; +import { f32 } from '../structure'; import { encodeAndDecode } from './_mock.test'; const expect = chai.expect; describe('FloatSchema', () => { - it('should encode and decode a float value', () => { - const value = randBetween(-100, 100); - const decoded = encodeAndDecode(FLOAT, value); + it('should encode and decode a f32 value', () => { + const value = randBetween(-100, 100); + const decoded = encodeAndDecode(f32, value); - expect(decoded).to.closeTo(value, 0.01); - }); -}); \ No newline at end of file + expect(decoded).to.closeTo(value, 0.01); + }); +}); diff --git a/src/test/int.test.ts b/src/test/int.test.ts index 2c04b64..d2e18c7 100644 --- a/src/test/int.test.ts +++ b/src/test/int.test.ts @@ -1,14 +1,14 @@ import * as chai from 'chai'; import { randIntBetween } from './random'; -import { INT } from '../structure'; +import { i32 } from '../structure'; import { encodeAndDecode } from './_mock.test'; const expect = chai.expect; describe('IntSchema', () => { - it('should encode and decode an int value', () => { - const value = randIntBetween(-100, 100); - const decoded = encodeAndDecode(INT, value); + it('should encode and decode an int value', () => { + const value = randIntBetween(-100, 100); + const decoded = encodeAndDecode(i32, value); - expect(decoded).to.equal(value); - }); -}); \ No newline at end of file + expect(decoded).to.equal(value); + }); +}); diff --git a/src/test/keyed.test.ts b/src/test/keyed.test.ts index 5d90876..4cb6ecf 100644 --- a/src/test/keyed.test.ts +++ b/src/test/keyed.test.ts @@ -1,249 +1,281 @@ import * as chai from 'chai'; import { encodeAndDecode } from './_mock.test'; -import { INT, STRING } from '../structure/baseTypes'; +import { i32, string } from '../structure/baseTypes'; import { keyed, object, generic, genericEnum, optional } from '../describe'; import { Parsed } from '..'; const expect = chai.expect; describe('KeyedSchema', () => { - it('should encode and decode a keyed object, no references', () => { - const Example = keyed('example', () => object({ - value: INT, - label: STRING, - })); - - const value = { - value: 70, - label: 'Banana', - }; - - const decoded = encodeAndDecode(Example, value); - expect(decoded).to.deep.equal(value); - }); - - it('should encode and decode a keyed object, with 0-level-deep references', () => { - const Example = keyed('example', (Example) => object({ - value: INT, - label: STRING, - next: optional(Example), - })); - - const value = { - value: 70, - label: 'Banana', - next: undefined, - }; - - const decoded = encodeAndDecode(Example, value); - expect(decoded).to.deep.equal(value); - }); - - it('should encode and decode a keyed object, with 1-level-deep references', () => { - const Example = keyed('example', (Example) => object({ - value: INT, - label: STRING, + it('should encode and decode a keyed object, no references', () => { + const Example = keyed('example', () => + object({ + value: i32, + label: string, + }), + ); + + const value = { + value: 70, + label: 'Banana', + }; + + const decoded = encodeAndDecode(Example, value); + expect(decoded).to.deep.equal(value); + }); + + it('should encode and decode a keyed object, with 0-level-deep references', () => { + const Example = keyed('example', (Example) => + object({ + value: i32, + label: string, + next: optional(Example), + }), + ); + + const value = { + value: 70, + label: 'Banana', + next: undefined, + }; + + const decoded = encodeAndDecode(Example, value); + expect(decoded).to.deep.equal(value); + }); + + it('should encode and decode a keyed object, with 1-level-deep references', () => { + const Example = keyed('example', (Example) => + object({ + value: i32, + label: string, + next: optional(Example), + }), + ); + + const value: Parsed = { + value: 70, + label: 'Banana', + next: { + value: 20, + label: 'Inner Banana', + next: undefined, + }, + }; + + const decoded = encodeAndDecode(Example, value); + expect(decoded).to.deep.equal(value); + }); + + it('should encode and decode a keyed object, with 2-level-deep references', () => { + const Example = keyed('example', (Example) => + object({ + value: i32, + label: string, + next: optional(Example), + }), + ); + + const value: Parsed = { + value: 70, + label: 'Banana', + next: { + value: 20, + label: 'Inner Banana', + next: { + value: 30, + label: 'Level-2 Banana', + next: undefined, + }, + }, + }; + + const decoded = encodeAndDecode(Example, value); + expect(decoded).to.deep.equal(value); + }); + + it('should encode and decode a keyed object, with an inner keyed-object', () => { + type Example = Parsed; + const Example = keyed('example', (Example) => + object({ + label: string, + next: optional(Example), + tree: keyed('tree', (Tree) => + object({ + value: i32, + child: optional(Tree), + }), + ), + }), + ); + + const value: Example = { + label: 'Banana', + next: { + label: 'Inner Banana', + next: undefined, + tree: { + value: 15, + child: undefined, + }, + }, + tree: { + value: 21, + child: { + value: 23, + child: undefined, + }, + }, + }; + + const decoded = encodeAndDecode(Example, value); + expect(decoded).to.deep.equal(value); + }); + + it('should encode and decode a keyed generic object, no references', () => { + type Example = Parsed; + const Example = keyed('example', () => + generic( + { + label: string, + }, + { + primary: object({ + primaryExtra: i32, + }), + secondary: object({ + secondaryExtra: i32, + }), + }, + ), + ); + + const value: Example = { + label: 'Example Label', + type: 'primary', + primaryExtra: 15, + }; + + const decoded = encodeAndDecode(Example, value); + expect(decoded).to.deep.equal(value); + }); + + it('should encode and decode a keyed generic object, with references', () => { + type Example = Parsed; + const Example = keyed('example', (Example) => + generic( + { + label: string, + }, + { + continuous: object({ next: optional(Example), - })); - - const value: Parsed = { - value: 70, - label: 'Banana', - next: { - value: 20, - label: 'Inner Banana', - next: undefined, - }, - }; - - const decoded = encodeAndDecode(Example, value); - expect(decoded).to.deep.equal(value); - }); - - it('should encode and decode a keyed object, with 2-level-deep references', () => { - const Example = keyed('example', (Example) => object({ - value: INT, - label: STRING, + }), + fork: object({ + left: optional(Example), + right: optional(Example), + }), + }, + ), + ); + + const value: Example = { + label: 'Root', + type: 'continuous', + next: { + label: 'Level 1', + type: 'fork', + left: { + label: 'Level 2-A', + type: 'continuous', + next: undefined, + }, + right: { + label: 'Level 2-B', + type: 'continuous', + next: undefined, + }, + }, + }; + + const decoded = encodeAndDecode(Example, value); + expect(decoded).to.deep.equal(value); + }); + + it('should encode and decode a keyed enum generic object, no base props, with references', () => { + type Example = Parsed; + const Example = keyed('example', (Example) => + genericEnum( + {}, + { + 0: object({ next: optional(Example), - })); - - const value: Parsed = { - value: 70, - label: 'Banana', - next: { - value: 20, - label: 'Inner Banana', - next: { - value: 30, - label: 'Level-2 Banana', - next: undefined, - }, - }, - }; - - const decoded = encodeAndDecode(Example, value); - expect(decoded).to.deep.equal(value); - }); - - it('should encode and decode a keyed object, with an inner keyed-object', () => { - type Example = Parsed; - const Example = keyed('example', (Example) => object({ - label: STRING, + }), + 1: object({ + left: optional(Example), + right: optional(Example), + }), + }, + ), + ); + + const value: Example = { + type: 0, + next: { + type: 1, + left: { + type: 0, + next: undefined, + }, + right: { + type: 0, + next: undefined, + }, + }, + }; + + const decoded = encodeAndDecode(Example, value); + expect(decoded).to.deep.equal(value); + }); + + it('should encode and decode a keyed enum generic object, with references', () => { + type Example = Parsed; + const Example = keyed('example', (Example) => + genericEnum( + { + label: string, + }, + { + 0: object({ next: optional(Example), - tree: keyed('tree', (Tree) => object({ - value: INT, - child: optional(Tree), - })), - })); - - const value: Example = { - label: 'Banana', - next: { - label: 'Inner Banana', - next: undefined, - tree: { - value: 15, - child: undefined, - }, - }, - tree: { - value: 21, - child: { - value: 23, - child: undefined, - }, - }, - }; - - const decoded = encodeAndDecode(Example, value); - expect(decoded).to.deep.equal(value); - }); - - it('should encode and decode a keyed generic object, no references', () => { - type Example = Parsed; - const Example = keyed('example', () => generic({ - label: STRING, - }, { - primary: object({ - primaryExtra: INT, - }), - secondary: object({ - secondaryExtra: INT, - }) - })); - - const value: Example = { - label: 'Example Label', - type: 'primary', - primaryExtra: 15, - }; - - const decoded = encodeAndDecode(Example, value); - expect(decoded).to.deep.equal(value); - }); - - it('should encode and decode a keyed generic object, with references', () => { - type Example = Parsed; - const Example = keyed('example', (Example) => generic({ - label: STRING, - }, { - continuous: object({ - next: optional(Example), - }), - fork: object({ - left: optional(Example), - right: optional(Example), - }) - })); - - const value: Example = { - label: 'Root', - type: 'continuous', - next: { - label: 'Level 1', - type: 'fork', - left: { - label: 'Level 2-A', - type: 'continuous', - next: undefined, - }, - right: { - label: 'Level 2-B', - type: 'continuous', - next: undefined, - }, - }, - }; - - const decoded = encodeAndDecode(Example, value); - expect(decoded).to.deep.equal(value); - }); - - it('should encode and decode a keyed enum generic object, no base props, with references', () => { - type Example = Parsed; - const Example = keyed('example', (Example) => genericEnum({}, { - 0: object({ - next: optional(Example), - }), - 1: object({ - left: optional(Example), - right: optional(Example), - }) - })); - - const value: Example = { - type: 0, - next: { - type: 1, - left: { - type: 0, - next: undefined, - }, - right: { - type: 0, - next: undefined, - }, - }, - }; - - const decoded = encodeAndDecode(Example, value); - expect(decoded).to.deep.equal(value); - }); - - it('should encode and decode a keyed enum generic object, with references', () => { - type Example = Parsed; - const Example = keyed('example', (Example) => genericEnum({ - label: STRING, - }, { - 0: object({ - next: optional(Example), - }), - 1: object({ - left: optional(Example), - right: optional(Example), - }) - })); - - const value: Example = { - label: 'Root', - type: 0, - next: { - label: 'Level 1', - type: 1, - left: { - label: 'Level 2-A', - type: 0, - next: undefined, - }, - right: { - label: 'Level 2-B', - type: 0, - next: undefined, - }, - }, - }; - - const decoded = encodeAndDecode(Example, value); - expect(decoded).to.deep.equal(value); - }); + }), + 1: object({ + left: optional(Example), + right: optional(Example), + }), + }, + ), + ); + + const value: Example = { + label: 'Root', + type: 0, + next: { + label: 'Level 1', + type: 1, + left: { + label: 'Level 2-A', + type: 0, + next: undefined, + }, + right: { + label: 'Level 2-B', + type: 0, + next: undefined, + }, + }, + }; + + const decoded = encodeAndDecode(Example, value); + expect(decoded).to.deep.equal(value); + }); }); diff --git a/src/test/object.test.ts b/src/test/object.test.ts index 78188a4..a41ee38 100644 --- a/src/test/object.test.ts +++ b/src/test/object.test.ts @@ -1,117 +1,122 @@ import * as chai from 'chai'; import { encodeAndDecode, makeIO } from './_mock.test'; -import { INT, STRING } from '../structure/baseTypes'; +import { i32, string } from '../structure/baseTypes'; import { generic, genericEnum, object, optional } from '../describe'; import { Parsed } from '../utilityTypes'; const expect = chai.expect; - describe('ObjectSchema', () => { - it('should encode and decode a simple object', () => { - const description = object({ - value: INT, - label: STRING, - }); - - const value = { - value: 70, - label: 'Banana', - }; - - const { output, input } = makeIO(64); - description.write(output, value); - expect(description.read(input)).to.deep.equal(value); + it('should encode and decode a simple object', () => { + const description = object({ + value: i32, + label: string, }); - it('should treat optional properties as undefined', () => { - const OptionalString = optional(STRING); - const schema = object({ - required: STRING, - optional: OptionalString, - }); - - const valueWithUndefined = { - required: 'Required', - optional: undefined, - }; - - expect(encodeAndDecode(schema, valueWithUndefined)).to.deep.equal(valueWithUndefined); + const value = { + value: 70, + label: 'Banana', + }; + + const { output, input } = makeIO(64); + description.write(output, value); + expect(description.read(input)).to.deep.equal(value); + }); + + it('should treat optional properties as undefined', () => { + const OptionalString = optional(string); + const schema = object({ + required: string, + optional: OptionalString, }); - it('should encode and decode a generic object', () => { - type GenericType = Parsed; - const GenericType = - generic({ - sharedValue: INT, - }, { - 'concrete': object({ - extraValue: INT, - }), - 'other': object({ - notImportant: INT, - }), - }); - - const value: GenericType = { - type: 'concrete' as const, - sharedValue: 100, - extraValue: 10, - }; - - const { output, input } = makeIO(64); - // Writing with the generic description. - GenericType.write(output, value); - // Reading with the generic description. - expect(GenericType.read(input)).to.deep.equal(value); + const valueWithUndefined = { + required: 'Required', + optional: undefined, + }; + + expect(encodeAndDecode(schema, valueWithUndefined)).to.deep.equal( + valueWithUndefined, + ); + }); + + it('should encode and decode a generic object', () => { + type GenericType = Parsed; + const GenericType = generic( + { + sharedValue: i32, + }, + { + concrete: object({ + extraValue: i32, + }), + other: object({ + notImportant: i32, + }), + }, + ); + + const value: GenericType = { + type: 'concrete' as const, + sharedValue: 100, + extraValue: 10, + }; + + const { output, input } = makeIO(64); + // Writing with the generic description. + GenericType.write(output, value); + // Reading with the generic description. + expect(GenericType.read(input)).to.deep.equal(value); + }); + + it('should encode and decode an enum generic object', () => { + type GenericType = Parsed; + const GenericType = genericEnum( + { + sharedValue: i32, + }, + { + 0: object({ + extraValue: i32, + }), + 1: object({ + notImportant: i32, + }), + }, + ); + + const value: GenericType = { + type: 0 as const, + sharedValue: 100, + extraValue: 10, + }; + + const { output, input } = makeIO(64); + // Writing with the generic description. + GenericType.write(output, value); + // Reading with the generic description. + expect(GenericType.read(input)).to.deep.equal(value); + }); + + it('preserves insertion-order of properties', () => { + const schema = object({ + a: i32, + c: i32, + b: i32, }); - it('should encode and decode an enum generic object', () => { - type GenericType = Parsed; - const GenericType = - genericEnum({ - sharedValue: INT, - }, { - 0: object({ - extraValue: INT, - }), - 1: object({ - notImportant: INT, - }), - }); - - const value: GenericType = { - type: 0 as const, - sharedValue: 100, - extraValue: 10, - }; - - const { output, input } = makeIO(64); - // Writing with the generic description. - GenericType.write(output, value); - // Reading with the generic description. - expect(GenericType.read(input)).to.deep.equal(value); - }); + // Purpusefully out-of-order. + const value: Parsed = { + a: 1, + b: 2, + c: 3, + }; - it('preserves insertion-order of properties', () => { - const schema = object({ - a: INT, - c: INT, - b: INT, - }); - - // Purpusefully out-of-order. - const value: Parsed = { - a: 1, - b: 2, - c: 3, - }; - - const { output, input } = makeIO(schema.sizeOf(value)); - schema.write(output, value); - - expect(input.readInt()).to.equal(1); // a - expect(input.readInt()).to.equal(3); // c - expect(input.readInt()).to.equal(2); // b - }); + const { output, input } = makeIO(schema.sizeOf(value)); + schema.write(output, value); + + expect(input.readInt()).to.equal(1); // a + expect(input.readInt()).to.equal(3); // c + expect(input.readInt()).to.equal(2); // b + }); }); diff --git a/src/test/optional.test.ts b/src/test/optional.test.ts index a130a2f..15a1093 100644 --- a/src/test/optional.test.ts +++ b/src/test/optional.test.ts @@ -1,26 +1,26 @@ import * as chai from 'chai'; import { randIntBetween } from './random'; -import { OptionalSchema, INT } from '../structure'; +import { OptionalSchema, i32 } from '../structure'; import { makeIO } from './_mock.test'; const expect = chai.expect; describe('OptionalSchema', () => { - it('should encode and decode an optional int, with a value', () => { - const innerValue = randIntBetween(-10000, 10000); + it('should encode and decode an optional int, with a value', () => { + const innerValue = randIntBetween(-10000, 10000); - const description = new OptionalSchema(INT); + const description = new OptionalSchema(i32); - const { output, input } = makeIO(1 + 4); // Extra 1 byte to hold the boolean value. - description.write(output, innerValue); - expect(description.read(input)).to.equal(innerValue); - }); + const { output, input } = makeIO(1 + 4); // Extra 1 byte to hold the boolean value. + description.write(output, innerValue); + expect(description.read(input)).to.equal(innerValue); + }); - it('should encode and decode a nullable int, with UNDEFINED value', () => { - const description = new OptionalSchema(INT); + it('should encode and decode a nullable int, with UNDEFINED value', () => { + const description = new OptionalSchema(i32); - const { output, input } = makeIO(1 + 4); // Extra 1 byte to hold the boolean value. - description.write(output, undefined); - expect(description.read(input)).to.equal(undefined); - }); -}); \ No newline at end of file + const { output, input } = makeIO(1 + 4); // Extra 1 byte to hold the boolean value. + description.write(output, undefined); + expect(description.read(input)).to.equal(undefined); + }); +}); diff --git a/src/test/string.test.ts b/src/test/string.test.ts index f190fcf..cbe2414 100644 --- a/src/test/string.test.ts +++ b/src/test/string.test.ts @@ -1,26 +1,32 @@ import * as chai from 'chai'; import { randIntBetween } from './random'; -import { STRING, } from '../structure'; +import { string } from '../structure'; import { encodeAndDecode } from './_mock.test'; const expect = chai.expect; describe('StringSchema', () => { - it('should encode and decode an empty string value', () => { - const decoded = encodeAndDecode(STRING, ''); + it('should encode and decode an empty string value', () => { + const decoded = encodeAndDecode(string, ''); - expect(decoded).to.equal(''); - }); + expect(decoded).to.equal(''); + }); - it('should encode and decode an alphanumerical string', () => { - const length = randIntBetween(0, 100); - let value = ''; - const ranges = [['A', 'Z'], ['a', 'z'], ['0', '9']]; - for (let i = 0; i < length; ++i) { - const range = ranges[randIntBetween(0, ranges.length) % ranges.length]; - value += String.fromCharCode(randIntBetween(range[0].charCodeAt(0), range[1].charCodeAt(0))); - } + it('should encode and decode an alphanumerical string', () => { + const length = randIntBetween(0, 100); + let value = ''; + const ranges = [ + ['A', 'Z'], + ['a', 'z'], + ['0', '9'], + ]; + for (let i = 0; i < length; ++i) { + const range = ranges[randIntBetween(0, ranges.length) % ranges.length]; + value += String.fromCharCode( + randIntBetween(range[0].charCodeAt(0), range[1].charCodeAt(0)), + ); + } - const decoded = encodeAndDecode(STRING, value); - expect(decoded).to.equal(value); - }); -}); \ No newline at end of file + const decoded = encodeAndDecode(string, value); + expect(decoded).to.equal(value); + }); +});