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.
Diese Seite gibt es noch nicht auf Deutsch, deshalb steht sie hier auf Englisch. Zur englischen Seite
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.