Object Library

Stof's standard object library ("Object").

Common Value Functions

Object.toString(object: obj): str

The default implementation for turning an object into a string is to use the "json" format. If not loaded, it will fall back to the "toml" format. If both are not loaded in the document, this function will return a debug implementation, dumping the entire contents of the object as a string (just like "std.dbg(obj)").

#[test]
fn test() {
    let obj = new {
        a: "a"
    };
    assertEq(obj.toString(), '{"a":"a"}');
}

Object.or(object: obj, ...): unknown

Returns the first non-empty (null or void) argument, just like the Standard Library "or" function.

#[test]
fn test() {
    let obj = self.dne.or(new { default: true });
    assert(obj.default);
}

Fields & Functions

Object.len(object: obj): int

Because the default iteration of an object is over its fields, the length of an object is defined by the number of fields it has.

object: {
    a: "a"
    b: "b"
    c: "c"
    
    fn another() {}
}

#[test]
fn test() {
    assertEq(self.object.len(), 3);
    assertEq(Object.len(self.object), 3);
}

Object.at(object: obj, index: str | int): unknown

Will get a field or function on this object according to an index.

If the index is a string, this function will return the first field or function with a name that matches the index, or null if not found.

If the index is an integer, this function will return the field at the desired index in relation to all other fields on the object as a Tuple (name, value). This is only useful for iterating over an object's fields (without using the preferred "Object.fields(object)" function).

field: 42

#[test]
fn getValueByName() {
    assertEq(self.at("field"), 42);
    assertEq(self["field"], 42);
    
    assertEq(self["getFieldByIndex"], self.getFieldByIndex);
    
    assertNull(self.at("dne"));
}

#[test]
fn getFieldByIndex() {
    assertEq(self.at(0), ("field", 42));
    
    // use case - faster to use self.fields(), self.keys(), or self.values()
    for (field in self) {
        let name = field[0];
        let value = field[1];
        
        // do something cool...
    }
}

Object.fields(object: obj): map

Creates a map out of all fields on this object.

This function differs from the Standard Library map initializer std.map(obj) with an object argument. This function will not create maps out of sub-objects like the standard library function will.

a: "a"
b: "b"

#[test]
fn fasterIteration() {
    for (field in self.fields()) {
        let name = field[0];
        let value = field[1];
        
        // do something cool...
    }
}

#[test]
fn more() {
    let fields = box(self.fields()); // loads all object fields into a map ref
    assert(fields.contains("a"));
    assertEq(fields.get("a"), "a");
}

Object.keys(object: obj): vec

Returns a vector of all field keys on this object.

a: "A"
b: "B"

#[test]
fn test() {
    let keys = self.keys() as set; // cast to set for order
    assertEq(keys, set("a", "b"));
}

Object.values(object: obj): vec

Returns a vector of all field values on this object.

a: "A"
b: "B"

#[test]
fn test() {
    let values = self.values() as set; // cast to set for order
    assertEq(values, set("A", "B"));
}

Object.set(object: obj, name: str, value: unknown): bool

Sets a field value on/from this object. Will create a new field if one does not already exist.

The field name is really a field path, starting at this object - Stof will create new objects as needed to make the path valid.

field: 42

#[test]
fn test() {
    // Field on self
    assertEq(self.field, 42);
    assert(self.set("field", 100));    // just like "self.field = 100"
    assertEq(self.field, 100);
    
    // Create new objects (or set fields if already exists)
    self.set("self.subobj.field", 222); // creates a child object named "subobj"
    assertEq(self.subobj.field, 222);
}

Object.removeField(object: obj, name: str, dropIfObject?: bool): bool

Remove a field by name/path, starting from this object. Path behavior is the same as for "set".

The parameter "dropIfObject" is only considered when the field being removed is an object. Objects in Stof are just references and can exist in multiple objects at once. This parameter gives more control over when those objects are removed from the document entirely. The default value is false, not dropping objects from the document. Unless you know what you're doing, it is recommended to leave it as false.

Returns whether or not the field was found and removed.

