Skip to content

Commit

Permalink
chore: improve record derivation
Browse files Browse the repository at this point in the history
  • Loading branch information
mikearnaldi committed Jun 22, 2022
1 parent 7203826 commit 0bb87dc
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 62 deletions.
5 changes: 5 additions & 0 deletions .changeset/quick-gorillas-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tsplus/runtime": patch
---

Improve Record Derivation
5 changes: 5 additions & 0 deletions .changeset/real-melons-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tsplus/stdlib": patch
---

Improve Check Types
56 changes: 49 additions & 7 deletions packages/runtime/_src/Decoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -455,16 +455,58 @@ export function deriveEmptyRecord<A extends {}>(
return Decoder((u) => record.is(u) ? Result.success(u) : Result.fail(new DecoderErrorPrimitive(u, "{}")))
}

/**
* @tsplus derive Decoder<_> 15
*/
export function deriveDictionary<A extends Record<string, any>>(
...[value]: Check<Check.IsDictionary<A>> extends Check.True ? [value: Decoder<A[keyof A]>] : never
): Decoder<A> {
return Decoder((u) => {
const asRecordResult = Derive<Decoder<{}>>().decodeResult(u)
if (asRecordResult.isFailure()) {
return Result.fail(asRecordResult.failure)
}
const asRecord = asRecordResult.success
const fieldErrors = Chunk.builder<Decoder.Error>()
let isFailure = false
const res = {}
for (const k of Object.keys(asRecord)) {
const valueResult = value.decodeResult(asRecord[k])
if (valueResult.isFailure()) {
isFailure = true
}
const valueError = valueResult.getWarningOrFailure()
if (valueError.isSome()) {
fieldErrors.append(new DecoderErrorRecordValue(k, valueError.value.merge))
}
const valueSuccess = valueResult.getSuccess()
if (valueSuccess.isNone()) {
continue
}
res[k] = valueSuccess.value
}
const errors = fieldErrors.build()
if (isFailure) {
return Result.fail(new DecoderErrorRecordFields(errors))
}
if (errors.size > 0) {
return Result.successWithWarning(res as A, new DecoderErrorRecordFields(errors))
}
return Result.success(res as A)
})
}

