Skip to content

Commit

Permalink
feat: separate input and output types (#155)
Browse files Browse the repository at this point in the history
  • Loading branch information
tjjfvi authored Apr 8, 2023
1 parent dc64f65 commit 1c211ce
Show file tree
Hide file tree
Showing 22 changed files with 160 additions and 117 deletions.
4 changes: 2 additions & 2 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ const decodedValue: Superhero = $superhero.decode(encodedBytes)
assertEquals(decodedValue, valueToEncode)
```

To extract the JS-native TypeScript type from a given codec, use the `Native` utility type.
To extract the type from a given codec, you can use the `Output` utility type.

```ts
type Superhero = $.Native<typeof $superhero>
type Superhero = $.Output<typeof $superhero>
// {
// pseudonym: string;
// secretIdentity?: string | undefined;
Expand Down
13 changes: 8 additions & 5 deletions codecs/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ type ArrayOfLength<
: L extends A["length"] ? A
: ArrayOfLength<T, L, [...A, T]>

export function sizedArray<L extends number, T>($el: Codec<T>, length: L): Codec<ArrayOfLength<T, L>> {
export function sizedArray<L extends number, I, O>($el: Codec<I, O>, length: L): Codec<
Readonly<ArrayOfLength<I, L>>,
ArrayOfLength<O, L>
> {
return createCodec({
_metadata: metadata("$.sizedArray", sizedArray, $el, length),
_staticSize: $el._staticSize * length,
Expand All @@ -22,11 +25,11 @@ export function sizedArray<L extends number, T>($el: Codec<T>, length: L): Codec
}
},
_decode(buffer) {
const value: T[] = Array(length)
const value: O[] = Array(length)
for (let i = 0; i < value.length; i++) {
value[i] = $el._decode(buffer)
}
return value as ArrayOfLength<T, L>
return value as ArrayOfLength<O, L>
},
_assert(assert) {
assert.instanceof(this, Array)
Expand All @@ -38,7 +41,7 @@ export function sizedArray<L extends number, T>($el: Codec<T>, length: L): Codec
})
}

export function array<T>($el: Codec<T>): Codec<T[]> {
export function array<I, O = I>($el: Codec<I, O>): Codec<readonly I[], O[]> {
return createCodec({
_metadata: metadata("$.array", array, $el),
_staticSize: compactU32._staticSize,
Expand All @@ -54,7 +57,7 @@ export function array<T>($el: Codec<T>): Codec<T[]> {
},
_decode(buffer) {
const length = compactU32._decode(buffer)
const value: T[] = Array(length)
const value: O[] = Array(length)
for (let i = 0; i < value.length; i++) {
value[i] = $el._decode(buffer)
}
Expand Down
2 changes: 1 addition & 1 deletion codecs/compact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ compactVisitor.add(constant<any>, (codec) => codec)
compactVisitor.add(tuple<any[]>, (codec, ...entries) => {
if (entries.length === 0) return codec
if (entries.length > 1) throw new Error("Cannot derive compact codec for tuples with more than one field")
return withMetadata(metadata("$.compact", compact, codec), tuple<any[]>(compact(entries[0]!)))
return withMetadata(metadata("$.compact", compact<any>, codec), tuple(compact(entries[0]!)))
})

compactVisitor.add(field<any, any>, (codec, key, value) => {
Expand Down
4 changes: 2 additions & 2 deletions codecs/deferred.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Codec, createCodec, metadata } from "../common/mod.ts"

export function deferred<T>(getCodec: () => Codec<T>): Codec<T> {
let $codec: Codec<T>
export function deferred<I, O>(getCodec: () => Codec<I, O>): Codec<I, O> {
let $codec: Codec<I, O>
const codec = createCodec({
_metadata: metadata("$.deferred", deferred, getCodec),
_staticSize: 0,
Expand Down
10 changes: 5 additions & 5 deletions codecs/instance.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { AssertState } from "../common/assert.ts"
import { Codec, createCodec, metadata } from "../common/mod.ts"

export function instance<A extends unknown[], T>(
ctor: new(...args: A) => T,
export function instance<A extends unknown[], O extends I, I = O>(
ctor: new(...args: A) => O,
$args: Codec<A>,
toArgs: (value: T) => [...A],
): Codec<T> {
toArgs: (value: I) => [...A],
): Codec<I, O> {
return createCodec({
_metadata: metadata("$.instance", instance, ctor, $args, toArgs),
_staticSize: $args._staticSize,
Expand All @@ -17,7 +17,7 @@ export function instance<A extends unknown[], T>(
},
_assert(assert) {
assert.instanceof(this, ctor)
$args._assert(new AssertState(toArgs(assert.value as T), "#arguments", assert))
$args._assert(new AssertState(toArgs(assert.value as O), "#arguments", assert))
},
})
}
2 changes: 1 addition & 1 deletion codecs/int.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export function int(signed: boolean, size: 8 | 16 | 32 | 64 | 128 | 256): Codec<
}

function intMetadata<T extends number | bigint>(signed: boolean, size: number) {
return metadata<T>(
return metadata<T, T>(
metadata(`$.${signed ? "i" : "u"}${size}`),
metadata("$.int", int as any, signed, size),
)
Expand Down
19 changes: 11 additions & 8 deletions codecs/iterable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ import { tuple } from "./tuple.ts"

const compactU32 = compact(u32)

export function iterable<T, I extends Iterable<T>>(
export function iterable<TI, I extends Iterable<TI>, TO = TI, O = I>(
props: {
$el: Codec<T>
$el: Codec<TI, TO>
calcLength: (iterable: I) => number
rehydrate: (iterable: Iterable<T>) => I
assert: (this: Codec<I>, assert: AssertState) => void
rehydrate: (iterable: Iterable<TO>) => O
assert: (this: Codec<I, O>, assert: AssertState) => void
},
): Codec<I> {
): Codec<I, O> {
return createCodec({
_metadata: metadata("$.iterable", iterable, props),
_staticSize: compactU32._staticSize,
Expand Down Expand Up @@ -64,7 +64,7 @@ export function iterable<T, I extends Iterable<T>>(
})
}

export function set<T>($el: Codec<T>): Codec<Set<T>> {
export function set<I, O>($el: Codec<I, O>): Codec<ReadonlySet<I>, Set<O>> {
return withMetadata(
metadata("$.set", set, $el),
iterable({
Expand All @@ -78,8 +78,11 @@ export function set<T>($el: Codec<T>): Codec<Set<T>> {
)
}

export function map<K, V>($key: Codec<K>, $value: Codec<V>): Codec<Map<K, V>> {
return withMetadata(
export function map<KI, KO, VI, VO>(
$key: Codec<KI, KO>,
$value: Codec<VI, VO>,
): Codec<ReadonlyMap<KI, VI>, Map<KO, VO>> {
return withMetadata<ReadonlyMap<KI, VI>, Map<KO, VO>>(
metadata("$.map", map, $key, $value),
iterable({
$el: tuple($key, $value),
Expand Down
2 changes: 1 addition & 1 deletion codecs/lenPrefixed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { u32 } from "./int.ts"

const compactU32 = compact(u32)

export function lenPrefixed<T>($inner: Codec<T>): Codec<T> {
export function lenPrefixed<I, O>($inner: Codec<I, O>): Codec<I, O> {
return createCodec({
_metadata: metadata("$.lenPrefixed", lenPrefixed, $inner),
_staticSize: compactU32._staticSize + $inner._staticSize,
Expand Down
32 changes: 23 additions & 9 deletions codecs/object.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { AnyCodec, Codec, CodecVisitor, createCodec, Expand, metadata, Native, U2I } from "../common/mod.ts"
import { AnyCodec, Codec, CodecVisitor, createCodec, Expand, Input, metadata, Output, U2I } from "../common/mod.ts"
import { constant } from "./constant.ts"
import { option } from "./option.ts"

export function field<K extends keyof any, V>(key: K, $value: Codec<V>): Codec<Expand<Record<K, V>>> {
export function field<K extends keyof any, V>(key: K, $value: Codec<V>): Codec<
Expand<Readonly<Record<K, V>>>,
Expand<Record<K, V>>
> {
return createCodec({
_metadata: metadata("$.field", field, key, $value),
_staticSize: $value._staticSize,
Expand All @@ -18,7 +21,10 @@ export function field<K extends keyof any, V>(key: K, $value: Codec<V>): Codec<E
})
}

export function optionalField<K extends keyof any, V>(key: K, $value: Codec<V>): Codec<Expand<Partial<Record<K, V>>>> {
export function optionalField<K extends keyof any, V>(key: K, $value: Codec<V>): Codec<
Expand<Readonly<Partial<Record<K, V>>>>,
Expand<Partial<Record<K, V>>>
> {
const $option = option($value)
return createCodec({
_metadata: metadata("$.optionalField", optionalField, key, $value),
Expand All @@ -43,11 +49,19 @@ export function optionalField<K extends keyof any, V>(key: K, $value: Codec<V>):
})
}

export type NativeObject<T extends AnyCodec[]> = Expand<
export type InputObject<T extends AnyCodec[]> = Expand<
U2I<
| { x: {} }
| {
[K in keyof T]: { x: Native<T[K]> }
[K in keyof T]: { x: Input<T[K]> }
}[number]
>["x"]
>
export type OutputObject<T extends AnyCodec[]> = Expand<
U2I<
| { x: {} }
| {
[K in keyof T]: { x: Output<T[K]> }
}[number]
>["x"]
>
Expand All @@ -56,17 +70,17 @@ type UnionKeys<T> = T extends T ? keyof T : never
export type ObjectMembers<T extends AnyCodec[]> = [
...never extends T ? {
[K in keyof T]:
& UnionKeys<Native<T[K]>>
& UnionKeys<Input<T[K]>>
& {
[L in keyof T]: K extends L ? never : UnionKeys<Native<T[L]>>
[L in keyof T]: K extends L ? never : UnionKeys<Input<T[L]>>
}[number] extends (infer O extends keyof any)
? [O] extends [never] ? Codec<Native<T[K]> & {}> : Codec<{ [_ in O]?: never }>
? [O] extends [never] ? Codec<Input<T[K]> & {}> : Codec<{ [_ in O]?: never }>
: never
}
: T,
]

export function object<T extends AnyCodec[]>(...members: ObjectMembers<T>): Codec<NativeObject<T>> {
export function object<T extends AnyCodec[]>(...members: ObjectMembers<T>): Codec<InputObject<T>, OutputObject<T>> {
return createCodec({
_metadata: metadata("$.object", object<T>, ...members),
_staticSize: members.map((x) => x._staticSize).reduce((a, b) => a + b, 0),
Expand Down
12 changes: 6 additions & 6 deletions codecs/option.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import { Codec, createCodec, metadata, ScaleDecodeError } from "../common/mod.ts"

export function option<T>($some: Codec<T>): Codec<T | undefined>
export function option<T, U>($some: Codec<T>, none: U): Codec<T | U>
export function option<T, U>($some: Codec<T>, none?: U): Codec<T | U> {
export function option<SI, SO>($some: Codec<SI, SO>): Codec<SI | undefined, SO | undefined>
export function option<SI, SO, N>($some: Codec<SI, SO>, none: N): Codec<SI | N, SO | N>
export function option<SI, SO, N>($some: Codec<SI, SO>, none?: N): Codec<SI | N, SO | N> {
if ($some._metadata.some((x) => x.factory === option && x.args[1] === none)) {
throw new Error("Nested option codec will not roundtrip correctly")
}
return createCodec({
_metadata: metadata("$.option", option<T, U>, $some, ...(none === undefined ? [] : [none!]) as [U]),
_metadata: metadata("$.option", option<SI, SO, N>, $some, ...(none === undefined ? [] : [none!]) as [N]),
_staticSize: 1 + $some._staticSize,
_encode(buffer, value) {
if ((buffer.array[buffer.index++] = +(value !== none))) {
$some._encode(buffer, value as T)
$some._encode(buffer, value as SI)
}
},
_decode(buffer) {
switch (buffer.array[buffer.index++]) {
case 0:
return none as U
return none as N
case 1: {
const value = $some._decode(buffer)
if (value === none) {
Expand Down
2 changes: 1 addition & 1 deletion codecs/promise.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Codec, createCodec, metadata } from "../common/mod.ts"

export function promise<T>($value: Codec<T>): Codec<Promise<T>> {
export function promise<I, O>($value: Codec<I, O>): Codec<Promise<I>, Promise<O>> {
return createCodec({
_metadata: metadata("$.promise", promise, $value),
_staticSize: $value._staticSize,
Expand Down
4 changes: 2 additions & 2 deletions codecs/record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { str } from "./str.ts"
import { transform } from "./transform.ts"
import { tuple } from "./tuple.ts"

export function record<V>($value: Codec<V>) {
return transform<[string, V][], Record<string, V>>({
export function record<I, O>($value: Codec<I, O>): Codec<Readonly<Record<string, I>>, Record<string, O>> {
return transform({
$base: array(tuple(str, $value)),
encode: Object.entries,
decode: Object.fromEntries,
Expand Down
12 changes: 6 additions & 6 deletions codecs/result.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Codec, createCodec, metadata, ScaleDecodeError } from "../common/mod.ts"

export function result<Ok, Err extends Error>(
$ok: Codec<Ok>,
$err: Codec<Err>,
): Codec<Ok | Err> {
export function result<TI, TO, UI extends Error, UO extends Error>(
$ok: Codec<TI, TO>,
$err: Codec<UI, UO>,
): Codec<TI | UI, TO | UO> {
if ($ok._metadata.some((x) => x.factory === result)) {
throw new Error("Nested result codec will not roundtrip correctly")
}
Expand All @@ -12,9 +12,9 @@ export function result<Ok, Err extends Error>(
_staticSize: 1 + Math.max($ok._staticSize, $err._staticSize),
_encode(buffer, value) {
if ((buffer.array[buffer.index++] = +(value instanceof Error))) {
$err._encode(buffer, value as Err)
$err._encode(buffer, value as UI)
} else {
$ok._encode(buffer, value as Ok)
$ok._encode(buffer, value as TI)
}
},
_decode(buffer) {
Expand Down
18 changes: 17 additions & 1 deletion codecs/test/__snapshots__/str.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,7 @@ f9
`;

snapshot[`\$.str words 1`] = `
15
55
03
61
76
Expand Down Expand Up @@ -557,6 +557,14 @@ snapshot[`\$.str words 1`] = `
6f
6e
0a
44
65
63
6f
64
65
63
0a
64
65
6e
Expand All @@ -578,6 +586,14 @@ snapshot[`\$.str words 1`] = `
6e
74
0a
45
6e
63
6f
64
65
63
0a
68
79
64
Expand Down
14 changes: 7 additions & 7 deletions codecs/transform.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { AssertState, Codec, createCodec, metadata } from "../common/mod.ts"

export function transform<T, U>(
export function transform<TI, UI, TO = TI, UO = UI>(
props: {
$base: Codec<T>
encode: (value: U) => T
decode: (value: T) => U
assert?: (this: Codec<U>, assert: AssertState) => void
$base: Codec<TI, TO>
encode: (value: UI) => TI
decode: (value: TO) => UO
assert?: (this: Codec<UI, UO>, assert: AssertState) => void
},
): Codec<U> {
): Codec<UI, UO> {
return createCodec({
_metadata: metadata("$.transform", transform, props),
_staticSize: props.$base._staticSize,
Expand All @@ -19,7 +19,7 @@ export function transform<T, U>(
},
_assert(assert) {
props.assert?.call(this, assert)
props.$base._assert(new AssertState(props.encode(assert.value as U), "#encode", assert))
props.$base._assert(new AssertState(props.encode(assert.value as UI), "#encode", assert))
},
})
}
11 changes: 7 additions & 4 deletions codecs/tuple.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { AnyCodec, Codec, createCodec, metadata } from "../common/mod.ts"
import { AnyCodec, Codec, createCodec, Input, metadata, Output } from "../common/mod.ts"

export type NativeTuple<ElCodecs extends AnyCodec[]> = {
[I in keyof ElCodecs]: ElCodecs[I] extends Codec<infer T> ? T : never
type InputTuple<T extends AnyCodec[]> = {
readonly [K in keyof T]: Input<T[K]>
}
type OutputTuple<T extends AnyCodec[]> = {
[K in keyof T]: Output<T[K]>
}

export function tuple<T extends AnyCodec[]>(...codecs: [...T]): Codec<NativeTuple<T>> {
export function tuple<T extends AnyCodec[]>(...codecs: [...T]): Codec<InputTuple<T>, OutputTuple<T>> {
return createCodec({
_metadata: metadata("$.tuple", tuple<T>, ...codecs),
_staticSize: codecs.map((x) => x._staticSize).reduce((a, b) => a + b, 0),
Expand Down
Loading

0 comments on commit 1c211ce

Please sign in to comment.