The CFG API for Dart¶
The CFG reference implementation for the Dart language assumes a Dart SDK version of 2.13.1 or later.
Installation¶
You can install the library for use by adding cfg_lib to your list of dependencies in your pubspec.yaml file:
dependencies:
cfg_lib: ^0.1.1
There’s a minimal example of a program that uses CFG here.
Exploration¶
As Dart doesn’t currently have a Read-Eval-Print-Loop (REPL) function available, you can follow along with an online setup here:
Getting Started with CFG in Dart¶
A configuration is accessed through the Config
class 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:
var cfg = Config.fromFile('test0.cfg');
Access elements with keys¶
Accessing elements of the configuration with a simple key is just like using a
Table
:
print('a is "${cfg['a']}"');
print('b is "${cfg['b']}"');
which prints:
a is "Hello, "
b is "world!"
Access elements with paths¶
As well as simple keys, elements can also be accessed using path strings:
print('c.d is "${cfg['c.d']}"');
which prints:
c.d is "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:
print('f.g is "${cfg['f.g']}"');
which prints:
f.g is "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 Dart date/time objects from a configuration, by using an ISO date/time pattern in a backtick-string:
print('Christmas morning is ${cfg['christmas_morning']} (${cfg['christmas_morning'].runtimeType})');
which prints:
Christmas morning is 2019-12-25 08:39:49.000Z (DateTime)
As Dart doesn’t currently support timezone-aware date/times out of the box, currently the approach used is to compute the offset and add to the UTC time to yield the result. Although there are some third-party timezone-aware libraries around, they don’t allow computing an offset and setting it on the date/time - they work from timezone names.
Access to environment variables¶
To access an environment variable, use a backtick-string of the form
`$VARNAME`
:
print(cfg['home'] == Platform.environment['HOME']);
which prints:
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.
print('foo is "${cfg['foo']}"');
which prints:
foo is "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. Follow along with this online setup:
https://replit.com/join/hewtgzpxgn-vsajip
var cfg = Config.fromFile('test0a.cfg');
for (var key in ['header_time', 'steady_time', 'trailer_time', 'log_file']) {
print('$key is ${cfg[key]} (${cfg[key].runtimeType})');
}
which prints:
header_time is 30.0 (double)
steady_time is 50.0 (double)
trailer_time is 20.0 (double)
log_file is /my/app/test.log (String)
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:
Follow along with this online setup:
https://replit.com/join/dprcppmdox-vsajip
var cfg = Config.fromFile('main.cfg');
for (var key in ['logging.layouts.brief.pattern', 'redirects.freeotp.url', 'redirects.freeotp.permanent']) {
print('$key is ${cfg[key]} (${cfg[key].runtimeType})');
}
which prints:
logging.layouts.brief.pattern is %d [%t] %p %c - %m%n (String)
redirects.freeotp.url is https://freeotp.github.io/ (String)
redirects.freeotp.permanent is false (bool)
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:
Follow along with this online setup:
https://replit.com/join/qsordhjspi-vsajip
var cfg = Config.fromFile('main.cfg');
for (var key in ['logging.appenders.file.append', 'logging.appenders.file.filename', 'logging.appenders.file.level',
'logging.appenders.debug.level', 'logging.appenders.debug.filename',
'logging.appenders.debug.append']) {
print('$key is ${cfg[key]} (${cfg[key].runtimeType})');
}
which prints:
logging.appenders.file.append is false (bool)
logging.appenders.file.filename is run/server.log (String)
logging.appenders.file.level is INFO (String)
logging.appenders.debug.level is DEBUG (String)
logging.appenders.debug.filename is run/server-debug.log (String)
logging.appenders.debug.append is false (bool)
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.
Classes¶
The Location
class¶
This represents a source location and has two integer attributes, line
and
column
. The line must be positive and the column non-negative (newlines have an
ending column of zero, as column 1 would have the first character of the next line).
The ConfigException
class¶
This is an Exception
subclass which has an error message in the message
attribute and a Location
instance in the location
attribute indicating the
position in the source of the error, where appropriate. (Some errors don’t have a
location, and in such cases the location
attribute of the instance will be set to
null
.)
The Config
class¶
This class implements access to a CFG configuration. You’ll generally interface to CFG files using this class. When you pass in a string or file path to the constructor the CFG source in the string or file is parsed and converted into an internal form which can then be accessed in a manner analogous to a Python dictionary.
-
class
Config
¶ Attributes
-
path
¶ The path to the file from which the configuration has been read. You won’t usually need to set this, unless you want to load a configuration from text that references included files. In that case, set the path to a value whose parent directory contains the included files, or specify relevant directories using
includePath
. When a configuration is loaded from a file, this attribute is automatically set.
-
includePath
¶ A list of directories which is searched for included sub-configurations if the parent directory of
path
(or the current directory, if path isn’t set) doesn’t contain them.
-
context
¶ A mapping of strings to objects which is used whenever an identifier is encountered when evaluating an expression in a configuration file.
-
noDuplicates = true
Whether keys are allowed to be duplicated in mappings. Usually, a
ConfigException
is raised if a duplicate key is seen. Iffalse
, then if a duplicate key is seen, its value silently replaces the value associated with the earlier appearance of the same key.
-
strictConversions = true
If
true
, aConfigException
is thrown if a string can’t be converted. It’s intended to help catch typos in backtick-strings.
Methods
-
Config
() Returns a new instance.
-
factory Config.fromSource(String source)
Returns a new instance initialized with a configuration provided in source.
-
factory Config.fromFile(String path)
Returns a new instance initialized with a configuration provided in the file specified by path.
-
void loadFile(String path)
Overwrite any configuration with a configuration provided in the file specified by path.
-
HashMap<String, dynamic> asDict()
Return’s this instance’s data as a HashMap<String, dynamic>.
-
dynamic get(String key, {dynamic dv})
Retrieve a value from the instance whose key or path is represented by key. If the key or path is absent from the configuration and dv is provided, it is returned. Otherwise, a
ConfigException
is thrown.
-
dynamic operator [](String key)
Retrieve a value from the instance whose key or path is represented by key. If the key or path is absent from the configuration, a
ConfigException
is thrown.
-