Prototypes & Schemas
A prototype is a named object template — useful for type-casting, structured creation, and, paired with #[schema(...)], validation. This is the same mechanism behind typename reporting something other than obj, back in Types & Units.
Declaring a Prototype
#[type] marks the object declaration that follows it as a named template. new TypeName { } creates an instance, filling in whatever defaults the template doesn't override:
#[type]
Customer: {
str id: ''
str plan: 'free'
fn greeting() -> str {
`Hello, ${self.id}`
}
}
#[main]
fn main() {
const c = new Customer { id: 'cust_1' };
pln(c.plan, c.greeting());
}The <TypeName> Path Shortcut
<TypeName> is a path directly to the prototype object itself, distinct from any instance of it — the same absolute-path mechanism from The Document Graph, just with its own syntax. Calling a function on it runs with the prototype object as self:
#[type]
Greeter: {
str prefix: 'Hello'
fn greet(name: str) -> str {
`${self.prefix}, ${name}!`
}
}
#[main]
fn main() {
pln(<Greeter>.greet('World'));
pln(<Greeter>.prefix);
}Constructors
#[constructor] marks a function to run automatically on every new instance:
#[type]
Point2D: {
float x: 0
float y: 0
#[constructor]
fn init() {
self.initialized = true;
}
}
#[main]
fn main() {
const p = new Point2D { x: 1, y: 2 };
pln(p.x, p.y, p.initialized);
}Inheritance with #[extends]
A prototype can extend another by name or by reference — the base type's constructor always runs first:
#[type]
Point2D: {
float x: 0
float y: 0
#[constructor]
fn init() {
self.isapoint = true;
}
}
#[type]
#[extends('Point2D')]
Point: {
float z: 0
#[constructor]
fn init() {
self.initialized = true;
}
}
#[main]
fn main() {
const p = new Point { x: 1, y: 2, z: 3 };
pln(p.isapoint, p.initialized, p.z);
}Inheritance is just a type chain, so easier to just cast directly:
#[type]
Point2D: {
float x: 0
float y: 0
fn length() -> m { Num.sqrt(self.x.pow(2) + self.y.pow(2)) }
}
#[type]
Point2D Point: {
float z: 0
fn length() -> m { Num.sqrt(self.x.pow(2) + self.y.pow(2) + self.z.pow(2)) }
}
#[main]
fn main() {
const p = new Point { x: 1m, y: 2ft, z: 300cm };
pln(str(p.length().round(2)), str(p.length<Point2D>().round(2)));
}Auto-Casting Typed Fields
A field declared with a prototype type auto-casts a plain new { } object assigned to it, merging in whatever defaults the prototype provides:
#[type]
SubType: {
str one: 'one'
str two: 'two'
}
#[type]
SuperType: {
SubType sub: {}
str msg: ''
}
#[main]
fn main() {
const o = new SuperType {
msg: 'hi',
sub: new { one: 'ONE' }
};
pln(typename o.sub, o.sub.one, o.sub.two);
}o.sub only specified one — two still came from SubType's own default.
Schemas: Validating with #[schema]
#[schema((target_value: T): bool => ...)] on a field attaches a validator; schemafy(target) checks an object against every field that has one:
#[type]
FirstSchema: {
#[schema((target_value: str): bool => target_value.len() > 2)]
first: "First"
}
#[main]
fn main() {
const target = new { first: "Bob" };
pln(<FirstSchema>.schemafy(target));
target.first = "AJ";
pln(<FirstSchema>.schemafy(target));
}Combining Validators
A list of functions on #[schema(...)] runs as a pipeline — each has to pass, in order, with short-circuiting:
fn is_string(target_val: unknown) -> bool {
"str" == typeof target_val
}
email_validation: [
self.is_string,
(target_val: str): bool => target_val.contains("@"),
]
#[schema(self.email_validation)]
email: "info@example.com"
#[main]
fn main() {
const target = new { email: "notvalid" };
pln(self.schemafy(target));
target.email = "info@stof.dev";
pln(self.schemafy(target));
}Sub-Schemas
A bare #[schema], with no function, tells schemafy to recurse into that field if the target's value is also an object:
#[type]
Schema: {
#[schema]
sub: {
#[schema((target_val: int): bool => target_val >= 0)]
field: 0
}
}
#[main]
fn main() {
const target = new {
sub: new { field: -42 }
};
pln(self.Schema.schemafy(target));
}Cleaning Up with remove_invalid
schemafy can also modify the target directly — remove_invalid = true strips any field that failed validation instead of just reporting the failure. (remove_undefined = true is the sibling option, for stripping fields the schema doesn't mention at all.)
fn is_string(target_val: unknown) -> bool {
"str" == typeof target_val
}
#[schema(self.is_string)]
label: "default"
#[main]
fn main() {
const target = new { label: 42 };
pln(self.schemafy(target, remove_invalid = true));
pln(target.label);
}