Skip to content

Commit b7802d0

Browse files
committed
implementation, tests, readme
1 parent 18c328e commit b7802d0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+5645
-1
lines changed

.gitignore

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
.idea
8+
# testing
9+
/coverage
10+
11+
# production
12+
/build
13+
14+
# misc
15+
.DS_Store
16+
.env.local
17+
.env.development.local
18+
.env.test.local
19+
.env.production.local
20+
21+
npm-debug.log*
22+
yarn-debug.log*
23+
yarn-error.log*
24+

.prettierrc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"tabWidth":2,
3+
"singleQuote":true,
4+
"printWidth":120,
5+
"bracketSpacing":false,
6+
"trailingComma": "es5",
7+
"endOfLine": "auto"
8+
}

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2015 Jenny Louthan
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 249 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,249 @@
1-
"# safe-schema"
1+
# Safe Schema
2+
3+
A dependencies-less solution to **safely** serialize your TypeScript models to and from `ArrayBuffer`s.
4+
5+
## Features
6+
7+
- Uses your TypeScript object definitions as a starting point
8+
- Type Safe throughout
9+
- Broad model support
10+
- Lightning Fast due to AOT
11+
- First class support for Type Lookups and Enums
12+
- Well tested
13+
- No external dependencies
14+
15+
## Install
16+
17+
With npm:
18+
19+
```
20+
$ npm install safe-schema --save
21+
```
22+
23+
With yarn:
24+
25+
```
26+
$ yarn add safe-schema
27+
```
28+
29+
## Basic Usage
30+
31+
### Node
32+
33+
```ts
34+
// Import
35+
import {SafeSchema, SchemaDefiner} from 'safe-schema';
36+
37+
38+
// Define your model
39+
type SimpleMessage = {count: number};
40+
41+
// Safely define your schema BASED on your model
42+
const SimpleMessageSchema: SafeSchema<SimpleMessage> = {count: 'uint8'};
43+
44+
...
45+
46+
// Initialize the AOT code generator (only once)
47+
const generator = SchemaDefiner.generate<SimpleMessage>(SimpleMessageSchema);
48+
49+
// Turn your object into an ArrayBuffer
50+
const buffer = SchemaDefiner.toBuffer({count: 12}, generator);
51+
52+
assert(buffer.byteLength === 1)
53+
...
54+
55+
// Turn your ArrayBuffer back into your object
56+
const result = SchemaDefiner.fromBuffer(buffer, generator);
57+
58+
59+
// Use your 100% type safe object on the other side of the wire
60+
assert(result.count === 12)
61+
62+
```
63+
64+
## How It Works
65+
66+
You define your network schema just as you normally would using TypeScript types, then use `SafeSchema` to generate a runtime version of that schema. You will get full intellisense support when defining `SafeSchema<SimpleMessage>`, allowing you to easily define the DataTypes of your model (for instance `number` as `uint16`), as well as the ability to easily **change your schema** and have TypeScript throw the appropriate errors for missing values at compile time.
67+
68+
Calling `SchemaDefiner.generate<SimpleMessage>(SimpleMessageSchema)` generates JavaScript **at runtime** that is hand built to read and write your model to and from an `ArrayBuffer`. There is no switch case behind the scenes, every model generates unique JavaScript which executes **lightning fast**! Only exactly as many bytes will be sent over the wire as needed.
69+
70+
Take a look at the [kitchenSink](__tests__/kitchenSink.ts), and the other tests for complex and realworld examples.
71+
72+
## Why It's Needed
73+
74+
Every other solution to this problem (protocol buffers, JSON, etc), did not allow me to define my models the way I wanted to, **using TypeScript**. TypeScript's modeling abilities are incredibly feature rich and I did not want to lose any of that functionality just because I needed to serialize my data. That's why I built in first class support for things like Discriminating Unions and Enums, so I can have a richly defined schema with the minimum amount of bytes used.
75+
76+
77+
## API Documentation
78+
79+
- [`numbers`](#numbers)
80+
- [`string`](#string)
81+
- [`boolean`](#boolean)
82+
- [`optional`](#optional)
83+
- [`array`](#array)
84+
- [`type-lookup`](#type-lookup)
85+
- [`enum`](#enum)
86+
- [`bitmask`](#bitmask)
87+
88+
### numbers
89+
<a name="numbers" />
90+
91+
When you define your model to be a number in TypeScript you must tell the Schema what type and how big the number is. This is to save on memory over the wire and to not make assumptions.
92+
93+
Example:
94+
95+
```ts
96+
type SimpleMessage = {count: number};
97+
98+
const SimpleMessageSchema: SafeSchema<SimpleMessage> = {count: 'int32'};
99+
```
100+
101+
TypeScript intellisense will only allow values that are valid. The valid values are:
102+
103+
- `uint8`
104+
- `uint16`
105+
- `uint32`
106+
- `int8`
107+
- `int16`
108+
- `int32`
109+
- `float32`
110+
- `float64`
111+
112+
### string
113+
<a name="string" />
114+
115+
SafeSchema encodes strings into utf16 values, plus one uint16 for its length. It does not currently support strings over 65535 in length.
116+
117+
Example:
118+
119+
```ts
120+
type SimpleMessage = {count: string};
121+
122+
const SimpleMessageSchema: SafeSchema<SimpleMessage> = {count: 'string'};
123+
```
124+
125+
### boolean
126+
<a name="boolean" />
127+
128+
SafeSchema encodes booleans into a single uint8 value
129+
130+
Example:
131+
132+
```ts
133+
type SimpleMessage = {count: boolean};
134+
135+
const SimpleMessageSchema: SafeSchema<SimpleMessage> = {count: 'boolean'};
136+
```
137+
138+
139+
### optional
140+
<a name="optional" />
141+
142+
SafeSchema allows any value to be optionally defined. This will send an extra byte over the wire to denote if the value is there or not. The type must be defined as optional in your model
143+
144+
Example:
145+
146+
```ts
147+
type SimpleMessage = {count?: number};
148+
149+
const SimpleMessageSchema: SafeSchema<SimpleMessage> = {
150+
count: {
151+
flag: 'optional',
152+
element: 'uint8',
153+
},
154+
};
155+
```
156+
157+
### array
158+
<a name="array" />
159+
160+
SafeSchema can encode any type as an array. You must specify the max length of the array, either `array-uint8` or `array-uint16`
161+
162+
Example:
163+
164+
```ts
165+
type SimpleMessage = {count: boolean[]};
166+
const SimpleMessageSchema: SafeSchema<SimpleMessage> = {
167+
count: {
168+
flag: 'array-uint8',
169+
elements: 'boolean',
170+
},
171+
};
172+
```
173+
174+
```ts
175+
type ComplexMessage = {count: {shoes: number}[]};
176+
const ComplexMessageSchema: SafeSchema<ComplexMessage> = {
177+
count: {
178+
flag: 'array-uint8',
179+
elements: {shoes: 'float64'},
180+
},
181+
};
182+
183+
```
184+
185+
### type-lookup
186+
<a name="type-lookup" />
187+
188+
SafeSchema has first class support for type lookups. This is useful for Discriminating Unions in TypeScript.
189+
190+
Example:
191+
192+
```ts
193+
type SimpleMessage = {type: 'run'; duration: number} | {type: 'walk'; speed: number};
194+
195+
const SimpleMessageSchema: SafeSchema<SimpleMessage> = {
196+
flag: 'type-lookup',
197+
elements: {
198+
run: {duration: 'uint8'},
199+
walk: {speed: 'float32'},
200+
},
201+
}
202+
```
203+
204+
### enum
205+
<a name="enum" />
206+
207+
SafeSchema has first class support TypeScript enums through string unions. It will only send a single byte over the wire.
208+
209+
Example:
210+
211+
```ts
212+
type SimpleMessage = {weapon:'sword'|'laser'|'shoe'};
213+
214+
const SimpleMessageSchema: SafeSchema<SimpleMessage> = {
215+
flag: 'enum',
216+
sword: '0',
217+
laser: '1',
218+
shoe: '2',
219+
}
220+
```
221+
222+
### bitmask
223+
<a name="bitmask" />
224+
225+
In rare cases you may want to send a bitmasked value over the wire. You define this as a single object that **only has boolean values**. It will send a single byte over the wire, and be serialized back into the complex object.
226+
227+
Example:
228+
229+
```ts
230+
type BitMaskMessage = {
231+
switcher: {
232+
up: boolean;
233+
down: boolean;
234+
left: boolean;
235+
right: boolean;
236+
};
237+
};
238+
239+
const BitMaskMessageSchema: SafeSchema<BitMaskMessage> = {
240+
switcher: {
241+
flag: 'bitmask',
242+
up: 0,
243+
down: 1,
244+
left: 2,
245+
right: 3,
246+
},
247+
};
248+
249+
```

__tests__/arrayLookup.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import {SafeSchema, SchemaDefiner} from '../src';
2+
3+
type Uint8ArrayMessage = {count: number[]};
4+
const Uint8ArrayMessageSchema: SafeSchema<Uint8ArrayMessage> = {
5+
count: {
6+
flag: 'array-uint8',
7+
elements: 'uint32',
8+
},
9+
};
10+
11+
test('array unit8 test', async () => {
12+
const generator = SchemaDefiner.generate<Uint8ArrayMessage>(Uint8ArrayMessageSchema);
13+
14+
const buffer = SchemaDefiner.toBuffer({count: [12, 24]}, generator);
15+
expect(buffer.byteLength).toEqual(4 * 2 + 1);
16+
17+
const result = SchemaDefiner.fromBuffer(buffer, generator);
18+
expect(result.count).toEqual([12, 24]);
19+
});
20+
21+
type Uint16ArrayMessage = {count: number[]};
22+
const Uint16ArrayMessageSchema: SafeSchema<Uint16ArrayMessage> = {
23+
count: {
24+
flag: 'array-uint16',
25+
elements: 'uint32',
26+
},
27+
};
28+
29+
test('array unit8 test', async () => {
30+
const generator = SchemaDefiner.generate<Uint16ArrayMessage>(Uint16ArrayMessageSchema);
31+
32+
const buffer = SchemaDefiner.toBuffer({count: [12, 24]}, generator);
33+
expect(buffer.byteLength).toEqual(4 * 2 + 2);
34+
35+
const result = SchemaDefiner.fromBuffer(buffer, generator);
36+
expect(result.count).toEqual([12, 24]);
37+
});
38+
39+
type Uint8ArrayObjectMessage = {count: {shoes: boolean; count: number}[]};
40+
const Uint8ArrayObjectMessageSchema: SafeSchema<Uint8ArrayObjectMessage> = {
41+
count: {
42+
flag: 'array-uint8',
43+
elements: {
44+
count: 'uint8',
45+
shoes: 'boolean',
46+
},
47+
},
48+
};
49+
50+
test('array unit8 object test', async () => {
51+
const generator = SchemaDefiner.generate<Uint8ArrayObjectMessage>(Uint8ArrayObjectMessageSchema);
52+
53+
const buffer = SchemaDefiner.toBuffer(
54+
{
55+
count: [
56+
{shoes: true, count: 12},
57+
{shoes: false, count: 34},
58+
],
59+
},
60+
generator
61+
);
62+
expect(buffer.byteLength).toEqual((1 + 1) * 2 + 1);
63+
64+
const result = SchemaDefiner.fromBuffer(buffer, generator);
65+
expect(result.count).toEqual([
66+
{shoes: true, count: 12},
67+
{shoes: false, count: 34},
68+
]);
69+
});

0 commit comments

Comments
 (0)