Skip to content

Commit

Permalink
refactor: rename to subShape (#168)
Browse files Browse the repository at this point in the history
  • Loading branch information
tjjfvi authored Jul 11, 2023
1 parent ee7b8ea commit 4692d7e
Show file tree
Hide file tree
Showing 152 changed files with 1,767 additions and 1,703 deletions.
178 changes: 97 additions & 81 deletions Readme.md
Original file line number Diff line number Diff line change
@@ -1,146 +1,162 @@
# SCALE Codecs for JavaScript and TypeScript
# subShape &nbsp;<sub><sup>composable shapes for cohesive code</sup></sub>

A TypeScript implementation of [SCALE (Simple Concatenated Aggregate Little-Endian) transcoding](https://docs.substrate.io/reference/scale-codec/) (see [Rust implementation here](https://github.com/paritytech/parity-scale-codec)), which emphasizes JS-land representations and e2e type-safety. **Faster than `JSON`, `avsc`, `jsbin` and `protobuf`** ([see benchmarks](https://github.com/paritytech/scale-ts-benchmark)).
> ### *one shape can do them all; one shape defined them*
subShape provides primitives and patterns for crafting composable shapes
featuring cohesive typing, validation, serialization, and reflection.

## Setup

If you're using [Deno](https://deno.land/), simply import via the `deno.land/x` specifier.
### Deno

```ts
import * as $ from "https://deno.land/x/scale/mod.ts"
import * as $ from "https://deno.land/x/subshape/mod.ts"
```

If you're using [Node](https://nodejs.org/), install as follows.
### Node

```
npm install scale-codec
npm install subshape
```

Then import as follows.

```ts
import * as $ from "scale-codec"
import * as $ from "subshape"
```

## Usage
## Demo

1. Import the library
2. Define a codec via the library's functions, whose names correspond to types
3. Utilize the codec you've defined

## Example
### Craft a Composable Shape

```ts
import * as $ from "https://deno.land/x/scale/mod.ts"
import * as $ from "https://deno.land/x/subshape/mod.ts"

const $superhero = $.object(
$.field("pseudonym", $.str),
$.optionalField("secretIdentity", $.str),
$.field("superpowers", $.array($.str)),
)

const valueToEncode = {
pseudonym: "Spider-Man",
secretIdentity: "Peter Parker",
superpowers: ["does whatever a spider can"],
}

const encodedBytes: Uint8Array = $superhero.encode(valueToEncode)
const decodedValue: Superhero = $superhero.decode(encodedBytes)

assertEquals(decodedValue, valueToEncode)
```

To extract the type from a given codec, you can use the `Output` utility type.
### And Get...

#### Typing

```ts
type Superhero = $.Output<typeof $superhero>
// {
// type Superhero = {
// pseudonym: string;
// secretIdentity?: string | undefined;
// superpowers: string[];
// }
```
You can also explicitly type the codec, which will validate that the inferred type aligns with the expected.
#### Validation
```ts
interface Superhero {
pseudonym: string
secretIdentity?: string
superpowers: string[]
const spiderMan = {
pseudonym: "Spider-Man",
secretIdentity: "Peter Parker",
superpowers: ["does whatever a spider can"],
}

const $superhero: Codec<Superhero> = $.object(
$.field("pseudonym", $.str),
$.optionalField("secretIdentity", $.str),
$.field("superpowers", $.array($.str)),
)
$.assert($superhero, spiderMan) // ok!

// @ts-expect-error
// Type 'Codec<{ pseudonym: string; secretIdentity?: string | undefined; }>' is not assignable to type 'Codec<Superhero>'.
// The types returned by 'decode(...)' are incompatible between these types.
// Type '{ pseudonym: string; secretIdentity?: string | undefined; }' is not assignable to type 'Superhero'.
const $plebeianHero: Codec<Superhero> = $.object(
$.field("pseudonym", $.str),
$.optionalField("secretIdentity", $.str),
)
const bob = {
pseudonym: "Bob",
secretIdentity: "Robert",
superpowers: null,
}

$.assert($superhero, bob) // ShapeAssertError: !(value.superpowers instanceof Array)
```

You can also validate a value against a codec using `$.assert` or `$.is`:
#### Serialization

```ts
value // unknown
if ($.is($superhero, value)) {
value // Superhero
}
const encoded = $superhero.encode(spiderMan)
// encoded: Uint8Array

const decoded = $superhero.decode(spiderMan)
// decoded: Superhero

console.log(decoded)
// Prints:
// {
// pseudonym: "Spider-Man",
// secretIdentity: "Peter Parker",
// superpowers: [ "does whatever a spider can" ]
// }
```

#### Reflection

value // unknown
$.assert($superhero, value)
value // Superhero
```ts
$superhero.metadata // Metadata<Superhero>

console.log($superhero)
// Prints:
// $.object(
// $.field("pseudonym", $.str),
// $.optionalField("secretIdentity", $.str),
// $.field("superpowers", $.array($.str))
// )
```

If `$.assert` fails, it will throw a `ScaleAssertError` detailing why the value was invalid.
## Examples

Further examples can be found in the [`examples`](https://github.com/paritytech/scale-ts/tree/main/examples) directory.
Further examples can be found in the
[`examples`](https://github.com/paritytech/scale-ts/tree/main/examples)
directory.

## Codec Naming
## Shape Naming Convention

This library adopts a convention of denoting codecs with a `$``$.foo` for built-in codecs, and `$foo` for user-defined codecs. This makes codecs easily distinguishable from other values, and makes it easier to have codecs in scope with other variables:
This library adopts a convention of denoting shapes with a `$``$.foo` for
built-in shapes, and `$foo` for user-defined shapes. This makes shapes easily
distinguishable from other values, and makes it easier to have shapes in scope
with other variables:

```ts
interface Person { ... }
const $person = $.object(...)
const person = { ... }
```

Here, the type, codec, and a value can all coexist without clashing, without having to resort to wordy workarounds like `personCodec`.
Here, the type, shape, and a value can all coexist without clashing, without
having to resort to wordy workarounds like `personShape`.

The main other library this could possibly clash with is jQuery, and its usage has waned enough that this is not a serious problem.
The main other library this could possibly clash with is jQuery, and its usage
has waned enough that this is not a serious problem.

While we recommend following this convention for consistency, you can, of course, adopt an alternative convention if the `$` is problematic – `$.foo` can easily become `s.foo` or `scale.foo` with an alternate import name.
While we recommend following this convention for consistency, you can, of
course, adopt an alternative convention if the `$` is problematic – `$.foo` can
easily become `s.foo` or `subshape.foo` with an alternate import name.

## Asynchronous Encoding

Some codecs require asynchronous encoding. Calling `.encode()` on a codec will throw if it or another codec it calls is asynchronous. In this case, you must call `.encodeAsync()` instead, which returns a `Promise<Uint8Array>`. You can call `.encodeAsync()` on any codec; if it is a synchronous codec, it will simply resolve immediately.
Some shapes require asynchronous encoding. Calling `.encode()` on a shape will
throw if it or another shape it calls is asynchronous. In this case, you must
call `.encodeAsync()` instead, which returns a `Promise<Uint8Array>`. You can
call `.encodeAsync()` on any shape; if it is a synchronous shape, it will simply
resolve immediately.

Asynchronous decoding is not supported.

## Custom Codecs
## Custom Shapes

If your encoding/decoding logic is more complicated, you can create custom codecs with `createCodec`:
If your encoding/decoding logic is more complicated, you can create custom
shapes with `createShape`:

```ts
const $foo = $.createCodec<Foo>({
_metadata: $.metadata("$foo"),
const $foo = $.createShape<Foo>({
metadata: $.metadata("$foo"),

// A static estimation of the encoded size, in bytes.
// This can be either an under- or over- estimate.
_staticSize: 123,
_encode(buffer, value) {
staticSize: 123,
subEncode(buffer, value) {
// Encode `value` into `buffer.array`, starting at `buffer.index`.
// A `DataView` is also supplied as `buffer.view`.
// At first, you may only write at most as many bytes as `_staticSize`.
// At first, you may only write at most as many bytes as `staticSize`.
// After you write bytes, you must update `buffer.index` to be the first unwritten byte.

// If you need to write more bytes, call `buffer.pushAlloc(size)`.
Expand All @@ -149,35 +165,35 @@ const $foo = $.createCodec<Foo>({

// You can also call `buffer.insertArray()` to insert an array without consuming any bytes.

// You can delegate to another codec by calling `$bar._encode(buffer, bar)`.
// Before doing so, you must ensure that `$bar._staticSize` bytes are free,
// either by including it in `_staticSize` or by calling `buffer.pushAlloc()`.
// Note that you should use `_encode` and not `encode`.
// You can delegate to another shape by calling `$bar.subEncode(buffer, bar)`.
// Before doing so, you must ensure that `$bar.staticSize` bytes are free,
// either by including it in `staticSize` or by calling `buffer.pushAlloc()`.
// Note that you should use `subEncode` and not `encode`.

// See the `EncodeBuffer` class for information on other methods.

// ...
},

_decode(buffer) {
subDecode(buffer) {
// Decode `value` from `buffer.array`, starting at `buffer.index`.
// A `DataView` is also supplied as `buffer.view`.
// After you read bytes, you must update `buffer.index` to be the first unread byte.

// You can delegate to another codec by calling `$bar._decode(buffer)`.
// Note that you should use `_decode` and not `decode`.
// You can delegate to another shape by calling `$bar.subDecode(buffer)`.
// Note that you should use `subDecode` and not `decode`.

// ...
return value
},

_assert(assert) {
// Validate that `assert.value` is valid for this codec.
subAssert(assert) {
// Validate that `assert.value` is valid for this shape.
// `assert` exposes various utility methods, such as `assert.instanceof`.
// See the `AssertState` class for information on other methods.

// You can delegate to another codec by calling `$bar._assert(assert)` or `$bar._assert(assert.access("key"))`.
// Any errors thrown should be an instance of `$.ScaleAssertError`, and should use `assert.path`.
// You can delegate to another shape by calling `$bar.subAssert(assert)` or `$bar.subAssert(assert.access("key"))`.
// Any errors thrown should be an instance of `$.ShapeAssertError`, and should use `assert.path`.

// ...
},
Expand Down
9 changes: 5 additions & 4 deletions _tasks/build_npm_pkg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@ import { build } from "https://deno.land/x/[email protected]/mod.ts"

await emptyDir("target/npm_pkg")

const DESCRIPTION = "A TypeScript Reference Implementation of SCALE Transcoding"
const description =
"subShape provides primitives and patterns for crafting composable shapes featuring cohesive typing, validation, serialization, and reflection."

await build({
entryPoints: ["mod.ts"],
outDir: "target/npm_pkg",
package: {
name: "scale-codec",
name: "subshape",
version: Deno.args[0]!,
description: DESCRIPTION,
description,
sideEffects: false,
repository: "github:paritytech/scale-ts",
repository: "github:paritytech/subshape",
},
shims: {
deno: {
Expand Down
18 changes: 0 additions & 18 deletions codecs/bench/array.bench.ts

This file was deleted.

4 changes: 0 additions & 4 deletions codecs/bench/bool.bench.ts

This file was deleted.

15 changes: 0 additions & 15 deletions codecs/bench/compact.bench.ts

This file was deleted.

14 changes: 0 additions & 14 deletions codecs/bench/int.bench.ts

This file was deleted.

8 changes: 0 additions & 8 deletions codecs/bench/option.bench.ts

This file was deleted.

19 changes: 0 additions & 19 deletions codecs/bench/str.bench.ts

This file was deleted.

10 changes: 0 additions & 10 deletions codecs/bench/tuple.bench.ts

This file was deleted.

Loading

0 comments on commit 4692d7e

Please sign in to comment.