field: 42
another: 42

#[test]
fn test() {
    assertEq(self.field, 42);
    assert(self.removeField("field"));
    assertNull(self.field);
    assertNot(self.removeField("field"));
    
    // alternative if you know the name
    assertEq(self.another, 42);
    drop self.another;
    assertNull(self.another);
}

Object.moveField(object: obj, from: str, to: str): bool

Alias function for "renameField". Both functions have the same implementation.

Object.renameField(object: obj, from: str, to: str): bool

Renames a field and/or moves it to a new location, starting from this object.

Both "from" and "to" are paths, which makes this function very useful.

saying: "hi there"

#[test]
fn test() {
    assertEq(self.saying, "hi there");
    self.renameField("saying", "message");
    assertNull(self.saying);
    assertEq(self.message, "hi there");

    self.moveField("message", "subobject.message");
    assertNull(self.message);
    assertEq(self.subobject.message, "hi there");
}

value: 10
collision: {
    dude: 100
}

#[test]
fn collides() {
    self.moveField("value", "collision.dude");
    // self.collision.moveField("super.value", "dude"); // does the same thing
    assertNull(self.value);
    assertEq(self.collision.dude, [100, 10]);
}

Object.mapFields(object: obj, mapping: map): map

Calls "moveField"/"renameField" for each key/value pair in a mapping, returning a map of all key/value pairs that were successfully moved/renamed.

Take note that the paths start at the calling object. So if you wanted to call this function instead from "source" (self.source.mapFields(mapping)), the mapping from field "a" to field "first" would instead be ("a", "super.destination.first").

source: {
    a: "A"
    b: "B"
    c: "C"
}
destination: {}  // will be created if not present already...

#[test]
fn test() {
    let mapping = map(
        ("source.a", "destination.first"),
        ("source.b", "destination.second"),
        ("source.c", "destination.third"),
        ("source.d", "destination.fourth") // this one doesn't exist
    );
    
    let res = self.mapFields(mapping);
    
    assertEq(res.keys(), ["source.a", "source.b", "source.c"]);
    assertEq(self.destination.first, "A");
    assertEq(self.destination.second, "B");
    assertEq(self.destination.third, "C");
    
    assertNull(self.source.a);
    assertNull(self.source.b);
    assertNull(self.source.c);
}

Object.attributes(object: obj, name: str): map

Returns a map of attributes on a field or function that exists at the name/path, starting at this object, or null if a field or function cannot be found.

See Fields & Functions for more information on attributes.

#[custom((): str => "hey there")] // attribute values get executed when parsed
field: 42;

#[test]  // default attribute value is null
fn test() {
    // gets the attributes from this function
    let testAttributes = self.attributes("test");
    assert(testAttributes.contains("test"));
    assertNull(testAttributes.get("test"));
    
    // gets the attributes from our field
    let fieldAttrs = self.attributes("field");
    assertEq(typeof fieldAttrs.get("custom"), "fn");
    assertEq(fieldAttrs.get("custom").call(), "hey there");
}

Object.functions(object: obj): map

Returns a map of all functions that exist on this object. Keys are function names and values are function pointers that can be called.

fn hello(): str { return "hello"; }
fn add(a: float, b: float): float { return a + b; }

#[test]
fn test() {
    let funcs = self.functions();
    assertEq(funcs.keys(), ["add", "hello", "test"]);
    assertEq(funcs.get("add").call(3, 4), 7);
}

Object.removeFunc(object: obj, name: str): bool

Remove a function from the document by name/path, starting at this object.

fn hello(): str { return "hello"; }

#[test]
fn test() {
    assertEq(self.hello(), "hello");
    
    assert(self.removeFunc("hello"));
    assertNull(self.hello);
    //assertEq(self.hello(), ""); // errors - func doesn't exist
}

Object.reference(object: obj, path: str): bool

Add a reference to a field or function on this object from a path.

In Stof's terms, this function merely attaches the field or function to this object in addition to any other objects it is already attached to - nothing is created, copied, or moved.

