1/**
2 * Controls how values appear in logs and debugging output.
3 *
4 * Effect data types use `Inspectable` to provide stable string, JSON, and
5 * Node.js inspection output. This keeps custom values readable in logs, REPLs,
6 * test failures, and diagnostics. This module defines the Node inspect symbol,
7 * the `Inspectable` interface, safe conversion helpers, and shared prototype or
8 * class implementations for custom values.
9 *
10 * @since 2.0.0
11 */
12import { format, formatJson } from "./Formatter.ts"
13import * as Predicate from "./Predicate.ts"
14import { redact } from "./Redactable.ts"
15
16/**
17 * Defines the symbol used by Node.js for custom object inspection.
18 *
19 * **When to use**
20 *
21 * Use to implement Node.js custom inspection for a value.
22 *
23 * **Details**
24 *
25 * This symbol is recognized by Node.js's `util.inspect()` function and the REPL
26 * for custom object representation. When an object has a method with this symbol,
27 * it will be called to determine how the object should be displayed.
28 *
29 * **Example** (Defining custom Node inspection)
30 *
31 * ```ts import.meta.vitest
32 * import { Inspectable } from "effect"
33 *
34 * class CustomObject {
35 * constructor(private value: string) {}
36 *
37 * [Inspectable.NodeInspectSymbol]() {
38 * return `CustomObject(${this.value})`
39 * }
40 * }
41 *
42 * const obj = new CustomObject("hello")
43 * obj[Inspectable.NodeInspectSymbol]() // => "CustomObject(hello)"
44 * ```
45 *
46 * @category symbols
47 * @since 2.0.0
48 */
49export const NodeInspectSymbol = Symbol.for("nodejs.util.inspect.custom")
50
51/**
52 * The type of the Node.js inspection symbol used for custom object inspection.
53 * This symbol type is used to implement custom inspection behavior in Node.js
54 * environments.
55 *
56 * **When to use**
57 *
58 * Use to type methods keyed by the Node.js custom inspection symbol.
59 *
60 * **Example** (Typing custom Node inspection)
61 *
62 * ```ts import.meta.vitest
63 * import { Inspectable } from "effect"
64 *
65 * class CustomObject {
66 * constructor(private value: string) {}
67 *
68 * [Inspectable.NodeInspectSymbol]() {
69 * return `CustomObject(${this.value})`
70 * }
71 * }
72 *
73 * const obj = new CustomObject("test")
74 * obj[Inspectable.NodeInspectSymbol]() // => "CustomObject(test)"
75 * ```
76 *
77 * @category symbols
78 * @since 2.0.0
79 */
80export type NodeInspectSymbol = typeof NodeInspectSymbol
81
82/**
83 * Interface for objects that can be inspected and provide custom string representations.
84 *
85 * **When to use**
86 *
87 * Use to define values with custom string, JSON, and Node.js inspection output.
88 *
89 * **Details**
90 *
91 * Objects implementing this interface can control how they appear in debugging contexts,
92 * JSON serialization, and Node.js inspection. This is particularly useful for creating
93 * custom data types that display meaningful information during development.
94 *
95 * **Example** (Implementing inspectable objects)
96 *
97 * ```ts import.meta.vitest
98 * import { Formatter, Inspectable } from "effect"
99 *
100 * class Result implements Inspectable.Inspectable {
101 * constructor(
102 * private readonly tag: "Success" | "Failure",
103 * private readonly value: unknown
104 * ) {}
105 *
106 * toString(): string {
107 * return Formatter.format(this.toJSON())
108 * }
109 *
110 * toJSON() {
111 * return { _tag: this.tag, value: this.value }
112 * }
113 *
114 * [Inspectable.NodeInspectSymbol]() {
115 * return this.toJSON()
116 * }
117 * }
118 *
119 * const success = new Result("Success", 42)
120 * success.toString() // => "{\"_tag\":\"Success\",\"value\":42}"
121 * ```
122 *
123 * @category models
124 * @since 2.0.0
125 */
126export interface Inspectable {
127 toString(): string
128 toJSON(): unknown
129 [NodeInspectSymbol](): unknown
130}
131
132/**
133 * Converts a value to its structured inspection representation.
134 *
135 * **When to use**
136 *
137 * Use when you need the structured representation of an inspectable value
138 * without risking unhandled errors.
139 *
140 * **Details**
141 *
142 * This function applies redaction before extracting data from objects that
143 * implement `toJSON`, recursively processes arrays, and handles errors
144 * gracefully. Plain objects are returned unchanged, so the result is not
145 * guaranteed to be accepted by `JSON.stringify`; it may still contain values
146 * such as `BigInt`, functions, or circular references.
147 *
148 * @see {@link toStringUnknown} for converting unknown values to strings
149 *
150 * @category converting
151 * @since 4.0.0
152 */
153export const toJson = (input: unknown): unknown => {
154 try {
155 input = redact(input)
156 if (
157 Predicate.hasProperty(input, "toJSON") &&
158 Predicate.isFunction(input["toJSON"]) &&
159 input["toJSON"].length === 0
160 ) {
161 return input.toJSON()
162 } else if (Array.isArray(input)) {
163 return input.map(toJson)
164 }
165 return input
166 } catch {
167 return "[toJSON threw]"
168 }
169}
170
171/**
172 * Converts an unknown value to a string for diagnostics.
173 *
174 * **When to use**
175 *
176 * Use to produce a diagnostic string from a value whose runtime type is unknown.
177 *
178 * **Details**
179 *
180 * Strings are returned unchanged. Objects are formatted as JSON using the
181 * provided whitespace setting when possible, and values that cannot be
182 * formatted are converted with `String`.
183 *
184 * @category converting
185 * @since 2.0.0
186 */
187export const toStringUnknown = (u: unknown, whitespace: number | string | undefined = 2): string => {
188 if (typeof u === "string") {
189 return u
190 }
191 try {
192 return typeof u === "object" ? formatJson(u, { space: whitespace }) : format(u, { space: whitespace })
193 } catch {
194 return String(u)
195 }
196}
197
198/**
199 * A base prototype object that implements the {@link Inspectable} interface.
200 *
201 * **When to use**
202 *
203 * Use as a prototype for plain objects that should share standard inspectable behavior.
204 *
205 * **Details**
206 *
207 * This object provides default implementations for the {@link Inspectable} methods.
208 * It can be used as a prototype for objects that want to be inspectable,
209 * or as a mixin to add inspection capabilities to existing objects.
210 *
211 * **Example** (Using the base inspectable prototype)
212 *
213 * ```ts import.meta.vitest
214 * import { Inspectable } from "effect"
215 *
216 * // Use as prototype
217 * const myObject = Object.create(Inspectable.BaseProto)
218 * myObject.name = "example"
219 * myObject.value = 42
220 *
221 * myObject.toString() // => "\"[toJSON threw]\""
222 *
223 * // Or extend in a constructor
224 * function MyClass(this: any, name: string) {
225 * this.name = name
226 * }
227 * MyClass.prototype = Object.create(Inspectable.BaseProto)
228 * MyClass.prototype.constructor = MyClass
229 * ```
230 *
231 * @category prototypes
232 * @since 2.0.0
233 */
234export const BaseProto: Inspectable = {
235 toJSON() {
236 return toJson(this)
237 },
238 [NodeInspectSymbol]() {
239 return this.toJSON()
240 },
241 toString() {
242 return format(this.toJSON())
243 }
244}
245
246/**
247 * Provides an abstract base class that implements the Inspectable interface.
248 *
249 * **When to use**
250 *
251 * Use as a base class for inspectable objects that define their own JSON representation.
252 *
253 * **Details**
254 *
255 * This class provides a convenient way to create inspectable objects by extending it.
256 * Subclasses only need to implement the `toJSON()` method, and they automatically
257 * get proper `toString()` and Node.js inspection support.
258 *
259 * **Example** (Extending the inspectable base class)
260 *
261 * ```ts import.meta.vitest
262 * import { Inspectable } from "effect"
263 *
264 * class User extends Inspectable.Class {
265 * constructor(
266 * public readonly id: number,
267 * public readonly name: string,
268 * public readonly email: string
269 * ) {
270 * super()
271 * }
272 *
273 * toJSON() {
274 * return {
275 * _tag: "User",
276 * id: this.id,
277 * name: this.name,
278 * email: this.email
279 * }
280 * }
281 * }
282 *
283 * const user = new User(1, "Alice", "alice@example.com")
284 * user.toString() // => "{\"_tag\":\"User\",\"id\":1,\"name\":\"Alice\",\"email\":\"alice@example.com\"}"
285 * user[Inspectable.NodeInspectSymbol]() // => { _tag: "User", id: 1, name: "Alice", email: "alice@example.com" }
286 * ```
287 *
288 * @category models
289 * @since 2.0.0
290 */
291export abstract class Class {
292 /**
293 * Returns a JSON representation of this object.
294 *
295 * **When to use**
296 *
297 * Use to provide the JSON representation consumed by inherited inspection methods.
298 *
299 * **Details**
300 *
301 * Subclasses must implement this method to define how the object
302 * should be serialized for debugging and inspection purposes.
303 *
304 * @since 2.0.0
305 */
306 abstract toJSON(): unknown
307 /**
308 * Node.js custom inspection method.
309 *
310 * **When to use**
311 *
312 * Use to expose the class JSON representation to Node.js inspection.
313 *
314 * @since 2.0.0
315 */
316 [NodeInspectSymbol]() {
317 return this.toJSON()
318 }
319 /**
320 * Returns a formatted string representation of this object.
321 *
322 * **When to use**
323 *
324 * Use to format the class JSON representation as a string.
325 *
326 * @since 2.0.0
327 */
328 toString() {
329 return format(this.toJSON())
330 }
331}