Structures
Version Note
This documentation describes Swim JS packages v4.0.0 or later. Users of earlier package versions may experience differences in behavior.
Item
At the center of @swim/structure is the Item
class, which defines an algebraic data type for representing and manipulating structured data. Item
provides many methods for operating on structured values, most of which are closed over the Item
type, meaning they always return other instances of Item
. This closure of operations over the Item
type makes it safe and expressive to traverse, transform, and convert arbitrary data structures, without excessive conditional logic to type check and validate structures obtained from external sources.
Every Item
is either a Field
or a Value
. Every Field
is either an Attr
or a Slot
. And every Value
is either a Record
, Data
, Text
, Num
, Bool
, Extant
, or Absent
. Think of Item
as analogous to the set of all JSON values, with the inclusion of object fields as first class elements.
Field
A Field
represents a key-value pair, where both the key and value are of type Value
. An Attr
is a discriminated kind of Field
whose key is always of type Text
. Every Field
that is not explicitly an Attr
is a Slot
. Think of a Slot
as a field of a JSON object, or as an attribute of an XML tag. Think of an Attr
like an XML tag, where the key of the Attr
is the tag name, and the value of the Attr
is a Record
containing the element’s attributes.
Value
Every Item
that is not a Field
is a Value
. A Value
can either be one of four primitive value types: Data
, Text
, Num
, or Bool
; one of two unit types: Extant
, or Absent
; or the composite type: Record
. Think of a Value
as representing an arbitrary data structure.
Primitive Value Types
A Data
object represents opaque binary data; it wraps a JavaScript Uint8Array. A Text
object represents a Unicode string, and wraps a primitive JavaScript string. A Num
object represents a numeric value, encapsulating a primitive JavaScript number. A Bool
object represents a boolean value, wrapping a primitive JavaScript boolean.
Unit Value Types
There are two unit types: Extant
, and Absent
. Extant
represents a thing that exists, but has no value; sort of like JavaScript’s null value, but a valid object on which you can invoke methods. Absent
represents something that does not exist; similar to JavaScript’s undefined value, but a valid instance of Item.
Composite Value Type
A Record
is a simple container of Item
members, and is the only composite structure type. A Record
containing only Field
members is analogous to a JSON object — though, unlike JSON, its keys are not restricted to strings. A Record
containing only Value
members is similar to a JSON array. A Record with a leading Attr
bears resemblance to an XML element. And a Record with a mixture of Field
and Value
members acts like a partially keyed list.
Item Reference
Since everything is an Item
, each of these methods are available on every kind of structure.
isDefined(): boolean;
Returns true
if this Item
is not Absent
.
isDistinct(): boolean;
Returns true
if this Item
is neither Extant
nor Absent
.
isDefinite(): boolean;
Returns true
if this Item
is not one of: an empty Record
, False
, Extant
, or Absent
.
isConstant(): boolean;
Returns true
if this Item
always evaluates
to the same Item
.
stringValue(): string | undefined;
Converts this Item
into a string
value, if possible; otherwise returns
undefined
if this Item
can’t be converted into a string
value.
// WARP message
// @event(node:"/home/electricityMeter",lane:status){currentReading:8432.7,model:"Single Phase 4P Din Rail Energy Meter",normalOperation:true,timestamp:1710272571408}
console.log(item.get("model")) // {value: 'Single Phase 4P Din Rail Energy Meter', hashValue: undefined, ...}
console.log(item.get("model").stringValue()) // "Single Phase 4P Din Rail Energy Meter"
console.log(item.get("currentReading").stringValue()) // "8432.7"
console.log(item.get("normalOperation").stringValue()) // "true"
stringValue<T>(orElse: T): string | T;
Converts this Item
into a string
value, if possible; otherwise returns
orElse
if this Item
can’t be converted into a string
value.
// WARP message
// @event(node:"/home/electricityMeter",lane:status){currentReading:8432.7,model:"Single Phase 4P Din Rail Energy Meter",normalOperation:true,timestamp:1710272571408}
console.log(item.get("foo")) // Absent {}
console.log(item.get("foo").stringValue()) // undefined
console.log(item.get("foo").stringValue("fallback")) // "fallback"
numberValue(): number | undefined;
Converts this Item
into a number
value, if possible; otherwise returns
undefined
if this Item
can’t be converted into a number
value.
// WARP message
// @event(node:"/home/electricityMeter",lane:status){currentReading:8432.7,model:"Single Phase 4P Din Rail Energy Meter",normalOperation:true,timestamp:1710272571408}
console.log(item.get("currentReading")) // {value: 8432.7, hashValue: undefined, ...}
console.log(item.get("currentReading").numberValue()) // 8432.7
console.log(item.get("model").numberValue()) // undefined
console.log(item.get("normalOperation").numberValue()) // undefined
numberValue<T>(orElse: T): number | T;
Converts this Item
into a number
value, if possible; otherwise returns
orElse
if this Item
can’t be converted into a number
value.
// WARP message
// @event(node:"/home/electricityMeter",lane:status){currentReading:8432.7,model:"Single Phase 4P Din Rail Energy Meter",normalOperation:true,timestamp:1710272571408}
console.log(item.get("foo")) // Absent {}
console.log(item.get("foo").numberValue()) // undefined
console.log(item.get("foo").numberValue(0)) // 0
booleanValue(): boolean | undefined;
Converts this Item
into a boolean
value, if possible; otherwise returns
undefined
if this Item
can’t be converted into a boolean
value.
// WARP message
// @event(node:"/home/electricityMeter",lane:status){currentReading:8432.7,model:"Single Phase 4P Din Rail Energy Meter",normalOperation:true,timestamp:1710272571408}
console.log(item.get("normalOperation")) // {value: true, hashValue: undefined, ...}
console.log(item.get("normalOperation").booleanValue()) // true
console.log(item.get("currentReading").booleanValue()) // true
console.log(item.get("model").booleanValue()) // undefined
booleanValue<T>(orElse: T): boolean | T;
Converts this Item
into a boolean
value, if possible; otherwise returns
orElse
if this Item
can’t be converted into a boolean
value.
// WARP message
// @event(node:"/home/electricityMeter",lane:status){currentReading:8432.7,model:"Single Phase 4P Din Rail Energy Meter",normalOperation:true,timestamp:1710272571408}
console.log(item.get("foo")) // Absent {}
console.log(item.get("foo").booleanValue()) // undefined
console.log(item.get("foo").booleanValue(false)) // false
toLike: ItemLike
Converts this Item
into the most appropriate JavaScript object. It will parse out strings, numbers, booleans, Arrays, or plain old JavaScript object. Nested structures are supported. Absent
is coerced to undefined and Extant
is coerced to null.
// WARP message
// @event(node:"/user/1234",lane:orders)@update(key:"/order/123456"){timestamp:1710295106760,totalPrice:29.98,cart:[{itemId:"/item/789012",qty:2,unitPrice:14.99}]}
console.log(item.get('totalPrice').toLike()) // 29.98
console.log(item.get('foo').toLike()) // undefined
console.log(item.header('update').toLike()) // { key: "/order/123456" }
console.log(item.toLike())
/* {
@update: { key: "/order/123456" },
timestamp: 1710295106760,
totalPrice: 29.98,
cart: [
{
itemId: "/item/789012",
qty: 2,
unitPrice: 14.99
}
]
} */
static fromLike: Item
Converts a JavaScript object into an item. Essentially the reverse of toLike
. Nested structures are supported. BigInt and Symbols types are not supported. Undefined is converted to Absent
and null type is converted to Extant
. Values of NaN are preserved.
console.log(Item.fromLike(null)) // Extant {}
const obj = {
string: "Hello, world!",
number: "123456",
boolean: true,
nan: NaN,
array: ["a", 2, { three: 3 }],
};
console.log(Item.fromLike().toString()) // Record.of(Slot.of("string", "Hello, world!"), Slot.of("number", "123456"), Slot.of("boolean", true), Slot.of("nan", NaN), Slot.of("array", Record.of("a", 2, Record.of(Slot.of("three", 3)))))
console.log(Item.fromLike().toLike()) // identical to original obj object
readonly key: Value;
Returns the key component of this Item
, if this Item
is a Field; otherwise returns Absent if this Item
is a Value
.
toValue(): Value;
Returns the value component of this Item
, if this Item
is a Field; otherwise returns this
if this Item
is a Value
.
readonly tag: string | undefined;
Returns the key string of the first member of this Item
, if this Item
is a Record, and its first member is an Attr; otherwise returns
undefined
if this Item
is not a Record
, or if this Item
is a Record
whose first member is not an Attr
.
Used to concisely get the name of the discriminating attribute of a structure. The tag can be used to discern the nominal type of a polymorphic structure, similar to an XML element tag.
/*
WARP message; update to value lane's synced value
@event(node:"/home/electricityMeter",lane:status){currentReading:8432.7,model:"Single Phase 4P Din Rail Energy Meter",normalOperation:true,timestamp:1710272571408}
*/
console.log(item.tag); // undefined
/*
WARP message; key updated or added to map-based lane
@event(node:"/user/1234",lane:orders)@update(key:"/order/123456"){timestamp:1710295106760,totalPrice:29.98,cart:[{itemId:"/item/789012",qty:2,unitPrice:14.99}]}
*/
console.log(item.tag); // "update"
/*
WARP message; key removed from map-based lane
@event(node:"/user/1234",lane:orders)@remove(key:"/order/456789")
*/
console.log(item.tag); // "remove"
readonly target: Value;
If this Item
is a Record, returns the flattened
members of this Item
after all attributes have been removed;
otherwise returns this
if this Item
is a non-Record
Value
, or returns the value component if this Item
is a Field
.
Used to concisely get the scalar value of an attributed structure. An attributed structure is a Record
with one or more attributes that modify one or more other members.
flattened(): Value;
Returns the sole member of this Item
, if this Item
is a Record
with exactly one member, and its member is a Value
; returns Extant
if this Item
is an empty Record
; returns Absent if this Item
is
a Field
; otherwise returns this
if this Item
is a Record
with more
than one member, or if this Item
is a non-Record
Value
.
Used to convert a unary Record
into its member Value
. Facilitates
writing code that treats a unary Record
equivalently to a bare Value
.
unflattened(): Record;
Returns this
if this Item
is a Record; returns a Record
containing just this Item
, if this Item
is distinct
; otherwise returns an empty Record
if this Item
is Extant or Absent. Facilitates writing code that treats a bare Value
equivalently to a unary Record
.
header(tag: string): Value;
Returns the value of the first member of this Item
, if this Item
is a
Record, and its first member is an Attr whose key
string is equal to tag
; otherwise returns Absent if this Item
is not a Record
, or if this Item
is a Record
whose first member is not an Attr
, or if this Item
is a Record
whose first member is an Attr
whose key
does not equal the tag
. Used to conditionally get the value of the head Attr
of a structure, if and only if the key string of the head Attr
is equal to the tag
. Can be used to check if a structure might conform to a nominal type named tag
, while simultaneously getting the value of the tag
attribute.
// WARP message
// @event(node:"/user/1234",lane:orders)@update(key:"/order/123456"){timestamp:1710295106760,totalPrice:29.98,cart:[{itemId:"/item/789012",qty:2,unitPrice:14.99}]}
value.header('update'); // RecordMap {length: 1, fieldCount: 1, ...}
value.header('update').get('key'); // "/order/123456"
value.header('replace').get('key'); // Absent {}
headers(tag: string): Record | undefined;
Returns the unflattened
header
of
this Item
, if this Item
is a Record, and its first member is an Attr whose key
string is equal to tag
; otherwise returns undefined
. The headers
of the tag
attribute of a structure are like the attributes of an XML element tag; through unlike an XML element, tag
attribute headers are not limited to string keys and values.
head(): Item;
Returns the first member of this Item
, if this Item
is a non-empty
Record; otherwise returns Absent.
tail(): Record;
Returns a view of all but the first member of this Item
, if this Item
is a non-empty Record
; otherwise returns an empty Record
if this Item
is not a Record
, or if this Item
is itself an empty Record
.
body(): Value;
Returns the flattened
tail
of this
Item
. Used to recursively deconstruct a structure, terminating with its last Value
, rather than a unary Record
containing its last value, if the structure ends with a Value
member.
readonly length: number;
Returns the number of members contained in this Item
, if this Item
is
a Record; otherwise returns 0
if this Item
is not a Record
.
// WARP message
// @event(node:"/home/electricityMeter",lane:status){currentReading:8432.7,model:"Single Phase 4P Din Rail Energy Meter",normalOperation:true,timestamp:1710272571408}
console.log(item.length) // 4
has(key: ValueLike): boolean;
Returns true
if this Item
is a Record that has a Field member
with a key that is equal to the given key; otherwise returns false
if this Item
is not a Record
, or if this Item
is a Record
, but has no Field
member with a key equal to the given key.
// WARP message
// @event(node:"/home/electricityMeter",lane:status){currentReading:8432.7,model:"Single Phase 4P Din Rail Energy Meter",normalOperation:true,timestamp:1710272571408}
console.log(item.has("model")) // true
console.log(item.has("currentReading")) // true
console.log(item.has("foo")) // false
get(key: ValueLike): Value;
Returns the value of the last Field member of this Item
whose key
is equal to the given key; returns Absent if this Item
is not a Record, or if this Item
is a Record
, but has no Field
member with a key equal to the given key.
// WARP message
// @event(node:"/home/electricityMeter",lane:status){currentReading:8432.7,model:"Single Phase 4P Din Rail Energy Meter",normalOperation:true,timestamp:1710272571408}
console.log(item.get("model")) // {value: 8432.7, hashValue: undefined, ...}
console.log(item.get("currentReading").numberValue()) // 8432.7
console.log(item.get("model").stringValue()) // "Single Phase 4P Din Rail Energy Meter"
console.log(item.get("normalOperation").booleanValue()) // true
getAttr(key: TextLike): Value;
Returns the value of the last Attr member of this Item
whose key
is equal to the given key
; returns Absent if this Item
is not a Record, or if this Item
is a Record
, but has no Attr
member with a key equal to the given key
.
// WARP message
// @event(node:"/user/1234",lane:orders)@update(key:"/order/123456"){timestamp:1710295106760,totalPrice:29.98,cart:[{itemId:"/item/789012",qty:2,unitPrice:14.99}]}
console.log(item.getAttr("update").get("key").stringValue()); // "/order/123456"
console.log(item.get("update").get("key").stringValue()); // "/order/123456"
console.log(item.getAttr("totalPrice").numberValue()); // undefined
console.log(item.get("totalPrice").numberValue()); // 29.98
getSlot(key: ValueLike): Value;
Returns the value of the last Slot member of this Item
whose key
is equal to the given key
; returns Absent if this Item
is not a Record, or if this Item
is a Record
, but has no Slot
member with a key equal to the given key
.
// WARP message
// @event(node:"/user/1234",lane:orders)@update(key:"/order/123456"){timestamp:1710295106760,totalPrice:29.98,cart:[{itemId:"/item/789012",qty:2,unitPrice:14.99}]}
console.log(item.get("totalPrice").numberValue()); // 29.98
console.log(item.getSlot("totalPrice").numberValue()); // 29.98
console.log(item.get("update").get("key").stringValue()); // "/order/123456"
console.log(item.getSlot("update").get("key").stringValue()); // "/order/123456"
getField(key: ValueLike): Field | undefined;
Returns the last Field member of this Item
whose key is equal to the
given key; returns undefined
if this Item
is not a Record, or if this Item
is a Record
, but has no Field
member with a key equal to the given key.
getItem(index: NumLike): Item;
Returns the member of this Item
at the given index
, if this Item
is
a Record, and the index
is greater than or equal to zero, and less than the length
of the Record
; otherwise returns Absent if this Item
is not a Record
, or if this Item
is a Record
, but the index
is out of bounds.
// WARP message
// @event(node:"/user/1234",lane:orders)@update(key:"/order/123456"){timestamp:1710295106760,totalPrice:29.98,cart:[{itemId:"/item/789012",qty:2,unitPrice:14.99}]}
console.log(item.getItem(0).key.stringValue()) // "update"
console.log(item.getItem(1).numberValue()) // 1710295106760
console.log(item.getItem(2).numberValue()) // 8432.7
console.log(item.getItem(99).stringValue()) // Absent {}
forEach(callback: (item: Item, index: number) => void)): undefined;
Iterates over every Attribute
or Slot
of an Item and executes the provided callback on it, receiving the individual Attribute
or Slot
and its index as arguments. If forEach
is called on either Absent
or an empty Record
, the callback is never invoked as the Item
does not contain a valid Attribute
or Slot
. If forEach
is called on Extant
, the callback gets invoked a single time with the Extant
unit type.
evaluate(interpreter: InterpreterLike): Item;
Returns a new Item
with all nested expressions interpreted in lexical order and scope.
readonly typeOrder: number;
Returns the heterogeneous sort order of this Item
. Used to impose a
total order on the set of all items. When comparing two items of
different types, the items order according to their typeOrder
.