This is a cool functionality in Stof, but use it wisely as it can make some interfaces hard to trace and use.

record: {
    nested: {
        name: {
            first: "Bob"
            last: "Jones"
            
            fn full(): str {
                return `${self.first} ${self.last}`;
            }
        }
    }
}

#[test]
fn test() {
    self.reference("self.record.nested.name");
    assertEq(self.name.first, self.record.nested.name.first);
    assertEq(self.name.full(), "Bob Jones");
    
    self.name.first = "Karen";
    assertEq(self.name.first, self.record.nested.name.first);
    assertEq(self.name.full(), "Karen Jones");
    assertEq(self.record.nested.name.full(), "Karen Jones");
    
    self.name.removeFunc("full"); // only one function ever existed.
    assertNull(self.name.full);
    assertNull(self.record.nested.name.full);
}

Object.search(object: obj, field: str, parentChildren: bool = true, ignore: vec = []): (unknown, int)

Search this object for a field by name. Will search both up (parents) and down (children), returning the closest found field. If a field was found in a parent and also found in a child and the distances are the same, this function will favor the child field (downwards).

Parameters:

  • field - the name of the field to search for.

  • parentChildren - allow searching through the children of parent nodes?

  • ignore - a list of objects to skip searching through (will still search parents & children of these objects).

a: "A"
b: "B"
middle: {
    d: "D"
    down: {
        a: "AD"
        c: "C"
    }
}

#[test]
fn test() {
    assertEq(self.middle.search("d"), ("D", 0));
    
    assertEq(self.middle.search("a"), ("AD", 1));
    assertEq(self.middle.search("a", true, [self.middle.down]), ("A", 1));
    
    assertEq(self.middle.search("c"), ("C", 1));
    assertEq(self.middle.search("b"), ("B", 1));
}

Object.searchUp(object: obj, field: str, parentChildren: bool = true, ignore: vec = []): (unknown, int)

Search this object for a field by name. Behaves just like "search", however, will only allow searching upwards, through this object's parents.

Object.searchDown(object: obj, field: str, currDist: int = 0, ignore: vec = []): (unknown, int)

Search this object for a field by name. Behaves just like "search", however, will only allow searching downwards, through this object's children.

Object Interface

Object.name(object: obj): str

Returns the name of this object. Typically, but not always the name of fields that reference this object.

record: {
    name: "Tom"
}

#[test]
fn test() {
    assertEq(self.name(), "root");
    assertEq(self.record.name(), "record");
    
    let array = [new { field: 42 }];
    let name = array.first().name(); // unique name - anonymous object in the doc
    let path = name + ".field";      // example only - referencing the obj by path
    assertEq(self.at(path), 42);
}

Object.id(object: obj): str

Returns this object's ID. Helpful for some debugging instances.

Object.parent(object: obj): obj

Returns this object's parent object, or null if this object is a root. It is preferred to use the "super" keyword when working with paths (if able).

Object.root(object: obj): obj

Returns this object's root object in the document.

root Variables: {
    Key: {
        token: "abcdefg"
    }
}

tests: {
    #[test]
    fn test() {
        assertEq(self.name(), "tests");
        assertEq(self.root().name(), "root");
        
        let key = Variables.Key;
        assertEq(key.root().name(), "Variables");
    }
}

Object.isRoot(object: obj): bool

Returns true if this object is a root object in the document.

Object.path(object: obj): str

Returns the dot-separated path to this object from a root of this document.

root Variables: {
    Space: {
        Info: {
            name: {
                first: "Joe"
            }
            
            #[test]
            fn test() {
                assertEq(self.path(), "Variables.Space.Info");
            }
        }
    }
}

other: {
    #[test]
    fn test() {
        assertEq(self.path(), "root.other");
    }
}

Object.children(object: obj): vec

Returns a vector containing this object's child objects, or an empty vector if this object does not have any children.

a: {}  // fields with values that point to child objects
b: {}
c: {}

#[test]
fn test() {
    assertEq(self.children().len(), 3);
}

