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.