ktm v0.28.3

Attributes

An attribute is a word starting with @, written before a struct, a field or a parameter. It asks the compiler to write code for the declaration, or tells a library how to treat it. This chapter covers where attributes go, @json and @yaml and the methods they generate, the field attributes @key, @skip and @omitEmpty, the settings attributes @env and @required, the validation rules @min, @max, @email, @url and @oneOf, @caller on parameters, seeing the generated code, and reading attributes through reflection.

Where attributes go

An attribute is @name or @name(arguments), before what it applies to. Several can be written one after another:

@json @yaml
struct User {
    @key("e-mail") @required String email;
    @skip i64 cacheHits;
};
AttributeGoes beforeWhat it does
@jsona structgenerates toJson, fromJson, freeDecoded
@yamla structgenerates toYaml, fromYaml (and freeDecoded)
@key("name")a fieldthe field's key in JSON and YAML, its column for ktm-pg
@skipa fieldleaves the field out of JSON, YAML, settings and validation
@omitEmptya fieldleaves it out of JSON and YAML when it is 0, empty or null
@env("NAME")a fieldits environment variable for config.fromEnv
@requireda fieldconfig.fromEnv fails without it; validate.check without a value
@min(n), @max(n)a fieldvalidate.check: a number's bounds, a text's or slice's length
@email, @urla fieldvalidate.check: an email address, an absolute URL
@oneOf("a", "b")a fieldvalidate.check: one of the values
@caller("file"), ("line"), ("func")a parameterfilled by every call with where the call is

An attribute ktm doesn't know, or one in the wrong place, is a compile error:

does not compilestruct User {
    @jsonn u8 *name;
};

i32 main() {
    return 0;
}
unknown attribute `@jsonn`

@json

@json before a struct makes the compiler write a JSON encoder and decoder for it, as methods. The struct's file must import json. json.decode<T> and json.encode then use them:

import json, reflect;

@json struct User {
    i64 id;
    String name;
    bool admin;
};

i32 main() {
    User u = json.decode<User>("{\"id\": 7, \"name\": \"Ada\", \"admin\": true}");
    printf("%d %s %d\n", u.id, u.name, u.admin);
    u.id = 8;
    scoped String text = json.encode(reflect.of(u));
    printf("%s\n", text);
    u.freeDecoded();
    return 0;
}
7 Ada 1
{"id":8,"name":"Ada","admin":true}
import json, reflect;

enum Role { guest, admin };

@json struct Tag {
    String name;
    i64 weight;
};

@json struct User {
    i64 id;
    Role role;
    Tag[] tags;
};

i32 main() {
    User u = json.decode<User>("{\"id\": 1, \"role\": \"admin\", \"tags\": [{\"name\": \"math\", \"weight\": 3}], \"since\": 1843}");
    printf("%d %s %d %s\n", u.id, reflect.name(u.role), u.tags.len, u.tags[0].name);
    scoped String text = json.encode(reflect.of(u));
    printf("%s\n", text);
    u.freeDecoded();
    return 0;
}
1 admin 1 math
{"id":1,"role":"admin","tags":[{"name":"math","weight":3}]}

Without @json the same calls work by reflection, about three times slower to decode; adding @json makes existing code faster with no other change. The struct's file must import json:

does not compile@json struct User {
    i64 id;
};

i32 main() {
    return 0;
}
needs `import json;`

The generated methods

MethodDoes
u.toJson(&w)writes u to a json.Writer
u.fromJson(&r)reads u from a json.Reader
u.freeDecoded()frees what fromJson (or decode) allocated
u.toYaml(&w), u.fromYaml(&r)the same for YAML, with @yaml
import json;

@json struct Point {
    i64 x;
    i64 y;
};

i32 main() {
    Point p = {3, 4};
    json.Writer w = json.writerNew();
    p.toJson(&w);
    scoped String text = w.take();
    w.free();
    printf("%s\n", text);
    return 0;
}
{"x":3,"y":4}

A struct with @json and no free() of its own can be declared scoped: it is freed with freeDecoded() (see defer and scoped).

@yaml

@yaml does the same for YAML, and the two go together on one struct:

import json, yaml;

@json @yaml struct Server {
    String host;
    i64 port;
    String[] aliases;
};

i32 main() {
    scoped Server s = json.decode<Server>("{\"host\": \"example.com\", \"port\": 443, \"aliases\": [\"www\", \"api\"]}");
    yaml.Writer y = yaml.writerNew();
    s.toYaml(&y);
    scoped String doc = y.take();
    y.free();
    printf("%s", doc);
    return 0;
}
host: example.com
port: 443
aliases:
  - www
  - api

Field attributes for JSON and YAML

@key("name") is the field's key in the text, @skip leaves the field out entirely, and @omitEmpty leaves it out of the output when it is 0, empty or null:

import json;

@json struct User {
    i64 id;
    @key("e-mail") String email;
    @omitEmpty String nickname;
    @skip i64 cacheHits;
};

i32 main() {
    scoped User u = json.decode<User>("{\"id\": 7, \"e-mail\": \"ada@example.com\"}");
    u.cacheHits = 99;
    json.Writer w = json.writerNew();
    u.toJson(&w);
    scoped String text = w.take();
    w.free();
    printf("%s\n", text);
    return 0;
}
{"id":7,"e-mail":"ada@example.com"}