Object.typename(object: obj): str

Returns the name of this object's type. If this object does not have a prototype, this will return "obj", the same as "typeof".

type CustomType {}

CustomType object: {}
regular: {}

#[test]
fn test() {
    assertEq(self.object.typename(), "CustomType");
    assertEq(self.regular.typename(), "obj");
    
    assertEq(typeof self.object, "obj");
    assertEq(typeof self.regular, "obj");
}

Object.typestack(object: obj): vec

Returns a vector containing the stack of type names associated with this object.

type CustomType {}
type SubType extends CustomType {}

SubType object: {}
regular: {}

#[test]
fn test() {
    assertEq(self.object.typestack(), ["SubType", "CustomType"]);
    assertEq(self.regular.typestack(), []);
}

Object.instanceOf(object: obj, type: str): bool

Returns true if this object has the type name "type" in its type stack.

type CustomType {}
type SubType extends CustomType {}

SubType object: {}

#[test]
fn test() {
    assert(self.object.instanceOf("SubType"));
    assert(self.object.instanceOf("CustomType"));
}

Object.prototype(object: obj): obj

Return this object's prototype object if it has one. The prototype object will most likely be referenced by other objects, so use this function carefully.

Object.setPrototype(object: obj, prototype: obj): void

Set this object's prototype to be the object "prototype".

type CustomType {
    fn hey(): str { return "hey"; }
}
CustomType typed: {}
untyped: {}

#[test]
fn get_set() {
    self.untyped.setPrototype(self.typed.prototype());
    assertEq(typename self.untyped, "CustomType");
    assertEq(self.untyped.hey(), "hey");
}

Object.upcast(object: obj): bool

Changes this object's prototype to be the prototype its current prototype extends.

type CustomType {}
type SubType extends CustomType {}

SubType object: {}

#[test]
fn test() {
    assert(self.object.instanceOf("SubType"));
    assert(self.object.upcast());
    
    assertNot(self.object.instanceOf("SubType"));
    assert(self.object.instanceOf("CustomType"));
    assertNot(self.object.upcast()); // prototype does not extend any other type
}

Object.removePrototype(object: obj): bool

Removes this object's prototype completely (regardless of whether it extends another), returning true if this object had a prototype that was removed.

type CustomType {}
type SubType extends CustomType {}

SubType object: {}

#[test]
fn test() {
    assert(self.object.removePrototype());
    assertEq(self.object.typename(), "obj");
}

Object.shallowCopy(object: obj, other: obj): void

Make "object" a shallow copy of "other" by attaching all of the data on "other" to it. Essentially, a "reference" call to each one of the fields on "other".

This function does not just shallow copy fields, but all data (functions, prototype, etc...).

type Custom {
    fn hey(): str { return "hey"; }
}

Custom first: {
    a: "a"
    b: "b"
}
second: {}

#[test]
fn test() {
    self.second.shallowCopy(self.first);
    
    assertEq(self.second.a, "a");
    assertEq(self.second.a, self.first.a);

    self.second.a = "modified";
    assertEq(self.second.a, "modified");
    assertEq(self.first.a, "modified");

    assertEq(self.second.hey(), "hey");
    assertEq(self.first.hey(), "hey");
}

Object.deepCopyFields(object: obj, other: obj): void

Copies all fields from "other" and places them on this object. This function is also called for every field on "other" that is an object, recursively deep copying all referenced (by field) sub-objects.

Unlike shallow copy, this function only operates on fields, not functions or any other type of data.

The object "other" is unmodified by this function.

type Custom {
    fn hey(): str { return "hey"; }
}

Custom first: {
    a: "a"
    b: "b"
}
second: {}

#[test]
fn test() {
    self.second.deepCopyFields(self.first);
    
    assertEq(self.second.a, "a");
    assertEq(self.second.a, self.first.a);

    self.second.a = "modified";
    assertEq(self.second.a, "modified");
    assertEq(self.first.a, "a");

    assertNull(self.second.hey);
    assertEq(self.first.hey(), "hey");
}

Last updated