/**
* @tsplus derive Decoder<_> 15
*/
export function deriveRecord<A extends Record<string, any>>(
...[keyGuard, valueDecoder, requiredKeysRecord]: [A] extends [Record<infer X, infer Y>] ? Check<
Check.Not<Check.IsUnion<A>> & Check.IsEqual<A, Record<X, Y>>
> extends Check.True ? [keyGuard: Guard<X>, valueDecoder: Decoder<Y>, requiredKeysRecord: { [k in X]: 0 }]
: never
...[value, requiredKeys]: Check<Check.IsRecord<A>> extends Check.True ? [
value: Decoder<A[keyof A]>,
requiredKeys: { [k in keyof A]: 0 }
]
: never
): Decoder<A> {
const keys = new Set(Object.keys(requiredKeys))
return Decoder((u) => {
const asRecordResult = Derive<Decoder<{}>>().decodeResult(u)
if (asRecordResult.isFailure()) {
Expand All @@ -473,11 +515,11 @@ export function deriveRecord<A extends Record<string, any>>(
const asRecord = asRecordResult.success
const fieldErrors = Chunk.builder<Decoder.Error>()
let isFailure = false
const missing = new Set(Object.keys(requiredKeysRecord))
const missing = new Set(Object.keys(requiredKeys))
const res = {}
for (const k of Object.keys(asRecord)) {
if (keyGuard.is(k)) {
const valueResult = valueDecoder.decodeResult(asRecord[k])
if (keys.has(k)) {
const valueResult = value.decodeResult(asRecord[k])
if (valueResult.isFailure()) {
isFailure = true
}
Expand Down
52 changes: 27 additions & 25 deletions packages/runtime/_src/Encoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,7 @@ export const date: Encoder<Date> = Encoder((u) => u.toISOString())
*
* @tsplus implicit
*/
export const taggedObject: Encoder<{
_tag: string
}> = Derive()
export const taggedObject: Encoder<{ _tag: string }> = Derive()

//
// Derivation Rules
Expand All @@ -83,10 +81,7 @@ export const taggedObject: Encoder<{
* @tsplus derive Encoder<_> 10
*/
export function deriveNamed<A extends Brand<any>>(
...[base]: Check<Check.IsUnion<A>> extends Check.False ? [
base: Encoder<Brand.Unnamed<A>>
]
: never
...[base]: Check<Check.IsUnion<A>> extends Check.False ? [base: Encoder<Brand.Unnamed<A>>] : never
): Encoder<A> {
// @ts-expect-error
return base
Expand All @@ -96,20 +91,15 @@ export function deriveNamed<A extends Brand<any>>(
* @tsplus derive Encoder<_> 10
*/
export function deriveValidation<A extends Brand.Valid<any, any>>(
...[base]: Check<Brand.IsValidated<A>> extends Check.True ? [
base: Encoder<Brand.Unbranded<A>>
]
: never
...[base]: Check<Brand.IsValidated<A>> extends Check.True ? [base: Encoder<Brand.Unbranded<A>>] : never
): Encoder<A> {
return Encoder((a) => base.encode(a as Brand.Unbranded<A>))
}

/**
* @tsplus derive Encoder lazy
*/
export function deriveLazy<A>(
fn: (_: Encoder<A>) => Encoder<A>
): Encoder<A> {
export function deriveLazy<A>(fn: (_: Encoder<A>) => Encoder<A>): Encoder<A> {
let cached: Encoder<A> | undefined
const encoder: Encoder<A> = Encoder((a) => {
if (!cached) {
Expand All @@ -130,18 +120,15 @@ function deriveEitherInternal<E, A>(left: Encoder<E>, right: Encoder<A>): Encode
* @tsplus derive Encoder[Either]<_> 10
*/
export function deriveEither<A extends Either<any, any>>(
...[left, right]: [A] extends [Either<infer _E, infer _A>] ? [left: Encoder<_E>, right: Encoder<_A>]
: never
...[left, right]: [A] extends [Either<infer _E, infer _A>] ? [left: Encoder<_E>, right: Encoder<_A>] : never
): Encoder<A> {
const structural = deriveEitherInternal(left, right)
return Encoder((u) => structural.encode(u))
}

type OptionStructural<A> = { _tag: "None" } | { _tag: "Some"; value: A }

function deriveOptionInternal<A>(
/** @tsplus implicit local */ value: Encoder<A>
): Encoder<OptionStructural<A>> {
function deriveOptionInternal<A>(value: Encoder<A>): Encoder<OptionStructural<A>> {
return Derive()
}

Expand Down Expand Up @@ -225,20 +212,35 @@ export function deriveEmptyRecord<A extends {}>(
return Encoder((a) => a)
}

/**
* @tsplus derive Encoder<_> 15
*/
export function deriveDictionary<A extends Record<string, any>>(
...[value]: Check<Check.IsDictionary<A>> extends Check.True ? [value: Encoder<A[keyof A]>] : never
): Encoder<A> {
return Encoder((u) => {
const encoded = {}
for (const k of Object.keys(u)) {
encoded[k] = value.encode(u[k])
}
return encoded
})
}

/**
* @tsplus derive Encoder<_> 15
*/
export function deriveRecord<A extends Record<string, any>>(
...[keyEncoder, valueEncoder]: [A] extends [Record<infer X, infer Y>] ? Check<
Check.IsEqual<A, Record<X, Y>> & Check.Not<Check.IsUnion<A>>
> extends Check.True ? [key: Encoder<X>, value: Encoder<Y>]
: never
...[value, requiredKeys]: Check<Check.IsRecord<A>> extends Check.True ? [
value: Encoder<A[keyof A]>,
requiredKeys: { [k in keyof A]: 0 }
]
: never
): Encoder<A> {
return Encoder((u) => {
const encoded = {}
for (const k of Object.keys(u)) {
encoded[keyEncoder.encode(k) as any] = valueEncoder.encode(u[k])
for (const k of Object.keys(requiredKeys)) {
encoded[k] = value.encode(u[k])
}
return encoded
})
Expand Down
72 changes: 45 additions & 27 deletions packages/runtime/_src/Guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,33 +278,6 @@ export function deriveEmptyRecord<A extends {}>(
return Guard((u): u is A => typeof u === "object" && u !== null)
}

/**
* @tsplus derive Guard<_> 15
*/
export function deriveRecord<A extends Record<string, any>>(
...[keyGuard, valueGuard, requiredKeysRecord]: [A] extends [Record<infer X, infer Y>] ? Check<
Check.IsEqual<A, Record<X, Y>> & Check.Not<Check.IsUnion<A>>
> extends Check.True ? [key: Guard<X>, value: Guard<Y>, requiredKeysRecord: { [k in X]: 0 }]
: never
: never
): Guard<A> {
return Guard((u): u is A => {
const missing = new Set(Object.keys(requiredKeysRecord))
if (Derive<Guard<{}>>().is(u)) {
for (const k of Object.keys(u)) {
if (keyGuard.is(k)) {
if (!valueGuard.is(u[k])) {
return false
}
missing.delete(k)
}
}
return missing.size === 0
}
return false
})
}

/**
* @tsplus derive Guard<_> 20
*/
Expand Down Expand Up @@ -343,6 +316,51 @@ export function deriveStruct<A extends Record<string, any>>(
})
}

/**
* @tsplus derive Guard<_> 15
*/
export function deriveDictionary<A extends Record<string, any>>(
...[valueGuard]: Check<Check.IsDictionary<A>> extends Check.True ? [value: Guard<A[keyof A]>] : never
): Guard<A> {
return Guard((u): u is A => {
if (Derive<Guard<{}>>().is(u)) {
for (const k of Object.keys(u)) {
if (!valueGuard.is(u[k])) {
return false
}
}
return true
}
return false
})
}

/**
* @tsplus derive Guard<_> 15
*/
export function deriveRecord<A extends Record<string, any>>(
...[value, requiredKeys]: Check<Check.IsRecord<A>> extends Check.True ? [
value: Guard<A[keyof A]>,
requiredKeys: { [k in keyof A]: 0 }
]
: never
): Guard<A> {
const keys = new Set(Object.keys(requiredKeys))
return Guard((u): u is A => {
const missing = new Set(Object.keys(requiredKeys))
if (Derive<Guard<{}>>().is(u)) {
for (const k of Object.keys(u)) {
if (keys.has(k) && !value.is(u[k])) {
return false
}
missing.delete(k)
}
return missing.size === 0
}
return false
})
}

/**
* @tsplus derive Guard<_> 20
*/
Expand Down
8 changes: 6 additions & 2 deletions packages/runtime/_test/Decoder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,15 +284,19 @@ describe.concurrent("Decoder", () => {
assert.deepEqual(decoder2.decode({ a: { foo: "ok" } }), Either.right({ a: { foo: "ok" } }))
assert.deepEqual(
decoder2.decode({}).left.value?.message,
"Encountered while parsing a record structure, missing keys: \"a\""
"Encountered while parsing an object structure\n" +
"└─ Field \"a\"\n" +
" └─ Missing"
)
assert.deepEqual(
decoder3.decode({}).left.value?.message,
"Encountered while parsing a record structure, missing keys: \"a\", \"b\""
)
assert.deepEqual(
decoder2.decode({ b: { foo: "ok" } }).left.value?.message,
"Encountered while parsing a record structure, missing keys: \"a\""
"Encountered while parsing an object structure\n" +
"└─ Field \"a\"\n" +
" └─ Missing"
)
})
})
21 changes: 20 additions & 1 deletion packages/stdlib/_src/type-level/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,26 @@ export declare namespace Check {
/**
* @tsplus type Check.IsStruct
*/
type IsStruct<A> = Check.Extends<keyof A, string> & Check.Not<Check.IsUnion<A>>
type IsStruct<A> =
& Not<Extends<string, keyof A>>
& Not<IsUnion<A>>
& IsEqual<A, { [k in keyof A]: A[k] }>

/**
* @tsplus type Check.IsDictionary
*/
type IsDictionary<A> =
& IsEqual<A, Record<keyof A, A[keyof A]>>
& Extends<string, keyof A>
& Not<IsUnion<A>>

/**
* @tsplus type Check.IsRecord
*/
type IsRecord<A> =
& IsEqual<A, Record<keyof A, A[keyof A]>>
& IsUnion<keyof A>
& Not<IsUnion<A>>

/**
* @tsplus type Check.HaveSameLength
Expand Down

0 comments on commit 0bb87dc

Please sign in to comment.