Skip to content

Instantly share code, notes, and snippets.

@briancavalier
Created January 14, 2025 14:50
Show Gist options
  • Save briancavalier/b4ba91e33a9454a30fa49fb6f269b773 to your computer and use it in GitHub Desktop.
Save briancavalier/b4ba91e33a9454a30fa49fb6f269b773 to your computer and use it in GitHub Desktop.
import { Fail, Ok, Parsed, Schema, _decode, ok } from './schema'
export const parse = <S extends Schema>(s: S, x: string): Ok<Parsed<S>> | Fail =>
_decode(s, JSON.parse(x))
export const stringify = <S>(s: S, x: Parsed<S>): Ok<string> | Fail => {
const r = _decode(s as any, x)
return r.ok ? ok(JSON.stringify(r.value)) : r
}
export const schema = Symbol.for('@braindump/codec/schema')
export interface AnyNumber { readonly [schema]: 'number' }
export const number: AnyNumber = { [schema]: 'number' }
export interface AnyString { readonly [schema]: 'string' }
export const string: AnyString = { [schema]: 'string' }
export interface AnyBoolean { readonly [schema]: 'boolean' }
export const boolean: AnyBoolean = { [schema]: 'boolean' }
export interface Union<Schemas extends readonly unknown[]> {
readonly [schema]: 'union',
readonly schemas: Schemas
}
export const union = <const S extends readonly Schema[]>(...schemas: S): Union<S> => ({ [schema]: 'union', schemas })
export interface ArrayOf<Schema> {
readonly [schema]: 'array',
readonly items: Schema
}
export const array = <S extends Schema>(s: S): ArrayOf<S> => ({ [schema]: 'array', items: s })
export interface Optional<S> {
readonly [schema]: 'optional',
readonly schema: S
}
export const optional = <S extends Schema>(s: S): Optional<S> => ({ [schema]: 'optional', schema: s })
export const isOptional = (s: unknown): s is Optional<unknown> => !!s && (s as Record<PropertyKey, unknown>)[schema] === 'optional'
export type Schema =
| string
| number
| boolean
| null
| AnyString
| AnyNumber
| AnyBoolean
| ((x: any) => x is unknown)
| Union<readonly Schema[]>
| ArrayOf<Schema>
| { readonly [s: string]: Schema | Optional<Schema> }
export type Parsed<S> =
S extends string | number | boolean | null ? S
: S extends AnyString ? string
: S extends AnyNumber ? number
: S extends AnyBoolean ? boolean
: S extends (x: any) => x is infer A ? A
: S extends Union<infer Schemas> ? Parsed<Schemas[number]>
: S extends ArrayOf<infer Schema> ? readonly Parsed<Schema>[]
: S extends { readonly [k: string]: Schema | Optional<Schema> }
? { readonly [K in RequiredKeys<S>]: Parsed<S[K]> } &
{ readonly [K in OptionalKeys<S>]?: S[K] extends Optional<infer SS> ? Parsed<SS> : never }
: unknown
type OptionalKeys<S> = {
readonly [K in keyof S]: S[K] extends Optional<unknown> ? K : never
}[keyof S]
type RequiredKeys<S> = Exclude<keyof S, OptionalKeys<S>>
export interface Ok<A> { readonly ok: true, readonly value: A }
export const ok = <A>(a: A) => ({ ok: true, value: a }) as const
export const unexpected = <S, I>(schema: S, input: I) => ({ ok: false, type: 'unexpected', schema, input }) as const
export const missing = <K, S>(key: K, schema: S) => ({ ok: false, type: 'missing', key, schema }) as const
export const at = <K, E>(key: K, error: E) => ({ ok: false, type: 'at', key, error }) as const
export const stopped = <I, E>(input: I, error: E) => ({ ok: false, type: 'stopped', input, error }) as const
export const none = <S, I, E extends readonly unknown[]>(schema: S, input: I, errors: E) => ({ ok: false, type: 'none', schema, input, errors }) as const
export const all = <I, E extends readonly unknown[]>(input: I, errors: E) => ({ ok: false, type: 'all', input, errors }) as const
export type Fail =
| ReturnType<typeof unexpected>
| ReturnType<typeof missing>
| ReturnType<typeof at>
| ReturnType<typeof stopped>
| ReturnType<typeof none>
| ReturnType<typeof all>
export const assertOk = <A>(r: Ok<A> | Fail): A => {
if (r.ok) return r.value
throw new AssertResultError(r)
}
class AssertResultError extends Error {
constructor(public readonly fail: Fail) {
super()
}
get message(): string {
return JSON.stringify(this.fail, null, 2)
}
}
export const assert = <S extends Schema>(s: S, x: unknown): Parsed<S> =>
assertOk(_decode(s, x))
export const decode = <S extends Schema>(s: S, x: unknown): Ok<Parsed<S>> | Fail =>
_decode(s, x)
export const _decode = <S extends Schema>(s: S, x: unknown): Ok<any> | Fail => {
if (s === null || typeof s === 'number' || typeof s === 'string' || typeof s === 'boolean' || typeof s === 'bigint')
return s === x ? ok(x) : unexpected(s, x)
if (typeof s === 'function')
return s(x) ? ok(x) : unexpected(s, x)
if (schema in s)
switch (s[schema]) {
case 'number':
case 'string':
case 'boolean':
return s[schema] === typeof x ? ok(x) : unexpected(s, x)
case 'union':
return decodeUnion(s, x)
case 'array':
return Array.isArray(x) ? decodeArray(s, x) : unexpected(s, x)
}
if (typeof x !== 'object')
return x && typeof x === 'object'
? decodeProperties(s, x as Record<string, unknown>)
: unexpected(s, x)
return unexpected(s, x)
}
const decodeArray = (s: ArrayOf<unknown>, x: readonly unknown[]) => {
const r = decodeArrayItems(s.items as Schema, x)
return r.ok ? r : stopped(x, r)
}
const decodeArrayItems = (items: Schema, x: readonly unknown[]) => {
const a = []
for (let i = 0; i < x.length; i++) {
const r = _decode(items, x[i])
if (!r.ok) return at(i, r)
else a[i] = r.value
}
return ok(a)
}
const decodeProperties = (s: Record<string, Schema | Optional<Schema>>, x: Record<string, unknown>) => {
const a = {} as Record<string, unknown>
const e = []
for (const k of Object.keys(s)) {
const sk = s[k]
if (k in x) {
const r = _decode(isOptional(sk) ? sk.schema as Schema : sk, x[k])
if (!r.ok) e.push(at(k, r))
else a[k] = r.value
} else {
if (!isOptional(s[k])) e.push(missing(k, sk))
}
}
return e.length === 0 ? ok(a) : all(x, e)
}
const decodeUnion = (s: Union<readonly unknown[]>, x: unknown) => {
const e = []
for (let i = 0; i < s.schemas.length; i++) {
const r = _decode(s.schemas[i] as Schema, x)
if (r.ok) return r
e.push(r)
}
return none(s, x, e)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment