Tiri Config API

The Config API provides encoding and decoding functionality for moving data between Config text and Tiri tables.

Config text is a simple, human-readable data format built from named groups of key-value pairs. It is the same format used by the Config class, so this library is a convenient way to produce and consume that data directly in script without instantiating an object.

The API can be loaded with the line:

import 'config'

Data Model

Config data is represented as a table of group tables. Each top-level key names a group, and each group is itself a table of string-valued keys:

data = {
   Display = { Width = '1280', Height = '720' },
   Audio   = { Volume = '80' }
}

This corresponds to the following Config text:

[Audio]
Volume = 80

[Display]
Height = 720
Width = 1280

A few properties of the format are worth noting:

  • Decoded values are always strings. Numeric- and boolean-looking values such as 8 or Yes are returned verbatim as '8' and 'Yes'; no type conversion is performed.
  • Comments (lines beginning with #), source ordering, and incidental formatting are not preserved across a decode.
  • Group and key names are case-sensitive. [Display] and [display] are distinct groups.

config.decode()

result = config.decode(String)

Decodes Config text into a table of groups and key-value tables.

data = config.decode('[Display]\nWidth = 1280\n')
-- Result: { Display = { Width = '1280' } }

The parser is deliberately lenient and mirrors the behaviour of the Config class:

  • Both LF (\n) and CRLF (\r\n) line endings are accepted.
  • Leading whitespace on a line is ignored.
  • Blank lines and comment lines (those whose first non-space character is #) are skipped.
  • Key-value lines are split on the first =. Surrounding whitespace is trimmed from the key, and leading whitespace is trimmed from the value. Trailing whitespace in a value is preserved.
  • Lines that appear before the first group, and malformed lines with no =, are ignored.
  • Repeated groups are merged, and when a key appears more than once the final occurrence takes precedence.
  • A malformed group header (for example [Invalid with no closing bracket, or a nested [) closes the current group, so subsequent keys are discarded until the next valid header.
source = [[
# Font definitions
[Clean]
Bold = fonts:fixed/clean.fon
Points = 8
Styles = Bold,Bold Italic,Italic,Regular
]]

data = config.decode(source)
-- data.Clean.Bold   = 'fonts:fixed/clean.fon'
-- data.Clean.Points = '8'                          (still a string)
-- data.Clean.Styles = 'Bold,Bold Italic,Italic,Regular'

config.encode()

text = config.encode(Value)

Encodes a table of groups and key-value tables as deterministic Config text.

text = config.encode({ Display = { Width = 1280 } })
-- Result: '[Display]\nWidth = 1280\n'

The output is canonical and reproducible:

  • Group names and, within each group, key names are sorted alphabetically.
  • Groups are separated by a single blank line. An empty group is emitted as a bare section heading with no keys.
  • string, number, and boolean values are supported. Numbers and booleans are converted to their string representations (3, true).
text = config.encode({
   Zulu  = { Enabled = true, Count = 3 },
   Alpha = { Name = 'First', Empty = '' }
})
-- Result:
-- [Alpha]
-- Empty =
-- Name = First
--
-- [Zulu]
-- Count = 3
-- Enabled = true

An empty table encodes to an empty string, and an empty group encodes to just its heading:

config.encode({ })            -- Result: ''
config.encode({ Empty = { } }) -- Result: '[Empty]\n'

Validation

Unlike decode(), the encoder is strict and raises an exception for any input that cannot be represented as valid, round-trippable Config text:

  • The root value must be a table, and every group value must be a table.
  • Group names must be non-empty strings and cannot contain [, ], or line breaks.
  • Key names must be non-empty strings with no surrounding whitespace, cannot start with # or [, and cannot contain = or line breaks.
  • Values must be strings, numbers, or booleans (nested tables are rejected) and cannot contain line breaks.
config.encode('not a table')                 -- error: root must be a table
config.encode({ Broken = 'value' })          -- error: group must be a table
config.encode({ Group = { ['Bad=Key'] = 'x' } }) -- error: key cannot contain '='
config.encode({ Group = { Key = 'a\nb' } })   -- error: value cannot contain line breaks

Round-Tripping

Because the encoder enforces exactly the constraints the decoder relies on, any table that encode() accepts survives a full encode → decode cycle with its group names, key names, and string values intact:

encoded = config.encode({ ['Mixed Case'] = { ['Key With Spaces'] = 'value' } })
decoded = config.decode(encoded)
-- decoded['Mixed Case']['Key With Spaces'] = 'value'

Note that the reverse is not guaranteed: decode() accepts input the encoder would reject (comments, malformed lines, duplicate keys), so decode → encode normalises the data rather than reproducing the original text byte-for-byte.

Interoperating with the Config Class

The text produced by config.encode() is directly consumable by the Config class via a DATA_TEXT data feed, which is useful when you want to build configuration data in script and then hand it to code that expects a Config object:

encoded = config.encode({ Font = { Name = 'Clean', Points = 8, Scalable = true } })

cfg = obj.new('config')
check(cfg.acDataFeed(nil, DATA_TEXT, encoded, 0))

name = [_*]cfg.mtReadValue('Font', 'Name')      -- 'Clean'
points = [_*]cfg.mtReadValue('Font', 'Points')  -- '8'

Conversely, use config.decode() when you have Config text (for instance loaded from a file) and prefer to work with it as a plain Tiri table rather than through the object API.