From 4f2e1a13e71a9c888947092245bef7150c9ed9f1 Mon Sep 17 00:00:00 2001 From: "Shahid N. Shah" Date: Wed, 21 Aug 2024 21:34:10 -0400 Subject: [PATCH] feat: introduce callable model --- lib/reflect/README.md | 473 +++++++++++++++++++++++++++++++++++ lib/reflect/callable.ts | 185 ++++++++++++++ lib/reflect/callable_test.ts | 214 ++++++++++++++++ 3 files changed, 872 insertions(+) create mode 100644 lib/reflect/README.md create mode 100644 lib/reflect/callable.ts create mode 100644 lib/reflect/callable_test.ts diff --git a/lib/reflect/README.md b/lib/reflect/README.md new file mode 100644 index 00000000..631c60e3 --- /dev/null +++ b/lib/reflect/README.md @@ -0,0 +1,473 @@ +# TypeScript and JavaScript runtime reflection module + +- The `callable.ts` module provides utility functions for working with objects + and class instances in TypeScript, allowing you to easily identify, filter, + and execute methods on these instances. This can be particularly useful in + environments where dynamic execution of methods is needed, such as in a + Jupyter or similar notebook-style setting. +- The `reflect.ts` module is designed to inspect and analyze the types of + variables at runtime. It provides a utility function, `reflect`, that + generates detailed information about the type and structure of a given + variable. This can be especially useful for debugging, logging, or dynamically + processing various data types. +- The `ts-content.ts` module is designed to dynamically generate TypeScript type + definitions and constant arrays based on the structure of provided row data. + The core function of this module is `toTypeScriptCode`, which processes data + rows to infer types, build TypeScript types, and output a constant that + represents the data. +- The `value.ts` module is designed to handle the detection and transformation + of various data types within streams of data. A key component of this module + is the `detectedValueNature` function, which automatically determines the type + of a given value and provides methods to convert and represent that value as a + TypeScript type. + +## `callable.ts` + +- **Identify Methods:** Extracts the identifiers of callable methods from an + object or class instance. +- **Filter Methods:** Provides a flexible filtering mechanism to include or + exclude methods based on names, regular expressions, or custom functions. +- **Execute Methods:** Returns callable interfaces that allow you to dynamically + invoke methods with proper context handling for class instances. + +### `callables(instance: T)` + +Returns method names ("identifiers"), type information, and a filter function +for the provided instance. + +- **Parameters:** + - `instance`: The object or class instance to analyze. + +- **Returns:** An object containing: + - `identifiers`: An array of method names (filtered based on the return type + `R` if specified). + - `target`: The target object or prototype from which the method names were + extracted. + - `nature`: An enum value indicating if the instance is an object or a class. + - `filter`: A function that filters and returns methods based on `include` and + `exclude` criteria, with a callable interface that handles `this` context + for class methods. + +- **Throws:** If the instance is not an object or a constructed class instance. + +## Example Usage + +### Notebook-Style Environment + +In a notebook-style environment, each class or object can be treated as a +_notebook_, and each method within it as a _cell_ that can be executed +dynamically. + +```typescript +// Define a class representing a notebook +class Notebook { + cell1() { + return "This is the first cell"; + } + + cell2(data: string) { + return `Processing: ${data}`; + } + + helper() { + return "This is a helper method"; + } +} + +// Create an instance of the notebook +const notebook = new Notebook(); + +// Analyze the notebook with callables +const result = callables(notebook); + +// Display the method identifiers (cells) +console.log(result.identifiers); // Output: ["cell1", "cell2", "helper"] + +// Execute a specific cell +const cell1Result = result.filter({ include: "cell1" })[0].call(); +console.log(cell1Result); // Output: "This is the first cell" + +// Execute another cell with arguments +const cell2Result = result.filter({ include: "cell2" })[0].call("my data"); +console.log(cell2Result); // Output: "Processing: my data" + +// Exclude helper methods and execute only cells +const cells = result.filter({ include: (name) => name.startsWith("cell") }); +cells.forEach((cell) => { + console.log(`Executing ${cell.callable}:`, cell.call()); +}); +``` + +## `reflect.ts`: + +### 1. Type Checking Functions: + +The module includes several helper functions (`isBoolean`, `isNull`, +`isUndefined`, etc.) that determine the type of a given variable. These +functions are used within `reflect` to categorize the input correctly. + +### 2. Type Definitions: + +- **PrimitiveType:** Represents basic primitive types in JavaScript like + `boolean`, `number`, `string`, etc. +- **NonPrimitiveType:** Represents more complex types such as `Date`, `RegExp`, + and collections like `Map` and `Set`. +- **TypeInfo:** A union type that can represent a primitive, function, or + object, with detailed type-specific information. + +### 3. Type Information Interfaces: + +These interfaces define the structure of the data returned by `reflect`. +Depending on the type of the input variable, `reflect` returns an object +conforming to one of these interfaces: + +- **PrimitiveTypeInfo:** Describes primitive types. +- **FunctionTypeInfo:** Extends `PrimitiveTypeInfo` with function-specific + details. +- **ObjectTypeInfo:** Describes objects, including their properties and symbols. + +## The `reflect` Function: + +The `reflect` function is the core utility of the module. It inspects the input +variable and returns a detailed description of its type, structure, and content. +The function handles different types (primitives, functions, objects) uniquely: + +- **For Primitive Types:** The function checks if the input is a primitive type + (e.g., `boolean`, `string`, `symbol`) and returns a `PrimitiveTypeInfo` object + containing the type and value. + +- **For Functions:** If the input is a function, `reflect` returns a + `FunctionTypeInfo` object with additional details such as the function's name + and stringified representation. + +- **For Objects:** When the input is an object, `reflect` recursively inspects + each property and symbol, returning an `ObjectTypeInfo` object that includes a + detailed breakdown of the object's structure. + +### Example of Using the `reflect` Function: + +Let's walk through an example to demonstrate how to use the `reflect` function: + +```typescript +import { reflect } from "./reflect.ts"; + +// Example object with various types of properties +const exampleObject = { + propString: "Hello, World!", + propNumber: 42, + propBoolean: true, + propSymbol: Symbol("example"), + propFunction: () => "I'm a function", + nestedObject: { + nestedProp: "Nested value", + }, + propArray: [1, 2, 3], +}; + +// Use the reflect function to inspect the object +const reflection = reflect(exampleObject); + +// Print the detailed type information +console.log(JSON.stringify(reflection, null, 2)); +``` + +### Explanation of the Output: + +When you run this code, the `reflect` function will generate and log a detailed +JSON object that describes the structure and types of `exampleObject`. The +output will include: + +- The **type** of each property (e.g., `"string"`, `"number"`, `"boolean"`). +- The **value** of each primitive property. +- For functions, the **name** and **stringified representation** of the + function. +- For nested objects and arrays, a recursive breakdown of their structure. +- Descriptions of **symbols** and their associated values. + +Here's a snippet of what the output might look like: + +```json +{ + "value": { + "propString": "Hello, World!", + "propNumber": 42, + "propBoolean": true, + "propSymbol": "Symbol(example)", + "propFunction": "() => "I'm a function"", + "nestedObject": { + "nestedProp": "Nested value" + }, + "propArray": [ + 1, + 2, + 3 + ] + }, + "type": "object", + "properties": [ + { + "value": "Hello, World!", + "type": "string", + "key": "propString", + "propertyDescription": { + "configurable": true, + "enumerable": true, + "writable": true, + "value": "Hello, World!" + } + }, + ... + ], + "symbols": [] +} +``` + +The `reflect` function in the `reflect.ts` Deno module is a powerful tool for +inspecting and understanding the structure and types of variables at runtime. It +can handle a wide range of types, from simple primitives to complex objects, and +provides detailed information that can be used for debugging, logging, or +dynamic processing. By using this function, you can gain insights into the +nature of your data in a structured and consistent manner. + +## `ts-content.ts` + +### Function Signature + +```typescript +export async function toTypeScriptCode( + rows: AsyncIterable | Iterable, + options?: { + readonly valueNature?: (index: number, sample?: string) => ValueNature; + readonly tsTypePropName?: (index: number) => string; + readonly propertyNames?: string[]; + readonly rowTypeName?: string; + readonly rowsConstName?: string; + }, +); +``` + +### Parameters: + +1. **`rows`**: An iterable or async iterable that yields arrays of strings, + where each array represents a row of data. The first row is typically + considered the header, and subsequent rows contain the actual data. + +2. **`options`** (optional): An object allowing customization: + - **`valueNature`**: A function to detect the nature (type) of each value. By + default, it uses `detectedValueNature`. + - **`tsTypePropName`**: A function to determine property names for the + TypeScript type. By default, it uses the inferred or provided property + names. + - **`propertyNames`**: An array of strings representing property names. If + not provided, they are inferred from the first row of the data. + - **`rowTypeName`**: The name for the TypeScript type to be generated. + Defaults to `"Row"`. + - **`rowsConstName`**: The name for the TypeScript constant that will hold + the array of objects. Defaults to `"rows"`. + +### Functionality: + +1. **Type and Property Name Detection**: + - If `propertyNames` are not provided, the first row of data is assumed to + contain them. + - The `valueNature` function is used to detect the type of each value in the + first non-header row. This information is stored and later used to generate + the TypeScript type definition. + +2. **Row Processing**: + - The function iterates through each row, updating the types for union values + (e.g., strings that can have multiple predefined values) and accumulating + these into a TypeScript union type. + - It also constructs the content of a TypeScript constant, representing the + data as an array of objects. + +3. **Output Generation**: + - The function generates a TypeScript type definition (`rowType`) based on + the detected types. + - It constructs a TypeScript constant (`rowsConst`) that holds the data rows + as objects typed with the generated type. + +4. **Return Value**: + - The function returns an object containing the TypeScript type definition + (`rowType`) and the data constant (`rowsConst`). + +## Example Usage + +Here’s an example of how to use the `toTypeScriptCode` function: + +```typescript +import { toTypeScriptCode } from "./ts-content.ts"; + +async function* rowsGenerator() { + yield ["name", "color", "isActive", "birthdate", "type"]; + yield ["John", "{Red}", "true", "1998-05-12", "undefined"]; + yield ["Doe", "{Green}", "false", "March 15, 1993", "something else"]; +} + +const result = await toTypeScriptCode( + rowsGenerator(), + { + rowTypeName: "Person", // Custom name for the TypeScript type + rowsConstName: "people", // Custom name for the constant array + valueNature: (index, value) => { + // Custom type handling for specific columns + if (index == 4) { + return { + nature: "custom", + emitTsType: () => `string | undefined`, + emitTsValue: (value) => + value.trim().length == 0 || value == "undefined" + ? "undefined" + : `"${value}"`, + transform: (value) => + value.trim().length == 0 || value == "undefined" + ? undefined + : value, + }; + } + return detectedValueNature(value); + }, + }, +); + +console.log(result.rowType); +console.log(result.rowsConst); +``` + +### Output: + +The `result` object will contain the following: + +1. **TypeScript Type Definition:** + +```typescript +type Person = { + name: string; + color: "Red" | "Green"; + isActive: boolean; + birthdate: Date; + type: string | undefined; +}; +``` + +2. **TypeScript Constant Array:** + +```typescript +const people: Person[] = [ + { + name: "John", + color: "Red", + isActive: true, + birthdate: Date.parse("1998-05-12"), + type: undefined, + }, + { + name: "Doe", + color: "Green", + isActive: false, + birthdate: Date.parse("March 15, 1993"), + type: "something else", + }, +]; +``` + +The `toTypeScriptCode` function in the `ts-content.ts` module automates the +generation of TypeScript type definitions and constant arrays from row-based +data. This is particularly useful in scenarios where the structure of the data +is dynamic or not predefined, ensuring type safety in your TypeScript code. By +providing flexible options for type detection and naming, the function can be +tailored to various use cases, making it a powerful tool for TypeScript +development. + +## `value.ts` + +The `detectedValueNature` function is the core utility that determines the +"nature" or type of a value based on its string representation. It analyzes the +input string and returns an object with methods to: + +- **Transform** the value into its corresponding JavaScript type (e.g., number, + boolean, Date, etc.). +- **Emit TypeScript type** information for the value. +- **Emit TypeScript value** representation for the value. + +The function supports various data types, including: + +- **Undefined**: If the input is undefined or an empty string, it returns a + `ValueNature` object representing `undefined`. +- **Number**: If the input can be parsed as a number, it returns a `ValueNature` + object representing a `number`. +- **Boolean**: If the input matches common boolean values (e.g., "true", + "false", "on", "off", "yes", "no"), it returns a `ValueNature` object + representing a `boolean`. +- **Date**: If the input can be parsed as a valid date, it returns a + `ValueNature` object representing a `Date`. +- **BigInt**: If the input matches the pattern for a BigInt (e.g., "123n"), it + returns a `ValueNature` object representing a `bigint`. +- **Union**: If the input appears to be a union type (e.g., "{Red}"), it returns + a `ValueNature` object representing a `union`. +- **String**: If none of the above conditions are met, it defaults to returning + a `ValueNature` object representing a `string`. + +## Example Usage of `detectedValueNature` + +Here's an example of how you can use the `detectedValueNature` function to +automatically detect and transform a series of string values: + +```typescript +import { detectedValueNature } from "./value.ts"; + +const sampleValues = ["true", "123", "2022-01-01", "42n", "{Red}", "Hello, +World!"]; + +const detectedNatures = sampleValues.map(detectedValueNature); + +detectedNatures.forEach((vn, index) => { const originalValue = +sampleValues[index]; const transformedValue = vn.transform(originalValue, vn); +const tsType = vn.emitTsType(vn); const tsValue = vn.emitTsValue(originalValue, +vn); + +console.log(`Original Value: ${originalValue}`); +console.log(`Detected Nature: ${vn.nature}`); +console.log(`Transformed Value: ${transformedValue}`); +console.log(`TypeScript Type: ${tsType}`); +console.log(`TypeScript Value: ${tsValue}`); +console.log("--------"); }); +``` + +## Example Output: + +When you run the above code, it will output something like this: + +``` +## Original Value: true Detected Nature: boolean Transformed Value: true TypeScript Type: boolean TypeScript Value: true + +## Original Value: 123 Detected Nature: number Transformed Value: 123 TypeScript Type: number TypeScript Value: 123 + +## Original Value: 2022-01-01 Detected Nature: Date Transformed Value: 1640995200000 TypeScript Type: Date TypeScript Value: Date.parse("2022-01-01") + +## Original Value: 42n Detected Nature: bigint Transformed Value: 42n TypeScript Type: bigint TypeScript Value: 42n + +## Original Value: {Red} Detected Nature: union Transformed Value: Red TypeScript Type: Red TypeScript Value: "Red" + +## Original Value: Hello, World! Detected Nature: string Transformed Value: Hello, World! TypeScript Type: string TypeScript Value: "Hello, World!" +``` + +## Explanation of the Example: + +- **Boolean**: The input "true" is detected as a boolean, transformed into the + JavaScript `true`, and emitted as `boolean` in TypeScript. +- **Number**: The input "123" is detected as a number, transformed into the + number `123`, and emitted as `number` in TypeScript. +- **Date**: The input "2022-01-01" is detected as a date, transformed into a + timestamp, and emitted as a Date constructor in TypeScript. +- **BigInt**: The input "42n" is detected as a BigInt, transformed accordingly, + and emitted as `42n` in TypeScript. +- **Union**: The input "{Red}" is treated as a union type, with the curly braces + removed and the value treated as a string type. +- **String**: The input "Hello, World!" defaults to a string type, with + TypeScript representations using double quotes. + +This example illustrates how `detectedValueNature` can simplify the process of +analyzing and converting string data into the appropriate types and +representations in TypeScript, making it easier to work with dynamic or loosely +typed data in a TypeScript environment. diff --git a/lib/reflect/callable.ts b/lib/reflect/callable.ts new file mode 100644 index 00000000..34190272 --- /dev/null +++ b/lib/reflect/callable.ts @@ -0,0 +1,185 @@ +/** + * Enum representing the nature of the instance being analyzed. + */ +export enum CallablesNature { + OBJECT = "OBJECT", + CLASS = "CLASS", +} + +/** + * Extracts the identifiers of callable methods in a given object or class type `T` + * that return a specific type `R`. + * + * @template T - The type of the object or class instance. + * @template R - The return type of the methods (not enforceable, merely informative). + */ +export type CallablesOfType = { + [K in keyof T]: T[K] extends (...args: unknown[]) => R ? K : never; +}[keyof T]; + +/** + * Type representing the different ways identifier of callable methods in a given object + * or class type `T` that return a specific type `R`. + * + * @template T - The type of the object or class instance. + * @template R - The return type of the methods (not enforceable, merely informative). + */ +export type CallableFilterPattern = + | string + | RegExp + | ((callable: CallablesOfType) => boolean); + +/** + * Identifiers, nature, and filter for callable methods in a given object + * or class type `T` that return a specific type `R`. + */ +export type Callables = ReturnType>; + +/** + * Returns method names ("identifiers"), type information, and a filter function for the provided instance. + * + * @template T - The type of the object or class definition. + * @template R - The return type of the methods (not enforceable, merely informative). + * @param instance - The object or class instance to analyze. + * @returns An object containing: + * - `identifiers`: An array of method names (filtered based on the return type `R` if specified). + * - `searched`: The target object or prototype from which the method names were extracted. + * - `instance`: The object or class instance analyzed. + * - `nature`: An enum value indicating if the instance is an object or a class. + * - `filter`: A function that filters and returns methods based on `include` and `exclude` criteria, with a callable interface that handles `this` context for class methods. + * @throws If the instance is not an object or a constructed class instance. + * + * @example + * class MyClass { + * foo() { return 42; } + * bar() { return "hello"; } + * baz = "not a method"; + * } + * + * const result = callables(new MyClass()); + * // Result: { + * // identifiers: ["foo", "bar"], + * // instance: new MyClass(), + * // searched: MyClass.prototype, + * // nature: CallablesNature.CLASS, + * // filter: [Function: filter] + * // } + */ +export function callables(instance: T) { + if ( + typeof instance !== "object" || instance === null || Array.isArray(instance) + ) { + throw new TypeError( + "The provided instance must be an object or a constructed class instance.", + ); + } + + const nature = Object.getPrototypeOf(instance) !== Object.prototype + ? CallablesNature.CLASS + : CallablesNature.OBJECT; + + const searched = + (nature === CallablesNature.CLASS + ? Object.getPrototypeOf(instance) + : instance) as T; + + const identifiers = (Object.getOwnPropertyNames(searched) as (keyof T)[]) + .filter((name) => { + return typeof searched[name] === "function" && name !== "constructor"; + }) as CallablesOfType[]; + + const filter = ( + options?: { + include?: CallableFilterPattern | CallableFilterPattern[]; + exclude?: CallableFilterPattern | CallableFilterPattern[]; + }, + ) => { + const include = options?.include + ? (Array.isArray(options.include) ? options.include : [options.include]) + : undefined; + const exclude = options?.exclude + ? (Array.isArray(options.exclude) ? options.exclude : [options.exclude]) + : undefined; + + return identifiers + .filter((nameSupplied) => { + const name = String(nameSupplied); + if (include && include.length > 0) { + const includeMatch = include.some((pattern) => { + if (typeof pattern === "string" && name.includes(pattern)) { + return true; + } + if (pattern instanceof RegExp && pattern.test(name)) return true; + if (typeof pattern === "function" && pattern(nameSupplied)) { + return true; + } + return false; + }); + if (!includeMatch) return false; + } + + if (exclude && exclude.length > 0) { + const excludeMatch = exclude.some((pattern) => { + if (typeof pattern === "string" && name.includes(pattern)) { + return true; + } + if (pattern instanceof RegExp && pattern.test(name)) return true; + if (typeof pattern === "function" && pattern(nameSupplied)) { + return true; + } + return false; + }); + if (excludeMatch) return false; + } + return true; + }) + .map((name) => ({ + source: { + // if you modify this list, update `return` value below, too + identifiers, + searched, + instance, + nature, + filter, + }, + callable: name, + call: (...args: unknown[]) => { + const method = searched[name] as unknown as (...args: unknown[]) => R; + if (nature === CallablesNature.CLASS) { + return method.apply(instance, args); + } + return method(...args); + }, + })); + }; + + return { + // if you modify this list, update `source` above, too + identifiers, + searched, + instance, + nature, + filter, + }; +} + +/** + * Analyzes multiple instances and returns their callables with a collective filter function. + * + * @template T - The type of the objects or class instances. + * @param instances - The array of objects or class instances to analyze. + * @returns An object containing: + * - `callables`: An array of callables from each instance. + * - `filter`: A function that filters and returns callables across all instances. + */ +export function callablesCollection(...instances: T[]) { + const callablesList = instances.map((instance) => callables(instance)); + const filter = (options?: Parameters["filter"]>[0]) => { + return callablesList.flatMap((c) => c.filter(options)); + }; + + return { + callables: callablesList, + filter, + }; +} diff --git a/lib/reflect/callable_test.ts b/lib/reflect/callable_test.ts new file mode 100644 index 00000000..d66a38ec --- /dev/null +++ b/lib/reflect/callable_test.ts @@ -0,0 +1,214 @@ +import { testingAsserts as ta } from "./deps-test.ts"; +import { callables, callablesCollection, CallablesNature } from "./callable.ts"; + +// Test class for testing callables +class TestClass { + private instanceVar = "instanceVar"; + + foo() { + return 42; + } + bar(message: string) { + return `Hello, ${message} (${this.instanceVar}Value)`; + } + baz = "not a method"; +} + +// Test object for testing callables +const testObject = { + greet: (name: string) => `Hi, ${name}`, + add: (a: number, b: number) => a + b, + description: "I am an object", +}; + +Deno.test("callables - should return method names for a class instance", () => { + const instance = new TestClass(); + const result = callables(instance); + + ta.assertEquals(result.identifiers, ["foo", "bar"]); + ta.assertEquals(result.nature, CallablesNature.CLASS); +}); + +Deno.test("callables - should return method names for an object", () => { + const result = callables(testObject); + + ta.assertEquals(result.identifiers, ["greet", "add"]); + ta.assertEquals(result.nature, CallablesNature.OBJECT); +}); + +Deno.test("callables - should filter methods based on include and exclude criteria", () => { + const instance = new TestClass(); + const result = callables(instance); + + let filteredMethods = result.filter({ + include: (name) => name.startsWith("f"), // Include methods starting with "f" + exclude: "bar", // Exclude method named "bar" + }); + + ta.assertEquals(filteredMethods.length, 1); + ta.assertEquals(filteredMethods[0].callable, "foo"); + ta.assertEquals(filteredMethods[0].call(), 42); + + filteredMethods = result.filter({ include: "bar" }); + + ta.assertEquals(filteredMethods.length, 1); + ta.assertEquals(filteredMethods[0].callable, "bar"); + ta.assertEquals( + filteredMethods[0].call("bar"), + "Hello, bar (instanceVarValue)", + ); +}); + +Deno.test("callables - should handle standalone object functions correctly", () => { + const result = callables(testObject); + + const filteredMethods = result.filter(); + + ta.assertEquals(filteredMethods.length, 2); + ta.assertEquals(filteredMethods[0].callable, "greet"); + ta.assertEquals(filteredMethods[0].call("Alice"), "Hi, Alice"); + + ta.assertEquals(filteredMethods[1].callable, "add"); + ta.assertEquals(filteredMethods[1].call(2, 3), 5); +}); + +Deno.test("callables - should throw an error for invalid instance types", () => { + ta.assertThrows( + () => { + callables(null); + }, + TypeError, + "The provided instance must be an object or a constructed class instance.", + ); + + ta.assertThrows( + () => { + callables("not an object"); + }, + TypeError, + "The provided instance must be an object or a constructed class instance.", + ); + + ta.assertThrows( + () => { + callables([1, 2, 3]); + }, + TypeError, + "The provided instance must be an object or a constructed class instance.", + ); +}); + +Deno.test("callables - should handle filtering by string, RegExp, and function", () => { + const instance = new TestClass(); + const result = callables(instance); + + // Filtering by string + const byString = result.filter({ include: "foo" }); + ta.assertEquals(byString.length, 1); + ta.assertEquals(byString[0].callable, "foo"); + + // Filtering by RegExp + const byRegExp = result.filter({ include: /ba/ }); + ta.assertEquals(byRegExp.length, 1); + ta.assertEquals(byRegExp[0].callable, "bar"); + + // Filtering by function + const byFunction = result.filter({ include: (name) => name.length === 3 }); + ta.assertEquals(byFunction.length, 2); + ta.assertEquals(byFunction[0].callable, "foo"); + ta.assertEquals(byFunction[1].callable, "bar"); +}); + +Deno.test("callables - should handle complex object with nested functions", () => { + const complexObject = { + level1: { + greet: () => "Hello from level 1", + level2: { + sayHi: () => "Hi from level 2", + }, + }, + describe: () => "I am a complex object", + }; + + const result = callables(complexObject); + + const filteredMethods = result.filter(); + + ta.assertEquals(filteredMethods.length, 1); + ta.assertEquals(filteredMethods[0].callable, "describe"); + ta.assertEquals(filteredMethods[0].call(), "I am a complex object"); +}); + +Deno.test("callables - should ensure `this` context is correctly handled in class methods", () => { + class ThisContextClass { + name = "Deno"; + + getName() { + return this.name; + } + } + + const instance = new ThisContextClass(); + const result = callables(instance); + + const filteredMethods = result.filter(); + + ta.assertEquals(filteredMethods.length, 1); + ta.assertEquals(filteredMethods[0].callable, "getName"); + ta.assertEquals(filteredMethods[0].call(), "Deno"); +}); + +Deno.test("callablesCollection - should return combined callables and allow filtering across multiple instances", () => { + class Notebook1 { + cell1() { + return "Notebook 1 - Cell 1"; + } + cell2() { + return "Notebook 1 - Cell 2"; + } + } + + class Notebook2 { + cell3() { + return "Notebook 2 - Cell 3"; + } + cell4() { + return "Notebook 2 - Cell 4"; + } + } + + const notebook1 = new Notebook1(); + const notebook2 = new Notebook2(); + + const collectionResult = callablesCollection( + notebook1, + notebook2, + ); + + // Assert that callables are identified correctly for each notebook + ta.assertEquals(collectionResult.callables.length, 2); + ta.assertEquals(collectionResult.callables[0].identifiers, [ + "cell1", + "cell2", + ]); + ta.assertEquals(collectionResult.callables[1].identifiers, [ + "cell3", + "cell4", + ]); + + // Filter callables across both notebooks using a RegExp + const filtered = collectionResult.filter({ include: /cell/ }); + + // Assert the filtered result contains the correct callable names + ta.assertEquals(filtered.length, 4); + ta.assertEquals(filtered[0].callable, "cell1"); + ta.assertEquals(filtered[1].callable, "cell2"); + ta.assertEquals(filtered[2].callable, "cell3"); + ta.assertEquals(filtered[3].callable, "cell4"); + + // Assert the callable methods return the expected results + ta.assertEquals(filtered[0].call(), "Notebook 1 - Cell 1"); + ta.assertEquals(filtered[1].call(), "Notebook 1 - Cell 2"); + ta.assertEquals(filtered[2].call(), "Notebook 2 - Cell 3"); + ta.assertEquals(filtered[3].call(), "Notebook 2 - Cell 4"); +});