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

support partially enumerable record #707

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
170 changes: 131 additions & 39 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,44 +324,103 @@ export type TypeOfDictionary<D extends Any, C extends Any> = { [K in TypeOf<D>]:
export type OutputOfDictionary<D extends Any, C extends Any> = { [K in OutputOf<D>]: OutputOf<C> }

function enumerableRecord<D extends Mixed, C extends Mixed>(
keys: Array<string>,
keys: Set<string>,
domain: D,
codomain: C,
name = `{ [K in ${domain.name}]: ${codomain.name} }`
name = getRecordName(domain, codomain)
): RecordC<D, C> {
const len = keys.length
const props: Props = {}
for (let i = 0; i < len; i++) {
props[keys[i]] = codomain
}
const exactCodec = strict(props, name)
keys.forEach((key) => {
props[key] = codomain
})
const strictCodec = strict(props, name)

return new DictionaryType(
name,
(u): u is { [K in TypeOf<D>]: TypeOf<C> } => exactCodec.is(u),
exactCodec.validate,
exactCodec.encode,
domain,
codomain
)
return new DictionaryType(name, strictCodec.is, strictCodec.validate, strictCodec.encode, domain, codomain)
}

type StringMembers =
| { literals: Set<string>; nonEnumerable?: Mixed | NeverC }
| { literals?: Set<string>; nonEnumerable: Mixed | NeverC }

/**
* @internal
*/
export function getDomainKeys<D extends Mixed>(domain: D): Record<string, unknown> | undefined {
if (isLiteralC(domain)) {
const literal = domain.value
export function enumerate<T extends Mixed>(codec: T): StringMembers {
if (isLiteralC(codec)) {
const literal = codec.value
if (string.is(literal)) {
return { [literal]: null }
return { literals: new Set([literal]) }
}
} else if (isKeyofC(codec)) {
return { literals: new Set(Object.keys(codec.keys)) }
} else if (isUnionC(codec)) {
const literals = new Set<string>()
const nonEnumerableSubtypes: Array<Mixed> = []
for (const type of codec.types) {
const { literals: subtypeLiterals, nonEnumerable: subtypeNonEnumerable } = enumerate(type)
subtypeLiterals?.forEach((key) => literals.add(key))
if (subtypeNonEnumerable && !(subtypeNonEnumerable instanceof NeverType)) {
nonEnumerableSubtypes.push(subtypeNonEnumerable)
}
}
const len = nonEnumerableSubtypes.length
if (len > 0) {
const nonEnumerable =
len > 1 ? union(nonEnumerableSubtypes as [Mixed, Mixed, ...Array<Mixed>]) : nonEnumerableSubtypes[0]
// allow broader non-enumerable type to subsume narrower literal types
literals.forEach((literal) => {
if (nonEnumerable.is(literal)) {
literals.delete(literal)
}
})
return literals.size > 0 ? { literals, nonEnumerable } : { nonEnumerable }
} else {
return literals.size > 0 ? { literals } : { nonEnumerable: never }
}
} else if (isIntersectionC(codec)) {
let literals: undefined | Set<string> = undefined
const nonEnumerableSupertypes: Array<Mixed | NeverC> = []
for (const type of codec.types) {
const { literals: supertypeLiterals, nonEnumerable: supertypeNonEnumerable } = enumerate(type)
if (supertypeLiterals) {
if (!literals) {
literals = supertypeLiterals
} else {
literals.forEach((key) => {
if (!supertypeLiterals.has(key)) {
literals?.delete(key)
}
})
}
}
if (supertypeNonEnumerable) {
nonEnumerableSupertypes.push(supertypeNonEnumerable)
}
}
if (literals) {
if (literals.size === 0) {
return { nonEnumerable: never }
}
const nonEnumerableSupertypesDisjointFromLiterals = nonEnumerableSupertypes.filter((nonEnumerable) => {
let shouldKeep = true
literals?.forEach((key) => {
if (nonEnumerable.is(key)) {
shouldKeep = false
return
}
})
return shouldKeep
})
if (nonEnumerableSupertypesDisjointFromLiterals.length > 0) {
return { nonEnumerable: never }
} else {
return { literals }
}
} else {
return { nonEnumerable: codec }
}
} else if (isKeyofC(domain)) {
return domain.keys
} else if (isUnionC(domain)) {
const keys = domain.types.map((type) => getDomainKeys(type))
return keys.some(undefinedType.is) ? undefined : Object.assign({}, ...keys)
}
return undefined
return { nonEnumerable: codec }
}

function stripNonDomainKeys(o: any, domain: Mixed) {
Expand All @@ -381,15 +440,16 @@ function stripNonDomainKeys(o: any, domain: Mixed) {
}

function nonEnumerableRecord<D extends Mixed, C extends Mixed>(
domain: D,
nonEnumerableDomain: Mixed,
entireDomain: D,
codomain: C,
name = `{ [K in ${domain.name}]: ${codomain.name} }`
name = getRecordName(entireDomain, codomain)
): RecordC<D, C> {
return new DictionaryType(
name,
(u): u is { [K in TypeOf<D>]: TypeOf<C> } => {
if (UnknownRecord.is(u)) {
return Object.keys(u).every((k) => !domain.is(k) || codomain.is(u[k]))
return Object.keys(u).every((k) => !nonEnumerableDomain.is(k) || codomain.is(u[k]))
}
return isAnyC(codomain) && Array.isArray(u)
},
Expand All @@ -403,7 +463,7 @@ function nonEnumerableRecord<D extends Mixed, C extends Mixed>(
for (let i = 0; i < len; i++) {
let k = keys[i]
const ok = u[k]
const domainResult = domain.validate(k, appendContext(c, k, domain, k))
const domainResult = nonEnumerableDomain.validate(k, appendContext(c, k, nonEnumerableDomain, k))
if (isLeft(domainResult)) {
changed = true
} else {
Expand All @@ -427,19 +487,19 @@ function nonEnumerableRecord<D extends Mixed, C extends Mixed>(
}
return failure(u, c)
},
domain.encode === identity && codomain.encode === identity
? (a) => stripNonDomainKeys(a, domain)
nonEnumerableDomain.encode === identity && codomain.encode === identity
? (a) => stripNonDomainKeys(a, nonEnumerableDomain)
: (a) => {
const s: { [key: string]: any } = {}
const keys = Object.keys(stripNonDomainKeys(a, domain))
const keys = Object.keys(stripNonDomainKeys(a, nonEnumerableDomain))
const len = keys.length
for (let i = 0; i < len; i++) {
const k = keys[i]
s[String(domain.encode(k))] = codomain.encode(a[k])
s[String(nonEnumerableDomain.encode(k))] = codomain.encode(a[k])
}
return s as any
},
domain,
entireDomain,
codomain
)
}
Expand All @@ -448,6 +508,10 @@ function getUnionName<CS extends [Mixed, Mixed, ...Array<Mixed>]>(codecs: CS): s
return '(' + codecs.map((type) => type.name).join(' | ') + ')'
}

function getRecordName<D extends Mixed, C extends Mixed>(domain: D, codomain: C): string {
return `{ [K in ${domain.name}]: ${codomain.name} }`
}

/**
* @internal
*/
Expand Down Expand Up @@ -1512,11 +1576,39 @@ export interface RecordC<D extends Mixed, C extends Mixed>
* @category combinators
* @since 1.7.1
*/
export function record<D extends Mixed, C extends Mixed>(domain: D, codomain: C, name?: string): RecordC<D, C> {
const keys = getDomainKeys(domain)
return keys
? enumerableRecord(Object.keys(keys), domain, codomain, name)
: nonEnumerableRecord(domain, codomain, name)
export function record<D extends Mixed, C extends Mixed>(
domain: D,
codomain: C,
name = getRecordName(domain, codomain)
): RecordC<D, C> {
const { literals, nonEnumerable } = enumerate(domain)
if (literals && nonEnumerable && !(nonEnumerable instanceof NeverType)) {
const enumerablesObj: Record<string, null> = {}
literals.forEach((k) => {
enumerablesObj[k] = null
})
const intersectionCodec = intersection(
[
nonEnumerableRecord(nonEnumerable, domain, codomain, getRecordName(nonEnumerable, codomain)),
enumerableRecord(literals, domain, codomain, getRecordName(keyof(enumerablesObj), codomain))
],
name
)
return new DictionaryType(
name,
intersectionCodec.is,
intersectionCodec.validate,
intersectionCodec.encode,
domain,
codomain
)
} else if (literals) {
return enumerableRecord(literals, domain, codomain, name)
} else if (nonEnumerable) {
return nonEnumerableRecord(nonEnumerable as any as Mixed, domain, codomain, name)
} else {
throw new Error(`unexpectedly found neither enumerable nor non-enumerable keys in ${domain.name}`)
}
}

/**
Expand Down