Varyk 0.3.0: data, config, logging, and tests
Varyk 0.3.0 is on crates.io. It is milestone 5a of the roadmap, the first part of the batteries for services. Milestone 4 made the everyday code of a service easy to write in Varyk, a language for backend services that compiles to Rust. Milestone 5a adds what a service does with data before it talks to the network: read and write JSON, take its settings from the environment, write logs, and test itself.
What is in it
varyk-std and Error. A new crate, varyk-std, is the one crate Varyk code reaches without a facade; every other crate is still reached through a .rs file. It is behind Error, a built-in error type with a message: Error::new("...") makes one, e.message() reads it, and it prints with {}. Every standard call that can fail returns Result<T, Error>, so ? works across all of them with no conversions.
JSON, with attributes. json::parse reads a struct from JSON text and json::stringify writes one. Structs, Option, Vec, HashMap with string keys, and enums whose variants carry no data all go through. Three attributes adjust the keys: #[rename("userName")] on a field or a variant, #[default(18)] for a field the text leaves out, and #[skip] for a field that is neither read nor written, such as a password hash. Varyk derives serde's traits only for the types a json or env call reaches, so a program that never calls json or env has no serde in it, and a type that cannot go through JSON is an error at the call.
Configuration from the environment. let c: Config = env::parse()?; fills a struct from environment variables, then from a .env file in the current directory, then from #[default]. A field database_url is read from DATABASE_URL, and #[rename] changes that. A variable that is not set, or does not read as its type, is an Error that names it.
Logging. log::debug, log::info, log::warn, and log::error take a format string and arguments exactly as println! does, and write a line to stderr. LOG sets the level, info when unset, and LOG_FORMAT=json writes one JSON object per line instead of text, with no change to the code.
Tests. A function marked #[test] is a test, in any module, and assert and assert_eq check results inside one. varyk test builds the program with its tests and runs them; varyk build and varyk run leave them out. A failing check names the Varyk file and line.
varyk init, varyk add, and the version check. varyk init now writes varyk-std into Cargo.toml at the compiler's own version, and .env into .gitignore, so a local secrets file is never committed. varyk add runs cargo add in the package with the arguments you give. varyk check reads the varyk-std line and Cargo.lock, and when they do not match the compiler it says so as V0404, with the line to write or cargo update -p varyk-std to run.
New diagnostic codes. V0112 is an attribute Varyk does not have, or one in a place it cannot go. V0113 is a reserved name. V0114 is a test with parameters or a return type, a call to a test, or assert outside a test. V0209 is an attribute value that does not fit, and V0210 a type that cannot go through JSON or the environment. V0404 is the varyk-std version check. The language reference has the full table.
Three new example programs and a new package show it in use, and the examples page has them with their output. The users package puts them together: a store of users read from JSON, configuration from a .env file, logging, and three tests. This is json.vr:
enum Role {
Admin,
#[rename("member")]
Member,
}
struct User {
id: u32,
#[rename("userName")]
user_name: string,
role: Role,
nickname: Option<string>,
tags: Vec<string>,
#[default(18)]
age: u8,
#[skip]
password_hash: Option<string>,
}
fn role_name(role: Role) -> string {
match role {
Role::Admin => "admin",
Role::Member => "member",
}
}
fn load(body: string) -> Result<User, Error> {
let u: User = json::parse(body)?;
Ok(u)
}
fn main() {
let text = "{\"id\": 7, \"userName\": \"ann\", \"role\": \"member\", \"tags\": [\"a\", \"b\"], \"password_hash\": \"x\"}";
match load(text) {
Ok(u) => {
println!("{} {} {}", u.id, u.user_name, role_name(u.role));
println!("{} tags, age {}, nickname: {}", u.tags.len(), u.age, u.nickname.is_some());
println!("{}", json::stringify(u));
}
Err(e) => println!("error: {}", e),
}
let bad = "{\"id\": 1, \"userName\": \"bo\", \"role\": \"Admin\", \"tags\": [], \"age\": 300}";
match load(bad) {
Ok(u) => println!("{}", u.id),
Err(e) => println!("error: {}", e),
}
}
It prints:
7 ann member
2 tags, age 18, nickname: false
{"id":7,"userName":"ann","role":"member","nickname":null,"tags":["a","b"],"age":18}
error: invalid value: integer `300`, expected u8 at line 1 column 67The breaking changes
s.parse() returns Result<T, Error> instead of Option<T>, so ? works on it in a function that returns Result<_, Error>, and the error names the text. Code that wanted the old Option writes two statements, since the expected type does not reach a method's receiver:
let r: Result<i32, Error> = text.parse();
let n = r.ok();
The new names are reserved: json, env, and log cannot name a module, a struct, an enum, or a use; Error cannot name a struct, an enum, a module, or a use; assert and assert_eq cannot name a function; and no function, method, struct, enum, module, or use name may start with varyk_, which is kept for what the compiler adds to the Rust it writes. Local variables and fields may still use any of these names.
The three crates, varyk, varyk-syntax, and varyk-std, are released together as 0.3.0. A package that uses varyk-std needs it in [dependencies] at the compiler's version: after cargo install varyk, run varyk check, and it says what to change.
Try it
You need a stable Rust toolchain installed through rustup.
cargo install varyk
varyk init hello
cd hello
varyk run
The getting started page goes from there, and the language reference describes everything the compiler accepts.
What is next
Milestone 5 is now three milestones. Milestone 5b1 is async/await on a built-in runtime and spawn, gated on two open questions, the runtime shape and ownership-transfer syntax. Milestone 5b2 is HTTP and the database: an HTTP server and client on one stack, and databases through one API. The bar for all three is one golden path: a users API on a database is varyk init, one file, and varyk run away, within fifteen minutes of cargo install varyk. It is met at the end of 5b2. The roadmap has every item, and nothing there is a date.
Varyk is experimental and pre-1.0. Try writing one small service and tell us where it hurts: the issues are the place.