Configuration

Kit provides rich support for reading and writing application configuration files. The standard library includes TOML and JSON parsers out of the box, and additional formats are available as packages.

Overview

Kit supports the following configuration formats:

Format Source Import Human-Readable
TOML Standard Library import Encoding.TOML Yes
JSON Standard Library import Encoding.JSON Yes
YAML Package (kit-yaml) import Kit.YAML as YAML Yes
EDN Package (kit-edn) import Kit.EDN as EDN Yes
XML Package (kit-xml) import Kit.XML as XML Yes

TOML and JSON are built into the standard library and require no additional dependencies. The remaining formats are available as packages that can be added with kit add.

Reading Config Files

Kit uses a capability-based security model for file system access. Rather than allowing any code to read files freely, your program receives an Env value at startup that carries a root authority token. You derive specific, narrower capabilities from this token and pass them explicitly to functions that perform I/O.

This design makes it visible in every function signature exactly what I/O operations it can perform, and ensures that untrusted code cannot access the file system unless you explicitly grant it permission.

The Capability Pattern

Reading a config file requires three imports and two steps:

import Auth.File.{file-auth}       # Authority derivation
import IO.FileAuth.{read-file}       # Capability-aware file operations
import Encoding.TOML                 # (or JSON, YAML, etc.)

main = fn(env: Env) =>
  # Step 1: Derive a FileAuth token from the root authority
  fa = file-auth env.root

  # Step 2: Pass the token to read-file
  match read-file fa "config.toml"
  | Ok contents -> println contents
  | Err e -> println "Error: ${e}"

main

Authority Hierarchy

The Auth.File module provides three levels of file authority, derived from the root:

RootAuth  (env.root)
    |
    +-- FileAuth          # Read + write + delete (full access)
            |
            +-- FileReadAuth   # Read-only access
            +-- FileWriteAuth  # Write-only access

Stronger authorities can create weaker ones, but not the reverse. This lets you follow the principle of least authority—if a function only needs to read config, give it a FileReadAuth:

import Auth.File.{file-auth, file-read-auth}
import IO.FileAuth.{read-file}

load-config = fn(ra, path) =>
  # ra is a FileReadAuth — can only read, not write or delete
  read-file ra path

main = fn(env: Env) =>
  fa = file-auth env.root
  ra = file-read-auth fa
  match load-config ra "config.toml"
  | Ok contents -> println contents
  | Err e -> println "Error: ${e}"

main

Available File Operations

The IO.FileAuth module wraps all file and directory builtins behind capability checks:

Function Required Auth Description
read-file auth path FileReadAuth or FileAuth Read file as string
read-file-bytes auth path FileReadAuth or FileAuth Read file as byte list
read-file-lines auth path FileReadAuth or FileAuth Read file as list of lines
file-exists? auth path FileReadAuth or FileAuth Check if file exists
write-file auth path content FileWriteAuth or FileAuth Write string to file
append-file auth path content FileWriteAuth or FileAuth Append string to file
delete-file auth path FileAuth Delete a file
copy-file auth src dest FileAuth Copy a file

TOML

