The CFG API for Nim¶
The CFG reference implementation for the Nim language assumes an Nim version of 1.6.0 or later.
Installation¶
You can install the library for use by adding config to your list of dependencies in your project.nimble file:
requires "config >= 0.1.1"
There’s a minimal example of a program that uses CFG here.
Exploration¶
To explore CFG functionality for Nim, we use the inim
program (from here), which is a Read-Eval-Print-Loop (REPL). You can
install it using
$ nimble install inim
and then just run inim in the shell to start the REPL.
Getting Started with CFG in Nim¶
A configuration is accessed through the Config
type. This has functions
which can be passed a filename or a string which contains the text for the
configuration. The text is read in, parsed and converted to an object that you can
then query. A simple example:
a: 'Hello, '
b: 'world!'
c: {
d: 'e'
}
'f.g': 'h'
christmas_morning: `2019-12-25 08:39:49`
home: `$HOME`
foo: `$FOO|bar`
Loading a configuration¶
The configuration above can be loaded as shown below. In the REPL shell:
nim> import config
var cfg = fromFile("test0.cfg")
Access elements with keys¶
Accessing elements of the configuration with a simple key is just like using a
Table
:
nim> echo cfg["a"]
(kind: StringValue, stringValue: "Hello, ")
nim> echo cfg["b"]
(kind: StringValue, stringValue: "world!")
As Nim is strongly and statically typed, configuration values are returned in a tagged
variant of type ConfigValue
, with the kind field as the discriminant.
Access elements with paths¶
As well as simple keys, elements can also be accessed using path strings:
nim> echo cfg["c.d"]
(kind: StringValue, stringValue: "e")
Here, the desired value is obtained in a single step, by (under the hood)
walking the path c.d
– first getting the mapping at key c
, and then
the value at d
in the resulting mapping.
Note that you can have simple keys which look like paths:
nim> echo cfg["f.g"]
(kind: StringValue, stringValue: "h")
If a key is given that exists in the configuration, it is used as such, and if
it is not present in the configuration, an attempt is made to interpret it as
a path. Thus, f.g
is present and accessed via key, whereas c.d
is not
an existing key, so is interpreted as a path.
Access to date/time objects¶
You can also get native Nim date/time objects from a configuration, by using an ISO date/time pattern in a backtick-string:
nim> echo cfg["christmas_morning"]
(kind: DateTimeValue, dateTimeValue: (nanosecond: 0, second: 49, minute: 39, hour: 8, monthdayZero: 25, monthZero: 12, year: 2019, weekday: Wednesday, yearday: 358, isDst: false, timezone: ..., utcOffset: 0))
Access to environment variables¶
To access an environment variable, use a backtick-string of the form
`$VARNAME`
:
nim> import os
nim> echo cfg["home"].stringValue == getEnv("HOME")
true
You can specify a default value to be used if an environment variable isn’t
present using the `$VARNAME|default-value`
form. Whatever string follows
the pipe character (including the empty string) is returned if VARNAME
is
not a variable in the environment.
nim> echo cfg["foo"]
(kind: StringValue, stringValue: "bar")
Access to computed values¶
Sometimes, it’s useful to have values computed declaratively in the configuration, rather than imperatively in the code that processes the configuration. For example, an overall time period may be specified and other configuration values are fractions thereof. It may also be desirable to perform other simple calculations declaratively, e.g. concatenation of numerous file names to a base directory to get a final pathname.
total_period : 100
header_time: 0.3 * ${total_period}
steady_time: 0.5 * ${total_period}
trailer_time: 0.2 * ${total_period}
base_prefix: '/my/app/'
log_file: ${base_prefix} + 'test.log'
When this file is read in, the computed values can be accessed directly:
nim> cfg = fromFile("test0a.cfg")
nim> echo cfg["header_time"]
(kind: FloatValue, floatValue: 30.0)
nim> echo cfg["steady_time"]
(kind: FloatValue, floatValue: 50.0)
nim> echo cfg["trailer_time"]
(kind: FloatValue, floatValue: 20.0)
nim> echo cfg["log_file"]
(kind: StringValue, stringValue: "/my/app/test.log")
Including one configuration inside another¶
There are times when it’s useful to include one configuration inside another. For example, consider the following configuration files:
layouts: {
brief: {
pattern: '%d [%t] %p %c - %m%n'
}
},
appenders: {
file: {
level: 'INFO',
layout: 'brief',
filename: 'run/server.log',
append: false,
charset: 'UTF-8'
},
error: {
level: 'ERROR',
layout: 'brief',
filename: 'run/server-errors.log',
append: false,
charset: 'UTF-8'
},
debug: {
level: 'DEBUG',
layout: 'brief',
filename: 'run/server-debug.log',
append: false,
charset: 'UTF-8'
}
},
loggers: {
mylib: {
level: 'INFO'
}
'mylib.detail': {
level: 'DEBUG'
}
},
root: {
handlers: ['file', 'error', 'debug'],
level: 'WARNING'
}
cookies: {
url: 'http://www.allaboutcookies.org/',
permanent: false
},
freeotp: {
url: 'https://freeotp.github.io/',
permanent: false
},
'google-auth': {
url: 'https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2',
permanent: false
}
secret: 'some random application secret',
port: 8000,
sitename: 'My Test Site',
default_access: 'public',
ignore_trailing_slashes: true,
site_options: {
want_ipinfo: false,
show_form: true,
cookie_bar: true
},
debug: true,
captcha_length: 4,
captcha_timeout: 5,
session_timeout: 7 * 24 * 60 * 60, # 7 days in seconds
redirects: @'redirects.cfg',
email: {
sender: 'no-reply@my-domain.com',
host: 'smtp.my-domain.com:587',
user: 'smtp-user',
password: 'smtp-pwd'
}
logging: @'logging.cfg'
The main.cfg
contents have been kept to the highest-level values,
within logging and redirection configuration relegated to other files
logging.cfg
and redirects.cfg
which are then included in
main.cfg
. This allows the high-level configuration to be more readable
at a glance, and even allows the separate configuration files to be e.g.
maintained by different people.
The contents of the sub-configurations are easily accessible from the main configuration just as if they had been defined in the same file:
nim> cfg = fromFile("main.cfg")
nim> echo cfg["logging.layouts.brief.pattern"]
(kind: StringValue, stringValue: "%d [%t] %p %c - %m%n")
nim> echo cfg["redirects.freeotp.url"]
(kind: StringValue, stringValue: "https://freeotp.github.io/")
nim> echo cfg["redirects.freeotp.permanent"]
(kind: BoolValue, boolValue: false)
Avoiding unnecessary repetition¶
Don’t Repeat Yourself (DRY) is a useful principle to follow. CFG can help
with this. You may have noticed that the logging.cfg
file above has
some repetitive elements:
appenders: {
file: {
level: 'INFO',
layout: 'brief',
filename: 'run/server.log',
append: true,
charset: 'UTF-8'
},
error: {
level: 'ERROR',
layout: 'brief',
filename: 'run/server-errors.log',
append: false,
charset: 'UTF-8'
},
debug: {
level: 'DEBUG',
layout: 'brief',
filename: 'run/server-debug.log',
append: false,
charset: 'UTF-8'
}
},
This portion could be rewritten as:
defs: {
base_appender: {
layout: 'brief',
append: false,
charset: 'UTF-8'
}
},
appenders: {
file: ${defs.base_appender} + {
level: 'INFO',
filename: 'run/server.log',
append: true,
},
error: ${defs.base_appender} + {
level: 'ERROR',
filename: 'run/server-errors.log',
},
debug: ${defs.base_appender} + {
level: 'DEBUG',
filename: 'run/server-debug.log',
}
},
where the common elements are separated out and just referenced where they are
needed. We find it useful to put all things which will be reused like this in
one place in the condiguration, so we always know where to go to make changes.
The key used is conventionally defs
or base
, though it can be anything
you like.
Access is just as before, and provides the same results:
nim> echo cfg["logging.appenders.file.append"]
(kind: BoolValue, boolValue: false)
nim> echo cfg["logging.appenders.file.filename"]
(kind: StringValue, stringValue: "run/server.log")
nim> echo cfg["logging.appenders.file.level"]
(kind: StringValue, stringValue: "INFO")
nim> echo cfg["logging.appenders.debug.level"]
(kind: StringValue, stringValue: "DEBUG")
nim> echo cfg["logging.appenders.debug.filename"]
(kind: StringValue, stringValue: "run/server-debug.log")
nim> echo cfg["logging.appenders.debug.append"]
(kind: BoolValue, boolValue: false)
The definition of logging.appenders.file
as ${defs.base_appender} + {
level: 'INFO', filename: 'run/server.log', append: true }
has resulted in an
evaluation which first fetches the defs.base_appender
value, which is a
mapping, and “adds” to that the literal mapping which defines the level
,
filename
and append
keys. The +
operation for mappings is
implemented as a copy of the left-hand side merged with the right-hand side.
Note that the append
value for logging.appenders.file
is overridden by
the right-hand side to true
, whereas that for e.g.
logging.appenders.error
is unchanged as false
.
We could do some further refinement by factoring out the common location for the log files:
defs: {
base_appender: {
layout: 'brief',
append: false,
charset: 'UTF-8'
}
log_prefix: 'run/',
},
appenders: {
file: ${defs.base_appender} + {
level: 'INFO',
filename: ${defs.log_prefix} + 'server.log',
append: true,
},
error: ${defs.base_appender} + {
level: 'ERROR',
filename: ${defs.log_prefix} + 'server-errors.log',
},
debug: ${defs.base_appender} + {
level: 'DEBUG',
filename: ${defs.log_prefix} + 'server-debug.log',
}
}
with the same result as before. It is slightly more verbose than before, but the location of all files can be changed in just one place now, as opposed to three, as it was before.
Types¶
The main objects used to interface with CFG configurations are Config
and
ConfigValue
. The former represents an entire CFG configuration; the latter
represents a value read from it. The ConfigValue is a tagged variant structure using
field kind, of type ValueKind
, as the discriminant. You may also have to deal
with ConfigError
, an exception which can be raised due to errors in
configurations, and Location
, which relates to a location in CFG source.
ValueKind (enum)¶
This represents the kinds of values that can be returned from a configuration. It has the following members:
Name | Significance | Corresponding field in ConfigValue |
---|---|---|
IntegerValue | A 64-bit signed integer value | intValue |
FloatValue | A 64-bit floating-point value | floatValue |
StringValue | A string value | stringValue |
BoolValue | A Boolean value | boolValue |
ComplexValue | A 64-bit complex number | complexValue |
NoneValue | A null value |
|
DateTimeValue | A date/time value with timezone offset | dateTimeValue |
ListValue | A list of values | listValue |
MappingValue | A mapping of strings to values | mappingValue |
NestedConfigValue | A nested configuration (sub-configuration) | configValue |
ConfigValue (object)¶
This represents values that can be returned from a configuration. It has the following properties (note that kind is always available, and then just one of the others depending on the value of kind):
Name | Type | Access |
---|---|---|
kind | ValueKind | RW |
intValue | int64 | RW |
floatValue | float64 | RW |
stringValue | string | RW |
boolValue | bool | RW |
complexValue | Complex64 | RW |
dateTimeValue | DateTime | RW |
listValue | seq[ConfigValue] | RW |
mappingValue | Table[string, ConfigValue] | RW |
configValue | Config | RW |
Location (object)¶
This represents a location in CFG source. It has the following properties:
Name | Type | Access |
---|---|---|
line | int | RW |
column | int | RW |
ConfigError (object)¶
This represents an error encountered when working with CFG. It has the following properties:
Name | Type | Access |
---|---|---|
msg | string | RW |
location | Location | RW |
Config (ref object)¶
This represents a CFG configuration. It has the following properties:
Name | Type | Default value | Access | Purpose |
---|---|---|---|---|
noDuplicates | bool | true | RW | Whether duplicate keys are allowed. |
strictConversions | bool | true | RW | Whether exceptions are raised if a conversion fails. |
path | string | RW | Path from which a configuration was loaded. This is set automatically when a configuration is read from a file, and only needs to be called if initializing the configuration from source and you need to specify a location for included configurations which are in files. | |
includePath | seq[string] | @[] | RW | Directories to search for included configurations (searched after the parent directory of path, or the current directory if that isn’t set. |
context | Table[string, ConfigValue] | (empty) | RW | A lookup table for any variables to be used. |
cached | bool | false | RW | If true, values fetched using get() or []() are cached. |
Procedures
-
newConfig*(): Config
Creates a new configuration object, without any actual configuration.
-
fromSource*(source: string): Config
Creates a new configuration object, with the configuration specified in source.
-
fromFile*(path: string): Config
Creates a new configuration object, with the configuration read from the file specified in path.
-
loadFile*(cfg: Config, string: path)
Load or reload the configuration cfg from the specified path.
-
load*(cfg: Config, stream: Stream)
Load or reload the configuration cfg from the specified stream.
-
get*(cfg: Config, key: string, default: ConfigValue): ConfigValue
Get a value from the configuration cfg using key, and returning default if not present in the configuration. The key can be a path as well as a simple key.
-
`[]`*(cfg: Config, key: string): ConfigValue
Get a value from the configuration cfg using the indexing idiom, with key as the index. The key can be a path as well as a simple key.
-
asDict*(cfg: Config): Table[string, ConfigValue]
Get the configuration cfg as a Table. This will recurse into included configurations.
-
getSubConfig*(cfg: Config, key: string): Config
Get an included configuration (sub-configuration) from cfg via key. This can be a path as well as a simple key.