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 "or" function.
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"));
}
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);
}
Box a field on this object without an additional assignment statement, optionally accepting a new value to box and set.
This function can be used like "set", but it makes sure that the value is boxed. If a field is not present, it will box a null value in preparation for an assignment.
value: 42
#[test]
fn box_field() {
assert(self.box("value"));
let v = self.value;
v = 100;
assertEq(self.value, 100);
}
#[test]
fn box_null_value() {
assert(self.box("dude", "hi"));
assertEq(self.dude, "hi");
let v = self.dude;
v = "hello";
assert(isBoxed(self.dude));
assertEq(self.dude, "hello");
}
Unbox a field on this object without an additional assignment statement, optionally accepting a new value to unbox and set.
Unlike the function "box", if the field is not present and a value is not given as an argument, this function will not set a null value and will return false.
This function can be used like "set", but it makes sure that the value is unboxed.
value: box(32)
#[test]
fn unbox_value() {
let v = self.value;
v = 10;
assertEq(self.value, 10);
assert(self.unbox("value"));
v = 100;
assertEq(self.value, 10);
}
#[test]
fn unbox_set() {
assertNot(self.unbox("dne")); // won't set without a given value...
assert(self.unbox("another", box(20)));
assertEq(self.another, 20);
let v = self.another;
v = 100;
assertEq(self.another, 20);
}
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.
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.
#[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.
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.
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).
Search this object for a field by name. Behaves just like "search", however, will only allow searching downwards, through this object's children.
Schemafy
Every object in Stof can be treated as a schema, capable of being applied to another object. We call this application "schemafy".
This is one of the most powerful and complex "standard" operations that Stof can perform, able to create new fields, remove lots of fields that are not defined, validate, control access, etc.
You might think that the interface is complex as well, but "schemafy" only looks at one singular attribute, #[schema(...)] and returns a boolean, reflecting the validity of the object in question (we call this the "target" object) with respect to this "schema" object.
schema - The first argument (or caller) of this function is considered the "schema" object. Fields defined on this object that have a #[schema] attribute will have that attribute applied to the "target" object's corresponding fields (by name).
target - The object that will be manipulated/validated using this "schema".
removeInvalidFields - If true, when a field is determined to be invalid (using the #[schema(..)] attribute), that field will be removed from the target object (including object fields). If false, invalid fields will be left on the target object (but the return value will still be false if an invalid field is found).
removeUndefinedFields - If true, after each schema field is applied to the target object, all fields on the target that are not defined on the schema object (#[schema] attribute or not) will be removed from the target object. Ex. the target object should only contain these few fields.
schema: {
// If you can think it, you can do it with the #[schema] attribute...
#[schema((value: int): bool => value >= 0 && value <= 90)]
field: 0
}
target: {
field: 90
}
#[test]
fn example() {
assert(self.schema.schemafy(self.target)); // assert the target is valid
self.target.field = 91;
assertNot(self.schema.schemafy(self.target)); // no longer valid
assertNull(self.target.field); // removeInvalidFields defaults to true
}
Schema Attribute Values
The #[schema(...)] attribute for "schemafy" is extremely powerful, enabling one to create new fields, conditionally validate values, etc. If you can think it, you can do it.
Objects
If an object value is given in the schema attribute, it will be treated as an additional schema to apply to the current field (like a proxy).
schema: {
#[schema((value: unknown): bool => set('s', 'm', 'l', 'xl', 'xxl').contains(value))]
field: ''
}
proxy: {
#[schema(super.schema)]
field: ''
}
#[test]
fn valid() {
let record = new {
field: 'xl';
};
assert(self.proxy.schemafy(record));
assertEq(record.field, 'xl');
}
#[test]
fn invalid() {
let record = new {
field: 'small';
};
assertNot(self.proxy.schemafy(record));
assertNull(record.field);
}
Null
A null attribute value will not do anything (just returns valid) unless the field is an object and the corresponding field on the target is also an object. In that case, the schema field object will be applied as the schema to the corresponding field object on the target.
schema: {
#[schema] // does nothing, just returns true (valid) for non-objects
field: ''
// This is the long way to do what #[schema] is doing below.
// However, this might be helpful if you don't want to propagate the behavior
// for "removeInvalidFields" & "removeUndefinedFields" to sub-objects.
// #[schema((value: obj): bool => self.name.schemafy(value))]
#[schema] // applies this object as the schema for the target "name" field
name: {
#[schema((value: str): bool => value.len() > 0)]
first: ''
#[schema((value: str): bool => value.len() > 0)]
last: ''
}
}
target: {
name: {
first: 'Bob'
last: 'Smith'
}
}
#[test]
fn example() {
assert(self.schema.schemafy(self.target));
self.target.name.first = '';
assertNot(self.schema.schemafy(self.target));
assertNull(self.target.name); // removeInvalidFields = true by default
}
Functions
The most common value contained within the attribute is a function. Schemafy functions can have up to 4 parameters and can either have a boolean return value (indicating validity), a null value (resulting in nothing being done), or anything other value, set as the new field value.
Parameters can be specified in any order, but are associated with arguments via name (and value type) at runtime.
target: obj - if found in the function's parameters, the target object that the schema is being applied to will be given as this argument.
schema: obj - if found in the function's parameters, the schema object being applied to the target will be given as this argument.
field: str - if found in the function's parameters, the name of the field that is currently being validated/manipulated will be given as this argument. If the type is a Box<str>, this will enable one to rename this field by setting this value in the function.
value: unknown - if found in the function's parameters, the current value associated with this field on the target will be given as this argument. Any type will work here as long as the name is "value". If the type is a box type (Ex. Box<unknown> instead of just unknown), this will box the field value and enable one to modify the field value by setting this value in the function (this is an alternative to returning a non-null and non-boolean value to be set as the new field value).
schema: {
// N.B. - marked as "invalid" if "value" cannot be cast to a string here or the function call fails...
// Avoid this scenario by setting "value" type to "undefined" or "Box<undefined>" if you aren't sure.
#[schema((target: obj, schema: obj, field: str, value: str): bool => true)] // return validity
a: ''
#[schema((schema: obj, target: obj, value: str, field: str): bool => true)] // ordering doesn't matter
b: ''
// If field names don't correlate with "target", "schema", "field", or "value", then they're set by type.
// First object seen is the "target" ("a" in this case), then "schema" ("b" in this case).
// First string seen is the "field" ("c" in this case).
// Any other value (or after the others if conflicting type) is set as the "value" ("d" in this case).
// Recommended to use the parameter names "target", "schema", "field", and "value" to avoid any issues.
#[schema((a: obj, b: obj, c: str, d: unknown): unknown) => d]
c: ''
// Not all parameters have to be given (could even have none).
#[schema((value: str): bool => value.len() > 0)]
d: ''
// If the return type is not "bool", then the return value is treated as the new field value (if not null).
// You can still modify a field value with a boolean return type if the value type is boxed.
#[schema((): str => 'always valid, and value always set (or created) to be this string')]
e: ''
}
Arrays
An array of objects and functions can be given as the schema attribute value, indicating that each function and additional schema should be evaluated in order, ceasing execution if the field is evaluated to be invalid.
schema: {
#[private]
hex_chars: {
let chars = 0..10;
for (char in chars) chars.set(index, char as str);
chars = chars as set;
chars.insert('a');
chars.insert('b');
chars.insert('c');
chars.insert('d');
chars.insert('e');
chars.insert('f');
return box(chars);
};
fn valid_hex_chars(hex: str): bool {
hex = hex.toLower().substring(1);
let valid = self.hex_chars;
for (char in hex) if (!valid.contains(char)) return false;
return true;
}
#[schema([
(value: Box<unknown>): bool => value != null, // has to exist
(value: Box<unknown>): bool => isString(value), // has to be a string
(value: Box<str>): bool => value.startsWith('#'), // has to start with '#'
(value: Box<str>): bool => value.len() == 4 || value.len() == 7, // has to be the right length
(value: Box<str>): bool => self.valid_hex_chars(value), // have to be valid chars
])]
hex: '#000'
}
#[test]
fn not_valid_char() {
let record = new { hex: '#aaaaag' };
assertNot(self.schema.schemafy(record));
assertNull(record.hex);
}
#[test]
fn valid_three() {
let record = new { hex: '#fff' };
assert(self.schema.schemafy(record));
assertEq(record.hex, '#fff');
}
#[test]
fn valid_hex() {
let record = new { hex: '#08fa2e' };
assert(self.schema.schemafy(record));
}
Execute
The "exec" functionality is another powerful paradigm in Stof. It orchestrates many objects together as tasks.
Object.exec(object: obj):void
Calls all functions on the object with a #[run] attribute. Also executes all object fields that also have a #[run] attribute.
The #[run] attribute value can optionally be a number, ordering the execution of sub-objects and functions together. By default, sub-objects have an order of -2 and functions have an order of -1 (sub-objects are executed first).
Functions that get executed should not have any parameters (or must have default values provided for all parameters).
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.
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.
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...).
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.