Referenz

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#

  1. Declare a type for the configuration. Every field that the file may set is a var field 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()
    }
  2. 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)
    }
  3. 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 const and an if all work. There is no template language.

  4. Load it and apply it. Loading and running are two steps with different failures: Sandbox.load reports what is wrong with the file - a syntax error, a type error against the receiver, a module it may not import - and Script.apply reports 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)?
  5. 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
    }?
  6. Use the same mechanism for a project manifest. project.trb is exactly this with the receiver Project, which is why name = "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 against DatabaseConfig and not against ServerConfig. 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. A const field is fixed after construction, so a configuration file cannot write it.
  • Sandbox.load resolves 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 == 8443 is 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()}"
Esc