import { isStruct, type PropertyValue, tagOf, textOf } from '../../../model/Properties'; import { itemKindOf, itemLike, makeItem } from '../../../properties/listItems'; import type { StructShape } from '../../lookup'; import { STRUCT_TYPE_SETTING } from '../../nested'; import type { NodeDef } from '../../NodeRegistry'; import { asList, asNumber, asString, asValueItem, isEntity, type Value, type ValueList } from '../../values'; import { any, int, list } from '../shared'; export const SetItem: NodeDef = { type: 'SetItem', label: 'Set item', category: 'data', description: 'The same list with one item replaced. The list that came in is left alone. With a type, only a struct with exactly its fields goes in – anything else is reported and the list stays as it was.', inputs: [list('list'), int('index'), any('value')], outputs: [list('result')], run: (ctx, inputs, params, _def, _entry, lookup) => { const items = asList(inputs.list); const i = Math.floor(asNumber(inputs.index)); if (i < 0 || i >= items.length) return { outputs: { result: items } }; // Checked like `Add item` (#254): by the field names, the only thing a run knows a struct by. const typeId = textOf(params[STRUCT_TYPE_SETTING.key] ?? ''); const type = typeId ? lookup.structType(typeId) : null; if (type && !fitsType(inputs.value, type)) { ctx.log(`Set item: ${asString(inputs.value ?? null) || 'nothing'} is no ${type.name} – not set`); return { outputs: { result: items } }; } const next = [...items]; // The slot keeps its kind: writing a number into a color item leaves a color behind, the same // way writing into a number variable leaves a number (see `asTypeOf`). next[i] = fitToSlot(items[i], asValueItem(inputs.value)); return { outputs: { result: next } }; }, params: [STRUCT_TYPE_SETTING], }; /** * A run knows a struct only by its fields (#254): one fits a type when it has exactly the type's * field names – none missing, none more. */ export function fitsType(value: Value | undefined, type: StructShape): boolean { if (isEntity(value) || !isStruct(value) || tagOf(value) !== undefined) return false; const keys = Object.keys(value); return keys.length === type.fields.length && type.fields.every((field) => field in value); } /** `value` as the kind `slot` holds, the way `Set item` and `Add item` write into a list. */ export function fitToSlot(slot: ValueList[number] | undefined, value: ValueList[number]): ValueList[number] { // An object has no kind to keep and none to take on, so it is written as it is (#157). if (isEntity(value) || isEntity(slot)) return value; return nestedKind(slot) ? sameNested(slot, value) : itemLike(slot, value); } /** Whether a slot holds a list or a struct, which may carry objects `itemLike` would not keep. */ const nestedKind = (slot: ValueList[number] | undefined): boolean => { const kind = itemKindOf(slot as PropertyValue); return kind === 'list' || kind === 'struct'; }; /** A list or struct slot takes a value of its own kind as it is, anything else as its empty one. */ function sameNested(slot: ValueList[number] | undefined, value: ValueList[number]): ValueList[number] { const kind = itemKindOf(slot as PropertyValue); return itemKindOf(value as PropertyValue) === kind ? value : makeItem(kind, undefined); }