/** * A list declares nothing about what it holds: every item carries its own kind, read back off the * value. JSON says what a number, a text, a boolean, a list and a struct are; a tagged kind says so * itself (`tagOf`), which keeps every list ever stored readable. * * Items nest to any depth (#239): the items of a list item and the fields of a struct item go * through the same coercion again, so nothing deeper is dropped. */ import { ITEM_TYPE_KEY, isList, isStruct, type ListValue, objectPlace, type PropertyValue, type StructValue, tagged, tagOf, untag, } from '../model/Properties'; import { coerceField, isKindWith, isTaggedKind, type NestedKind, plainKindOf } from './kinds'; /** What a list holds. Identical to `ListValue[number]`, named for the places that mean one item. */ export type ListItem = PropertyValue; /** The kind an item is, taken from the item itself – there is nowhere else it could come from. */ export function itemKindOf(item: PropertyValue | undefined): NestedKind { const tag = tagOf(item); if (tag !== undefined && isTaggedKind(tag)) return tag; if (isList(item)) return 'list'; if (isStruct(item)) return 'struct'; const plain = plainKindOf(typeof item); return plain !== undefined && isKindWith('field', plain) ? plain : 'number'; } /** `value` as an item of `kind`, which is how a row changes what it holds. */ export function makeItem(kind: NestedKind, value: PropertyValue | undefined): ListItem { if (kind === 'list') return listOf(value); if (kind === 'struct') return structItemOf(value); if (kind === 'object') return objectPlace(value); if (isTaggedKind(kind)) return tagged(kind, coerceField(untag(value), kind)); return coerceField(untag(value), kind); } /** `value` as an item of the kind the slot already holds, so writing a color keeps it a color. */ export function itemLike(current: PropertyValue | undefined, value: PropertyValue | undefined): ListItem { return makeItem(itemKindOf(current), value); } /** Any value as a valid item; the one coercion a list value goes through. */ export function listItemOf(value: PropertyValue | undefined): ListItem { return makeItem(itemKindOf(value), value); } /** The items of a list, each as the kind its own value says; anything else holds none. */ export function listOf(value: PropertyValue | undefined): ListValue { return isList(value) ? value.map(listItemOf) : []; } /** A struct item keeps the fields it carries, each as the kind its own value says. */ export function structItemOf(value: PropertyValue | undefined): StructValue { if (!isStruct(value) || tagOf(value) !== undefined) return {}; const fields: Record = {}; for (const [key, item] of Object.entries(value)) fields[key] = listItemOf(item); return fields; } /** The shape a struct item has right now: its keys in order, each with the kind of its value. */ export function structItemFields(item: PropertyValue | undefined): { key: string; kind: NestedKind }[] { if (!isStruct(item) || tagOf(item) !== undefined) return []; return Object.entries(item) .filter(([key]) => key !== ITEM_TYPE_KEY) .map(([key, value]) => ({ key, kind: itemKindOf(value) })); }