TOML (Tom's Obvious Minimal Language) is the recommended format for Kit application configuration. It is also the format used by Kit's own kit.toml manifest files.

Parsing TOML

import Auth.File.{file-auth}
import Encoding.TOML
import IO.FileAuth.{read-file}

# Read and parse a TOML config file
main = fn(env: Env) =>
  fa = file-auth env.root
  match read-file fa "config.toml"
  | Ok contents ->
      match TOML.parse contents
      | Ok config ->
          # Access typed values
          name = TOML.get-string config "name" ?? "MyApp"
          port = TOML.get-int config "port" ?? 8080
          debug = TOML.get-bool config "debug" ?? false
          println "Starting ${name} on port ${show port} (debug: ${show debug})"
      | Err e -> println "Parse error: ${Error.message e}"
  | Err e -> println "Read error: ${e}"

main

TOML Value Types

Kit represents TOML documents using the TomlValue algebraic data type:

type TomlValue =
  | TomlString String
  | TomlInt Int
  | TomlFloat Float
  | TomlBool Bool
  | TomlArray [TomlValue]
  | TomlTable [(String, TomlValue)]

Typed Accessors

The TOML module provides type-safe getter functions that return Option values. Each returns None if the key is missing or the value has a different type.

# Access specific types directly
TOML.get-string config "name"     # => Option String
TOML.get-int config "port"        # => Option Int
TOML.get-float config "rate"      # => Option Float
TOML.get-bool config "debug"      # => Option Bool

# Access raw TomlValue (useful for nested tables)
TOML.get config "database"         # => Option TomlValue

# Access array elements by index
TOML.at array-val 0               # => Option TomlValue

# Type predicates
TOML.is-string? val              # => Bool
TOML.is-table? val               # => Bool

Nested Tables

TOML tables naturally map to sections in a configuration file:

# Given config.toml:
#   [database]
#   host = "localhost"
#   port = 5432
#   name = "mydb"

match TOML.get config "database"
| Some db ->
    host = TOML.get-string db "host" ?? "localhost"
    port = TOML.get-int db "port" ?? 5432
    db-name = TOML.get-string db "name" ?? "app"
    println "Connecting to ${host}:${show port}/${db-name}"
| None -> println "No database section found"

Generating TOML

# Build TOML values programmatically
config = TomlTable [
  ("name", TomlString "MyApp"),
  ("version", TomlInt 1),
  ("debug", TomlBool false),
  ("database", TomlTable [
    ("host", TomlString "localhost"),
    ("port", TomlInt 5432)
  ])
]

output = TOML.stringify config
println output

JSON

JSON (JavaScript Object Notation) is a ubiquitous data format with broad tooling support. Kit's JSON support is RFC 8259 compliant and built into the standard library.

Parsing JSON

import Auth.File.{file-auth}
import Encoding.JSON
import IO.FileAuth.{read-file}

# Parse a JSON configuration file
main = fn(env: Env) =>
  fa = file-auth env.root
  match read-file fa "config.json"
  | Ok contents ->
      match JSON.parse contents
      | Ok config ->
          name = JSON.get-string config "name" ?? "MyApp"
          port = JSON.get-int config "port" ?? 8080
          println "Config loaded: ${name} on port ${show port}"
      | Err e -> println "Parse error: ${Error.message e}"
  | Err e -> println "Read error: ${e}"

main

JSON Value Types

type JSONValue =
  | JSONNull
  | JSONBool Bool
  | JSONInt Int
  | JSONFloat Float
  | JSONString String
  | JSONArray [JSONValue]
  | JSONObject [(String, JSONValue)]

Typed Accessors

# Same pattern as TOML: type-safe getters returning Option
JSON.get-string config "name"     # => Option String
JSON.get-int config "port"        # => Option Int
JSON.get-float config "rate"      # => Option Float
JSON.get-bool config "debug"      # => Option Bool
JSON.get-array config "items"     # => Option [JSONValue]
JSON.get-object config "db"       # => Option [(String, JSONValue)]

# Raw access and array indexing
JSON.get config "database"         # => Option JSONValue
JSON.at array-val 0               # => Option JSONValue

Nested Objects

# Given config.json:
# {
#   "database": {
#     "host": "localhost",
#     "port": 5432
#   },
#   "features": ["auth", "logging"]
# }

match JSON.get config "database"
| Some db ->
    host = JSON.get-string db "host" ?? "localhost"
    println "Database host: ${host}"
| None -> println "No database config"

# Iterate over an array
match JSON.get-array config "features"
| Some features ->
    List.for-each (fn(f) =>
      match f
      | JSONString s -> println "Feature: ${s}"
      | _ -> no-op
    ) features
| None -> no-op

Generating JSON

# Build JSON values and stringify
config = JSONObject [
  ("name", JSONString "MyApp"),
  ("version", JSONInt 1),
  ("features", JSONArray [
    JSONString "auth",
    JSONString "logging"
  ])
]

output = JSON.stringify config
println output
# => {"name":"MyApp","version":1,"features":["auth","logging"]}

YAML

YAML (YAML Ain't Markup Language) is a human-friendly data serialization format popular for configuration files. Available via the kit-yaml package.

# Install the package
kit add gitlab.com/kit-lang/packages/kit-yaml.git

Parsing YAML

import Kit.YAML as YAML

# YAML content using heredoc
yaml-content = <<~YAML
  name: My Application
  version: "1.0.0"
  debug: true
  port: 8080
YAML

main = fn =>
  doc = YAML.parse yaml-content

  match YAML.get-string doc "name"
  | Some name -> println "App: ${name}"
  | None -> println "Name not found"

  match YAML.get-int doc "port"
  | Some p -> println "Port: ${show p}"
  | None -> println "Port not found"

  match YAML.get-bool doc "debug"
  | Some d -> println ("Debug: " ++ (if d then "on" else "off"))
  | None -> println "Debug not found"

main

YAML Value Types

type YamlValue =
  | YamlString String
  | YamlInt Int
  | YamlFloat Float
  | YamlBool Bool
  | YamlNull
  | YamlList [YamlValue]

Generating YAML

# Generate YAML from key-value pairs
output = YAML.generate [
  {key: "name", value: YamlString "MyApp"},
  {key: "version", value: YamlInt 1},
  {key: "debug", value: YamlBool true}
]

println output
# => name: MyApp
# => version: 1
# => debug: true

EDN

EDN (Extensible Data Notation) is a data format from the Clojure ecosystem. It supports rich types including keywords, symbols, sets, and tagged literals. Available via the kit-edn package.

# Install the package
kit add gitlab.com/kit-lang/packages/kit-edn.git

Parsing EDN

import Kit.EDN as EDN

# Parse an EDN configuration map
match EDN.parse "{:name \"Kit\" :version 1 :debug true}"
| Ok value -> println "Parsed: ${EDN.stringify value}"
| Err e -> println "Error: ${Error.message e}"

# Parse keywords and vectors
match EDN.parse "[:auth :logging :metrics]"
| Ok value -> println "Features: ${EDN.stringify value}"
| Err e -> println "Error: ${Error.message e}"

Generating EDN

# Stringify Kit values to EDN
record = {name: "Kit", version: 1, active: true}
edn-str = EDN.stringify record
println edn-str

# Pretty-print EDN
pretty-output = EDN.pretty record
println pretty-output

EDN is a good choice when interoperating with Clojure systems or when you need built-in support for keywords, symbols, and sets.

XML

XML (Extensible Markup Language) is supported through the kit-xml package, which wraps libxml2. It provides full XML parsing, XPath queries, and namespace support.

# Install the package
kit add gitlab.com/kit-lang/packages/kit-xml.git

Parsing XML Configuration

import Kit.XML as XML

xml-config = "<?xml version='1.0'?>
<config>
  <app name='MyApp' version='1.0'/>
  <database>
    <host>localhost</host>
    <port>5432</port>
  </database>
</config>"

main = fn =>
  match XML.parse xml-config
  | Ok doc ->
      defer XML.free doc

      # Use XPath to extract values
      match XML.xpath-first doc "//database/host/text()"
      | Ok host -> println "Database host: ${host}"
      | Err e -> println "Error: ${Error.message e}"

      match XML.xpath-first doc "//database/port/text()"
      | Ok port -> println "Database port: ${port}"
      | Err e -> println "Error: ${Error.message e}"

      # Read attributes
      match XML.root doc
      | Ok root ->
          match XML.find-child root "app"
          | Some app-node ->
              match XML.attr app-node "name"
              | Some name -> println "App: ${name}"
              | None -> no-op
          | None -> no-op
      | Err _ -> no-op

  | Err e -> println "Parse error: ${Error.message e}"

main

XPath Queries

The XML package provides powerful XPath support for querying configuration values:

# Extract text content
XML.xpath-first doc "//host/text()"         # => Result String XMLError
XML.xpath-text doc "//item/text()"          # => Result [String] XMLError

# Count elements
XML.xpath-count doc "//server"              # => Int

# Check existence
XML.xpath-exists? doc "//database"         # => Bool

# Navigate the DOM
XML.root doc                                # => Result XmlNode XMLError
XML.child-elements node                     # => [XmlNode]
XML.find-child node "tag-name"             # => Option XmlNode
XML.attr node "attribute-name"             # => Option String

XML is useful for configuration when integrating with systems that use XML-based configuration (such as Maven pom.xml or .NET app.config), or when you need namespace and schema support.

Choosing a Format

Use TOML when:

  • Writing application configuration files
  • You want a format designed specifically for configuration
  • You need sections/tables for organizing settings
  • No external dependencies are acceptable (stdlib)

Use JSON when:

  • Interoperating with web services or APIs
  • Configuration is generated by or consumed by other tools
  • You need a universally supported format
  • No external dependencies are acceptable (stdlib)

Use YAML when:

  • You need multiline strings and anchors/aliases
  • Your team is already using YAML-based tooling (Kubernetes, CI/CD)

Use EDN when:

  • Interoperating with Clojure systems
  • You need keywords, symbols, or tagged literals

Use XML when:

  • Integrating with XML-based systems (Maven, SOAP, RSS/Atom)
  • You need namespaces or XPath queries

Common Patterns

Config with Defaults

Use the ?? operator to provide default values when a key is missing:

import Auth.File.{file-auth}
import Encoding.TOML
import IO.FileAuth.{read-file}

main = fn(env: Env) =>
  fa = file-auth env.root
  match read-file fa "config.toml"
  | Err _ ->
      println "No config file, using defaults"
  | Ok contents ->
      match TOML.parse contents
      | Err _ ->
          println "Invalid config, using defaults"
      | Ok config ->
          host = TOML.get-string config "host" ?? "localhost"
          port = TOML.get-int config "port" ?? 8080
          debug = TOML.get-bool config "debug" ?? false
          println "Host: ${host}, Port: ${show port}, Debug: ${show debug}"

main

Environment-Aware Config

Load different configuration files based on an environment variable:

import Auth.File.{file-auth}
import Encoding.TOML
import Env
import IO.FileAuth.{read-file}

main = fn(env: Env) =>
  fa = file-auth env.root
  kit-env = Env.get "KIT_ENV" ?? "development"
  config-path = "config/${kit-env}.toml"

  match read-file fa config-path
  | Ok contents ->
      match TOML.parse contents
      | Ok _ -> println "Loaded config from ${config-path}"
      | Err e -> println "Invalid config: ${Error.message e}"
  | Err _ -> println "Config file not found: ${config-path}"

main

Validating Configuration

Combine parsing with validation:

import Auth.File.{file-auth}
import Encoding.TOML
import IO.FileAuth.{read-file}

validate-config = fn(config) =>
  match TOML.get-int config "port"
  | None -> Err "Missing required field: port"
  | Some port ->
      if port < 1 then
        Err "Port must be positive, got: ${show port}"
      else if port > 65535 then
        Err "Port must be <= 65535, got: ${show port}"
      else
        Ok config

main = fn(env: Env) =>
  fa = file-auth env.root
  match read-file fa "config.toml"
  | Err _ -> println "Cannot read config.toml"
  | Ok contents ->
      match TOML.parse contents
      | Err e -> println "Parse error: ${Error.message e}"
      | Ok config ->
          match validate-config config
          | Ok valid-config ->
              name = TOML.get-string valid-config "name" ?? "unknown"
              println "Valid config: ${name}"
          | Err e -> println "Validation error: ${e}"

main

Reading Config from a File or Inline

Kit's heredoc syntax makes it easy to embed configuration directly in source for testing or defaults:

import Auth.File.{file-auth}
import Encoding.TOML
import IO.FileAuth.{read-file}

default-config = <<~TOML
  host = "localhost"
  port = 8080
  debug = false

  [logging]
  level = "info"
  format = "json"
TOML

main = fn(env: Env) =>
  fa = file-auth env.root
  contents = read-file fa "config.toml" ?? default-config
  match TOML.parse contents
  | Ok config ->
      host = TOML.get-string config "host" ?? "localhost"
      port = TOML.get-int config "port" ?? 8080
      println "Running on ${host}:${show port}"
  | Err e -> println "Config error: ${Error.message e}"

main