Write a configuration file
Declare a type for the configuration, write the file as TorbScript against it, and load it through the sandbox with the capabilities you grant.
TorbScript is its own configuration format. A configuration file is an ordinary .trb file that runs as the body of a
receiver closure against a type you declared, so it is statically type checked, autocompleted in an editor, and sandboxed
when it runs.
Steps#
Declare a type for the configuration. Every field that the file may set is a
varfield with a default. A field without a default is a value the file has to provide.type ServerConfig { var host: String = "localhost" var port: Int = 8080 var database: DatabaseConfig = DatabaseConfig() }Add a method for anything that is more than "set a field". A field is set with
=on its name, and a nested field is configured in place by a command with a block. Only a real operation needs a method.var fn route(path: String, to: String) { routes.append Route(path, to) }Write the configuration file against that type. Names inside it resolve against the receiver, so no prefix is needed. A field is written with
=; a command with a block configures the field's value in place.host = "0.0.0.0" port = 8080 database { url = "postgres://localhost:5432/app" } for name in ["users", "orders"] { route "/api/{name}", to: name }It is a program, so a loop, a
constand anifall work. There is no template language.Load it and apply it. Loading and running are two steps with different failures:
Sandbox.loadreports what is wrong with the file - a syntax error, a type error against the receiver, a module it may not import - andScript.applyreports what went wrong while it ran, including a step, memory or time limit and a panic inside the script.const script = Sandbox.load<ServerConfig>("./config.trb")? var config = ServerConfig() script.apply(config)?Grant capabilities at the call site, never in a project file. Without a block the script has no file system, no network, no clock, no environment and no foreign functions. Whoever loads it decides what it may reach.
const script = Sandbox.load<ServerConfig>("./config.trb") { modules "std/text", "std/time" files readOnly: "./config" limits steps: 1_000_000 }?Use the same mechanism for a project manifest.
project.trbis exactly this with the receiverProject, which is whyname = "acme/shop"works and why reading the metadata of a package is safe.
Pitfalls#
- Only the innermost receiver is implicit. Inside
database { ... }the names resolve againstDatabaseConfigand not againstServerConfig. To reach the outer one, name the parameter:server { s => s.database { url = "{s.host}/db" } }. - A field is written only with
=, and calling it never writes it.onStart = { ... }assigns the closure to the field;onStart()calls what is in it. That is the whole rule, and it has no exception. - A field the file sets has to be
var. Aconstfield is fixed after construction, so a configuration file cannot write it. Sandbox.loadresolves its path against the project directory, not against the file that calls it. An import (use Name from "./x") is the other way round, because that is a question about the source tree.- A field's value is an ordinary expression after
=, so an operator needs no extra parentheses.tls = port == 8443is assignment, not a command call, so the command-call rule about an operator at the top level of an argument does not apply to it.
Full example#
The receiver type, and a program that configures a value of it in place through a receiver closure. This is the same mechanism a file uses, without the sandbox.
type DatabaseConfig {
var url: String = ""
var poolSize: Int = 10
}
type Route {
path: String
handler: String
}
type ServerConfig {
var host: String = "localhost"
var port: Int = 8080
var database: DatabaseConfig = DatabaseConfig()
protected var routes: List<Route> = []
/** More than setting a field, so it is a method. */
var fn route(path: String, to: String) {
routes.append Route(path, to)
}
}
/** Takes a receiver closure over a fresh configuration and answers the configured value. */
fn server(configure: (var self: ServerConfig) => Void): ServerConfig {
var config = ServerConfig()
configure config
config
}
const config = server {
host = "0.0.0.0"
port = 9000
database {
url = "postgres://localhost:5432/app"
poolSize = 20
}
route "/health", to: "health"
}
print "{config.host}:{config.port} {config.database.url} {config.routes.length()}"Related#
- Command calls - why
route "/health", to: "health"is written without parentheses. - Declaring a type -
varfields,protected varand methods. - Read a file - the plain way to read text when no receiver is involved.
- Install and run - the
project.trbof a new project.