feat(codemode): add groupBy parity (#37992)
This commit is contained in:
parent
17c1e9b083
commit
0c18d06c9f
7 changed files with 289 additions and 9 deletions
|
|
@ -12,7 +12,7 @@ import {
|
|||
PromiseNamespace,
|
||||
UriFunction,
|
||||
} from "./model.js"
|
||||
import { containsOpaqueReference, rejectCircularInsertion, typeofValue } from "./references.js"
|
||||
import { containsOpaqueReference, isRuntimeReference, rejectCircularInsertion, typeofValue } from "./references.js"
|
||||
import { isBlockedMember, type SafeObject } from "../tool-runtime.js"
|
||||
import {
|
||||
CodeModeDate,
|
||||
|
|
@ -413,6 +413,89 @@ export const invokeArrayFrom = <R>(
|
|||
})
|
||||
}
|
||||
|
||||
export const invokeGroupBy = <R>(
|
||||
runner: CallbackRunner<R>,
|
||||
namespace: "Map" | "Object",
|
||||
args: Array<unknown>,
|
||||
node: AstNode,
|
||||
): Effect.Effect<unknown, unknown, R> => {
|
||||
const source = args[0]
|
||||
if (source === null || source === undefined) {
|
||||
throw new InterpreterRuntimeError(`${namespace}.groupBy expects an iterable collection.`, node).as("TypeError")
|
||||
}
|
||||
const apply = applyCollectionCallback(runner, args[1], `${namespace}.groupBy`, node)
|
||||
const items = groupByItems(source)
|
||||
if (items === undefined) {
|
||||
throw new InterpreterRuntimeError(`${namespace}.groupBy expects an iterable collection.`, node).as("TypeError")
|
||||
}
|
||||
return Effect.gen(function* () {
|
||||
if (namespace === "Map") {
|
||||
const result = new CodeModeMap()
|
||||
let index = 0
|
||||
for (const item of items) {
|
||||
const key = yield* apply([item, index])
|
||||
const group = result.map.get(key)
|
||||
if (group === undefined) result.map.set(key, [item])
|
||||
else (group as Array<unknown>).push(item)
|
||||
index += 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const result: SafeObject = Object.create(null) as SafeObject
|
||||
let index = 0
|
||||
for (const item of items) {
|
||||
const key = yield* coerceGroupByPropertyKey(runner, yield* apply([item, index]), node)
|
||||
if (isBlockedMember(key)) {
|
||||
throw new InterpreterRuntimeError(`Property '${key}' is not available in CodeMode.`, node)
|
||||
}
|
||||
const group = result[key]
|
||||
if (group === undefined) result[key] = [item]
|
||||
else (group as Array<unknown>).push(item)
|
||||
index += 1
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
const groupByItems = (source: unknown): Iterable<unknown> | undefined => {
|
||||
if (Array.isArray(source) || typeof source === "string") return source
|
||||
if (source instanceof CodeModeMap) return source.map.entries()
|
||||
if (source instanceof CodeModeSet) return source.set.values()
|
||||
if (source instanceof CodeModeURLSearchParams) return source.params.entries()
|
||||
}
|
||||
|
||||
const coerceGroupByPropertyKey = <R>(
|
||||
runner: CallbackRunner<R>,
|
||||
value: unknown,
|
||||
node: AstNode,
|
||||
): Effect.Effect<string, unknown, R> => {
|
||||
if (value === null || typeof value !== "object" || Array.isArray(value) || isCodeModeValue(value)) {
|
||||
return Effect.succeed(coerceToString(value))
|
||||
}
|
||||
if (value instanceof CodeModePromise) return Effect.succeed("[object Promise]")
|
||||
if (isRuntimeReference(value)) {
|
||||
throw new InterpreterRuntimeError("Object.groupBy callback must return a data value.", node, "InvalidDataValue")
|
||||
}
|
||||
const object = value as Record<string, unknown>
|
||||
if (!Object.hasOwn(object, "toString")) return Effect.succeed(coerceToString(value))
|
||||
return Effect.gen(function* () {
|
||||
if (typeofValue(object.toString) === "function") {
|
||||
const result = yield* runner.invokeCallable(object.toString, [], node)
|
||||
if (result === null || (typeof result !== "object" && typeof result !== "function")) {
|
||||
return coerceToString(result)
|
||||
}
|
||||
}
|
||||
if (Object.hasOwn(object, "valueOf") && typeofValue(object.valueOf) === "function") {
|
||||
const result = yield* runner.invokeCallable(object.valueOf, [], node)
|
||||
if (result === null || (typeof result !== "object" && typeof result !== "function")) {
|
||||
return coerceToString(result)
|
||||
}
|
||||
}
|
||||
throw new InterpreterRuntimeError("Cannot convert object to primitive value.", node).as("TypeError")
|
||||
})
|
||||
}
|
||||
|
||||
const invokeStringReplacer = <R>(
|
||||
runner: CallbackRunner<R>,
|
||||
value: string,
|
||||
|
|
@ -491,7 +574,7 @@ export const applyCollectionCallback = <R>(
|
|||
node,
|
||||
)
|
||||
}
|
||||
throw new InterpreterRuntimeError(`${name} expects a function callback.`, node)
|
||||
throw new InterpreterRuntimeError(`${name} expects a function callback.`, node).as("TypeError")
|
||||
}
|
||||
return (callbackArgs) => runner.invokeCallable(callback, callbackArgs, node)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,14 @@ import {
|
|||
UriFunction,
|
||||
} from "./model.js"
|
||||
import { caughtErrorValue, constructErrorValue } from "./errors.js"
|
||||
import { arrayStatics, type CallbackRunner, invokeArrayFrom, invokeGlobalMethod, invokeIntrinsic } from "./methods.js"
|
||||
import {
|
||||
arrayStatics,
|
||||
type CallbackRunner,
|
||||
invokeArrayFrom,
|
||||
invokeGlobalMethod,
|
||||
invokeGroupBy,
|
||||
invokeIntrinsic,
|
||||
} from "./methods.js"
|
||||
import {
|
||||
constructPromise,
|
||||
invokePromiseInstanceMethod,
|
||||
|
|
@ -45,7 +52,7 @@ import {
|
|||
} from "./promises.js"
|
||||
import { containsOpaqueReference, isRuntimeReference, rejectCircularInsertion, typeofValue } from "./references.js"
|
||||
import { ScopeStack } from "./scope.js"
|
||||
import { arrayMethods, mapMethods, setMethods, spreadItems } from "../stdlib/collections.js"
|
||||
import { arrayMethods, mapMethods, mapStatics, setMethods, spreadItems } from "../stdlib/collections.js"
|
||||
import { consoleMethods, formatConsoleMessage } from "../stdlib/console.js"
|
||||
import { dateMethods, dateStatics } from "../stdlib/date.js"
|
||||
import { jsonStatics } from "../stdlib/json.js"
|
||||
|
|
@ -100,6 +107,7 @@ const globalStaticMembers: Partial<Record<GlobalNamespaceName, Set<string>>> = {
|
|||
console: consoleMethods,
|
||||
Date: dateStatics,
|
||||
RegExp: regexpStatics,
|
||||
Map: mapStatics,
|
||||
URL: urlStatics,
|
||||
}
|
||||
|
||||
|
|
@ -1608,6 +1616,9 @@ export class Interpreter<R> {
|
|||
if (callable.namespace === "Array" && callable.name === "from") {
|
||||
return yield* invokeArrayFrom(self.runner, args, node)
|
||||
}
|
||||
if ((callable.namespace === "Object" || callable.namespace === "Map") && callable.name === "groupBy") {
|
||||
return yield* invokeGroupBy(self.runner, callable.namespace, args, node)
|
||||
}
|
||||
if (callable.namespace === "Array" && callable.name === "of") {
|
||||
return invokeGlobalMethod(callable, args, node)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ export const arrayMethods = new Set([
|
|||
|
||||
export const mapMethods = new Set(["get", "set", "has", "delete", "clear", "forEach", "keys", "values", "entries"])
|
||||
|
||||
export const mapStatics = new Set(["groupBy"])
|
||||
|
||||
export const setMethods = new Set(["add", "has", "delete", "clear", "forEach", "keys", "values", "entries"])
|
||||
|
||||
export const spreadItems = (value: unknown): Array<unknown> | undefined => {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { boundedData, coerceToString } from "./value.js"
|
|||
|
||||
export const objectMethodsPreservingIdentity = new Set(["assign", "values", "entries", "fromEntries"])
|
||||
|
||||
export const objectStatics = new Set(["keys", "values", "entries", "hasOwn", "is", "assign", "fromEntries"])
|
||||
export const objectStatics = new Set(["keys", "values", "entries", "hasOwn", "is", "assign", "fromEntries", "groupBy"])
|
||||
|
||||
export const invokeObjectMethod = (name: string, args: Array<unknown>, node: AstNode): unknown => {
|
||||
const requireObject = (): Record<string, unknown> => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue