Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: separate input and output types #155

Merged
merged 1 commit into from
Apr 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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