Structures and Enumerations¶
Structures (named-field records) and enumerations are user-defined types that
must be registered with Definitions before use. This chapter covers creating
and working with both from Node. The Python equivalent is
Structures and Enumerations; the type system is identical — only the idiom
differs. The one real difference is field access: Node has no attribute protocol
(see the warning below), so reads and writes go through explicit methods.
Examples assume the binding is in scope:
const {
Value, Type, Definitions, NameSpace, ValueUUId,
TypeStructureDescriptor, ValueStructure,
TypeVector, ValueVector,
} = require('@digitalsubstrate/dsviper');
Defining a structure¶
Structures are defined within a Definitions context. Build a descriptor, add
fields, then materialise the type with createStructure:
const defs = new Definitions();
const ns = new NameSpace(ValueUUId.create(), 'Tuto');
const d = new TypeStructureDescriptor('Point');
d.addField('x', Type.INT32);
d.addField('y', Type.INT32);
const tPoint = defs.createStructure(ns, d);
tPoint.fields().map((f) => f.name()); // ['x', 'y']
addField also accepts a default Value instead of a bare type. The default is
applied to every instance that does not override the field:
const dCfg = new TypeStructureDescriptor('Config');
dCfg.addField('name', Value.create(Type.STRING, 'Default'));
dCfg.addField('count', Value.create(Type.INT32, 42));
const tConfig = defs.createStructure(ns, dCfg);
new ValueStructure(tConfig).at('name'); // 'Default'
new ValueStructure(tConfig).at('count'); // 42
A descriptor with no fields, or a duplicate field name, throws — the descriptor is validated eagerly:
defs.createStructure(ns, new TypeStructureDescriptor('Empty')); // throws ViperError
const dup = new TypeStructureDescriptor('Dup');
dup.addField('field1', Type.STRING);
dup.addField('field1', Type.INT32); // throws ViperError
Constructing instances¶
The ergonomic form is to construct directly from a plain object. Nested objects are accepted for nested structure fields:
const p = new ValueStructure(tPoint, { x: 3, y: 4 });
p.at('x'); // 3
p.at('y'); // 4
Omit the object to get the type’s defaults (zero, or per-field defaults declared on the descriptor):
new ValueStructure(tConfig).at('count'); // 42
Passing another ValueStructure of the same type makes a deep copy:
const q = new ValueStructure(tPoint, p);
q.set('x', 99);
p.at('x'); // 3 (independent copy)
When a structure is the document of an attachment, Attachment.createStructure
is the equivalent factory — see Using structures with databases.
Field access¶
Warning
Node has no attribute protocol. struct.x is undefined — silently, with
no error — because x is a Viper field, not a JavaScript property. This is the
one quiet footgun on this page. Always read and write fields through at / set
(or the bulk and deep-access methods below). Every one of those is fail-fast: an
unknown field, a wrong-type value, or an empty key-path throws.
One field at a time¶
set(field, value) writes; at(field) reads back the JavaScript native:
const s = new ValueStructure(tPoint);
s.set('x', 10);
s.at('x'); // 10
s.x; // undefined <-- NOT a field read; there is no attribute protocol
By default at returns the native (a JS number for INT32/FLOAT/DOUBLE
fields, a bigint for INT64, a string for STRING, …). Pass false as a
second argument to get the wrapped Value handle instead:
const { ValueString } = require('@digitalsubstrate/dsviper');
const cfg = new ValueStructure(tConfig, { name: 'x', count: 1 });
cfg.at('name', false) instanceof ValueString; // true
Both at and set are type-checked and fail-fast:
s.set('nope', 1); // throws ViperError — unknown field
s.at('nope'); // throws ViperError — unknown field
s.set('x', 'not an int'); // throws ViperError — wrong type
Several fields at once¶
assign(object) writes several fields in one call. It is partial (only the keys
you pass are touched), type-checked per field, and returns the structure so it
chains:
const s2 = new ValueStructure(tPoint);
s2.assign({ x: 3, y: 4 });
s2.at('x'); // 3
s2.assign({ y: 9 }); // partial update
s2.at('y'); // 9
s2.assign({ nope: 1 }); // throws ViperError — unknown field
toObject() is the reverse: it returns the fields as a plain object (natives by
default, wrapped handles with toObject(false)), which destructures cleanly:
const { x, y } = new ValueStructure(tPoint, { x: 3, y: 4 }).toObject();
// x === 3, y === 4
INT64 fields are bigint¶
INT32, FLOAT, and DOUBLE fields round-trip as JavaScript numbers; INT64
fields are bigint. Mind the n suffix:
const dSample = new TypeStructureDescriptor('Sample');
dSample.addField('count', Type.INT64);
dSample.addField('ratio', Type.DOUBLE);
const tSample = defs.createStructure(ns, dSample);
const s = new ValueStructure(tSample, { count: 9876543210n, ratio: 3.14159 });
typeof s.at('count'); // 'bigint'
typeof s.at('ratio'); // 'number'
Nested structures and deep access¶
A structure can hold other structures. Construct them inline with nested objects:
const dAddr = new TypeStructureDescriptor('Address');
dAddr.addField('city', Type.STRING);
dAddr.addField('zip', Type.INT32);
const tAddr = defs.createStructure(ns, dAddr);
const dUser = new TypeStructureDescriptor('User');
dUser.addField('name', Type.STRING);
dUser.addField('address', tAddr);
const tUser = defs.createStructure(ns, dUser);
const u = new ValueStructure(tUser, {
name: 'Bob',
address: { city: '', zip: 0 },
});
u.at('address') instanceof ValueStructure; // true
To reach into a nested field, chaining at works (u.at('address').at('city')),
but setIn / getIn express a deep read or write directly with a key-path:
u.setIn(['address', 'city'], 'Paris');
u.getIn(['address', 'city']); // 'Paris'
A key-path step is resolved polymorphically by what sits at that level — a field name for a structure, an index for a vector, a key for a map:
const dUser2 = new TypeStructureDescriptor('User2');
dUser2.addField('tags', new TypeVector(Type.STRING));
const tUser2 = defs.createStructure(ns, dUser2);
const u2 = new ValueStructure(tUser2, {
tags: new ValueVector(new TypeVector(Type.STRING), ['a', 'b']),
});
u2.setIn(['tags', 1], 'z'); // vector index step
u2.getIn(['tags', 1]); // 'z'
setIn returns the structure, so calls chain. getIn(path, false) returns the
wrapped handle instead of the native. Both are fail-fast — an unknown field, a
wrong-type leaf, or an empty key-path throws:
u.setIn(['address', 'nope'], 'x'); // throws — unknown field
u.setIn(['address', 'city'], 42); // throws — wrong-type leaf
u.getIn([]); // throws — empty key-path
Deep access is offered only on the nestable containers (structures, vectors,
maps); it is not defined on flat bytes (ValueBlob) or a by-membership
ValueSet.
Enumerations¶
Enumerations are types with a fixed set of named cases. Build them through a
TypeEnumerationDescriptor, mirroring the structure flow:
const { TypeEnumerationDescriptor, ValueEnumeration } = require('@digitalsubstrate/dsviper');
const eStatus = new TypeEnumerationDescriptor('Status');
eStatus.addCase('pending', 'Waiting to start');
eStatus.addCase('active', 'In progress');
eStatus.addCase('completed', 'Finished');
const tStatus = defs.createEnumeration(ns, eStatus);
Create a value by case name (most common) or by 0-based index:
new ValueEnumeration(tStatus, 'active').name(); // 'active'
new ValueEnumeration(tStatus, 0).name(); // 'pending'
Read its properties:
const status = new ValueEnumeration(tStatus, 'active');
status.name(); // 'active'
status.index(); // 1
status.typeEnumeration().equals(tStatus); // true
The default value of an enumeration type is its first case:
Value.create(tStatus).name(); // 'pending'
Note
Compare enumeration values with .equals / .compare (both accept a native
case name), never with == — between two Values == is reference equality,
not value equality. See Types and Values for the value-semantics rules.
Errors¶
A thrown Viper error is a JavaScript Error whose name is 'ViperError':
try {
new ValueStructure(tPoint).at('nope');
} catch (e) {
e.name; // 'ViperError'
}
See Error Handling for the full error model.
Using structures with databases¶
Structures are usually stored as the documents of an attachment. Define the
attachment against a concept type, then mint documents with
Attachment.createStructure:
const tNode = defs.createConcept(ns, 'Node');
const att = defs.createAttachment(ns, 'Node_Data', tNode, tPoint);
const doc = att.createStructure({ x: 1, y: 2 });
Locating and mutating a field of a stored document inside a transaction goes
through a Path rather than at / setIn — see Database (Path) for the
mutation and commit API.