288 lines
6.4 KiB
TypeScript
Executable File
288 lines
6.4 KiB
TypeScript
Executable File
/**
|
|
* @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
|
|
}
|