effect
Version:
The missing standard library for TypeScript, for writing production-grade software.
288 lines (255 loc) • 6.59 kB
text/typescript
/**
* @since 2.0.0
*/
import type * as FiberRefs from "./FiberRefs.js"
import { globalValue } from "./GlobalValue.js"
import * as Predicate from "./Predicate.js"
/**
* @since 2.0.0
* @category symbols
*/
export const NodeInspectSymbol = Symbol.for("nodejs.util.inspect.custom")
/**
* @since 2.0.0
* @category symbols
*/
export type NodeInspectSymbol = typeof NodeInspectSymbol
/**
* @since 2.0.0
* @category models
*/
export interface Inspectable {
toString(): string
toJSON(): unknown
[NodeInspectSymbol](): unknown
}
/**
* @since 2.0.0
*/
export const toJSON = (x: unknown): unknown => {
try {
if (
Predicate.hasProperty(x, "toJSON") && Predicate.isFunction(x["toJSON"]) &&
x["toJSON"].length === 0
) {
return x.toJSON()
} else if (Array.isArray(x)) {
return x.map(toJSON)
}
} catch {
return {}
}
return redact(x)
}
const CIRCULAR = "[Circular]"
/** @internal */
export function formatDate(date: Date): string {
try {
return date.toISOString()
} catch {
return "Invalid Date"
}
}
function safeToString(input: any): string {
try {
const s = input.toString()
return typeof s === "string" ? s : String(s)
} catch {
return "[toString threw]"
}
}
/** @internal */
export function formatPropertyKey(name: PropertyKey): string {
return Predicate.isString(name) ? JSON.stringify(name) : String(name)
}
/** @internal */
export function formatUnknown(
input: unknown,
options?: {
readonly space?: number | string | undefined
readonly ignoreToString?: boolean | undefined
}
): string {
const space = options?.space ?? 0
const seen = new WeakSet<object>()
const gap = !space ? "" : (Predicate.isNumber(space) ? " ".repeat(space) : space)
const ind = (d: number) => gap.repeat(d)
const wrap = (v: unknown, body: string): string => {
const ctor = (v as any)?.constructor
return ctor && ctor !== Object.prototype.constructor && ctor.name ? `${ctor.name}(${body})` : body
}
const ownKeys = (o: object): Array<PropertyKey> => {
try {
return Reflect.ownKeys(o)
} catch {
return ["[ownKeys threw]"]
}
}
function go(v: unknown, d = 0): string {
if (Array.isArray(v)) {
if (seen.has(v)) return CIRCULAR
seen.add(v)
if (!gap || v.length <= 1) return `[${v.map((x) => go(x, d)).join(",")}]`
const inner = v.map((x) => go(x, d + 1)).join(",\n" + ind(d + 1))
return `[\n${ind(d + 1)}${inner}\n${ind(d)}]`
}
if (Predicate.isDate(v)) return formatDate(v)
if (
!options?.ignoreToString &&
Predicate.hasProperty(v, "toString") &&
Predicate.isFunction(v["toString"]) &&
v["toString"] !== Object.prototype.toString &&
v["toString"] !== Array.prototype.toString
) {
const s = safeToString(v)
if (v instanceof Error && v.cause) {
return `${s} (cause: ${go(v.cause, d)})`
}
return s
}
if (Predicate.isString(v)) return JSON.stringify(v)
if (
Predicate.isNumber(v) ||
v == null ||
Predicate.isBoolean(v) ||
Predicate.isSymbol(v)
) return String(v)
if (Predicate.isBigInt(v)) return String(v) + "n"
if (v instanceof Set || v instanceof Map) {
if (seen.has(v)) return CIRCULAR
seen.add(v)
return `${v.constructor.name}(${go(Array.from(v), d)})`
}
if (Predicate.isObject(v)) {
if (seen.has(v)) return CIRCULAR
seen.add(v)
const keys = ownKeys(v)
if (!gap || keys.length <= 1) {
const body = `{${keys.map((k) => `${formatPropertyKey(k)}:${go((v as any)[k], d)}`).join(",")}}`
return wrap(v, body)
}
const body = `{\n${
keys.map((k) => `${ind(d + 1)}${formatPropertyKey(k)}: ${go((v as any)[k], d + 1)}`).join(",\n")
}\n${ind(d)}}`
return wrap(v, body)
}
return String(v)
}
return go(input, 0)
}
/**
* @since 2.0.0
*/
export const format = (x: unknown): string => JSON.stringify(x, null, 2)
/**
* @since 2.0.0
*/
export const BaseProto: Inspectable = {
toJSON() {
return toJSON(this)
},
[NodeInspectSymbol]() {
return this.toJSON()
},
toString() {
return format(this.toJSON())
}
}
/**
* @since 2.0.0
*/
export abstract class Class {
/**
* @since 2.0.0
*/
abstract toJSON(): unknown
/**
* @since 2.0.0
*/
[NodeInspectSymbol]() {
return this.toJSON()
}
/**
* @since 2.0.0
*/
toString() {
return format(this.toJSON())
}
}
/**
* @since 2.0.0
*/
export const toStringUnknown = (u: unknown, whitespace: number | string | undefined = 2): string => {
if (typeof u === "string") {
return u
}
try {
return typeof u === "object" ? stringifyCircular(u, whitespace) : String(u)
} catch {
return String(u)
}
}
/**
* @since 2.0.0
*/
export const stringifyCircular = (obj: unknown, whitespace?: number | string | undefined): string => {
let cache: Array<unknown> = []
const retVal = JSON.stringify(
obj,
(_key, value) =>
typeof value === "object" && value !== null
? cache.includes(value)
? undefined // circular reference
: cache.push(value) && (redactableState.fiberRefs !== undefined && isRedactable(value)
? value[symbolRedactable](redactableState.fiberRefs)
: value)
: value,
whitespace
)
;(cache as any) = undefined
return retVal
}
/**
* @since 3.10.0
* @category redactable
*/
export interface Redactable {
readonly [symbolRedactable]: (fiberRefs: FiberRefs.FiberRefs) => unknown
}
/**
* @since 3.10.0
* @category redactable
*/
export const symbolRedactable: unique symbol = Symbol.for("effect/Inspectable/Redactable")
/**
* @since 3.10.0
* @category redactable
*/
export const isRedactable = (u: unknown): u is Redactable =>
typeof u === "object" && u !== null && symbolRedactable in u
const redactableState = globalValue("effect/Inspectable/redactableState", () => ({
fiberRefs: undefined as FiberRefs.FiberRefs | undefined
}))
/**
* @since 3.10.0
* @category redactable
*/
export const withRedactableContext = <A>(context: FiberRefs.FiberRefs, f: () => A): A => {
const prev = redactableState.fiberRefs
redactableState.fiberRefs = context
try {
return f()
} finally {
redactableState.fiberRefs = prev
}
}
/**
* @since 3.10.0
* @category redactable
*/
export const redact = (u: unknown): unknown => {
if (isRedactable(u) && redactableState.fiberRefs !== undefined) {
return u[symbolRedactable](redactableState.fiberRefs)
}
return u
}