nickname is empty, so @omitEmpty left it out; cacheHits is @skip, so it is never written, whatever its value.

Strict decoding refuses keys the struct doesn't have and keys it misses, but a @skip field is never required:

import json;

@json struct Point {
    i64 x;
    i64 y;
    @skip i64 cached;
};

i32 main() {
    u8[] inputs[3] = {"{\"x\": 1, \"y\": 2}", "{\"x\": 1}", "{\"x\": 1, \"y\": 2, \"z\": 3}"};
    for (i64 i = 0; i < 3; i++) {
        try {
            Point p = json.decode<Point>(inputs[i], json.STRICT);
            printf("ok: %d %d\n", p.x, p.y);
        } catch (Error e) {
            printf("%s\n", e.message);
        }
    }
    return 0;
}
ok: 1 2
1:9: y: missing
1:22: z: no such field in Point

A field whose type can't be written (a union, a function pointer, a struct without @json when the others need it) is an error at that field; mark it @skip.

Settings: @env and @required

config.fromEnv fills a struct of settings from environment variables. A field's variable is a prefix and its name in SNAKE_CASE (databaseUrl with APP_ reads APP_DATABASE_URL). @env("NAME") names the variable exactly, and @required makes a missing one an error:

import config, proc, reflect;

struct Settings {
    i64 port;
    @required String databaseUrl;
    @env("LOG_LEVEL") u8[] level;
};

i32 main() {
    proc.setenv("APP_DATABASE_URL", "postgres://localhost/app");
    proc.setenv("LOG_LEVEL", "warn");
    Settings s;
    s.port = 8080;
    config.fromEnv(reflect.of(s), "APP_");
    printf("%d %s %.*s\n", s.port, s.databaseUrl, s.level.len, s.level.ptr);
    config.free(reflect.of(s));
    proc.unsetenv("APP_DATABASE_URL");
    try {
        config.fromEnv(reflect.of(s), "APP_");
    } catch (Error e) {
        printf("%s\n", e.message);
    }
    return 0;
}
8080 postgres://localhost/app warn
config: APP_DATABASE_URL is required

A field keeps what it holds when its variable isn't set, so defaults are whatever the struct holds first (port above). See config.

Validation rules

validate.check checks a struct's values against rules written on its fields, and collects one error per broken rule, naming the field:

import reflect, str, validate;

struct Signup {
    @required @email u8[] email;
    @required @min(3) @max(20) String name;
    @min(18) i64 age;
    @oneOf("free", "pro") u8[] plan;
};

i32 main() {
    Signup s;
    s.email = "ann@example";
    s.name = str.from("an");
    s.age = 17;
    s.plan = "gold";
    validate.Errors errs;
    if (!validate.check(reflect.of(s), &errs)) {
        for (i64 i = 0; i < errs.items.len; i++) {
            validate.FieldError e = errs.items.get(i);
            printf("%s: %s\n", e.field, e.message);
        }
    }
    errs.free();
    s.name.free();
    return 0;
}
email: not an email address
name: must have at least 3 characters
age: must be at least 18
plan: must be one of free, pro
RuleOnHolds when
@requiredany fieldthe value isn't 0, false, "", an empty slice or null
@min(n), @max(n)a numberits value is at least / at most n
@min(n), @max(n)String, u8[]its length in characters is at least / at most n
@min(n), @max(n)a sliceit has at least / at most n items
@emailString, u8[]it looks like name@example.com
@urlString, u8[]it is an absolute URL with a host
@oneOf("a", "b")String, u8[]it is exactly one of the values

See validate for the error list's methods and HTTP use.

@caller on parameters

A parameter marked @caller("file"), @caller("line") or @caller("func") is filled in by every call with where that call is written. Callers never pass it:

void trace(@caller("file") u8 *file, @caller("line") i64 line, @caller("func") u8 *func, u8 *what) {
    printf("%s:%d in %s: %s\n", file, line, func, what);
}

void load() {
    trace("loading");
}

i32 main() {
    trace("start");
    load();
    return 0;
}
example.ktm:10 in main: start
example.ktm:6 in load: loading

Seeing the generated code

ktm --expand prints the methods @json and @yaml write, on stderr:

$ ktm --expand user.ktm -o user

Attributes through reflection

The type information of a struct carries its attributes: whether it is @json, and each field's key and @skip (see Reflection):

import json, reflect;

@json struct Item {
    @key("item-id") i64 id;
    @skip i64 internal;
    u8 *label;
};

i32 main() {
    reflect.Type *t = reflect.typeOf(Item);
    printf("json: %d\n", t.json);
    for (i64 i = 0; i < t.fields.len; i++) {
        reflect.Field f = t.fields[i];
        printf("%.*s key=%.*s skip=%d\n", f.name.len, f.name.ptr, f.key.len, f.key.ptr, f.skip);
    }
    return 0;
}
json: 1
id key=item-id skip=0
internal key=internal skip=1
label key=label skip=0

Field.key is the key a field is written under: its @key, or its name.

Rules