.. default-domain:: rust .. _rust-api: .. meta:: :description: CFG configuration format Rust API :keywords: CFG, configuration, Rust The CFG API for Rust -------------------- The CFG reference implementation for Rust assumes you are using a version of Rust which is 1.41 or later. Usage +++++ This library is on `crates.io `_ and can be used by adding ``cfg-lib`` to your dependencies in your project's Cargo.toml: .. code-block:: toml [dependencies] cfg-lib = "0" There's a minimal example of a program that uses CFG `here `__. Exploration +++++++++++ To explore CFG functionality for Rust, we use the `evcxr` Read-Eval-Print-Loop (REPL), which is available from `here `_. Once installed, you can invoke a shell using .. code-block:: shell $ evcxr You'll normally need to do the following at the start of a session: .. code-block:: rust >> :dep cfg-lib >> use cfg_lib::config::*; Getting Started with CFG in Rust ++++++++++++++++++++++++++++++++ A configuration is represented by an instance of the :struct:`Config` struct. A reference to one can be obtained using either the :func:`new` or :func:`from_file` functions. The former creates an instance with no configuration loaded, while the latter initialises using a configuration in a specified file: the text is read in, parsed and converted to an object that you can then query. A simple example: .. include:: go-test0.cfg.txt Loading a configuration ~~~~~~~~~~~~~~~~~~~~~~~ The configuration above can be loaded as shown below. In the REPL shell: .. code-block:: rust >> let cfg = Config::from_file("test0.cfg").unwrap(); The :func:`from_file` call returns a :struct:`Config` instance which can be used to query the configuration. Access elements with keys ~~~~~~~~~~~~~~~~~~~~~~~~~ Accessing elements of the configuration with a simple key is not much harder than using a ``HashMap``: .. code-block:: rust >> cfg.get("a") Ok(Base(String("Hello, "))) >> cfg.get("b") Ok(Base(String("world!"))) The values returned are of type :enum:`Value`. Access elements with paths ~~~~~~~~~~~~~~~~~~~~~~~~~~ As well as simple keys, elements can also be accessed using :term:`path` strings: .. code-block:: rust >> cfg.get("c.d") Ok(Base(String("e"))) .. include:: api-snip-02.inc .. code-block:: rust >> cfg.get("f.g") Ok(Base(String("h"))) .. include:: api-snip-03.inc Access to date/time objects ~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can also get native Rust date/time objects from a configuration, by using an ISO date/time pattern in a :term:`backtick-string`: .. code-block:: rust >> cfg.get("christmas_morning") Ok(Base(DateTime(2019-12-25T08:39:49+00:00))) You get either ``NaiveDate`` objects, if you specify the date part only, or else ``DateTime`` objects, if you specify a time component as well. If no offset is specified, it is assumed to be zero. Access to environment variables ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To access an environment variable, use a :term:`backtick-string` of the form ```$VARNAME```: .. code-block:: rust >> cfg.get("home") Ok(Base(String("/home/vinay"))) .. include:: api-snip-07.inc .. code-block:: rust >> cfg.get("foo") Ok(Base(String("bar"))) Access to computed values ~~~~~~~~~~~~~~~~~~~~~~~~~ .. include:: api-snip-04.inc .. code-block:: cfg-body :caption: test0a.cfg 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: .. code-block:: rust >> cfg.get("header_time") Ok(Base(Float(30.0))) >> cfg.get("steady_time") Ok(Base(Float(50.0))) >> cfg.get("trailer_time") Ok(Base(Float(20.0))) >> cfg.get("log_file") Ok(Base(String("/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: .. code-block:: cfg-body :caption: logging.cfg layouts: { brief: { pattern: '%d [%t] %p %c - %m%n' } }, appenders: { file: { layout: 'brief', append: false, charset: 'UTF-8' level: 'INFO', filename: 'run/server.log', append: true, }, error: { layout: 'brief', append: false, charset: 'UTF-8' level: 'ERROR', filename: 'run/server-errors.log', }, debug: { layout: 'brief', append: false, charset: 'UTF-8' level: 'DEBUG', filename: 'run/server-debug.log', } } loggers: { mylib: { level: 'INFO' } 'mylib.detail': { level: 'DEBUG' } }, root: { handlers: ['file', 'error', 'debug'], level: 'WARNING' } .. code-block:: cfg-body :caption: redirects.cfg 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 } .. code-block:: cfg-body :caption: main.cfg 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 }, connection: 'postgres+pool://db_user:db_pwd@localhost:5432/db_name', 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' .. include:: api-snip-05.inc .. code-block:: rust >> cfg.get("logging.appenders.file.filename") Ok(Base(String("run/server.log"))) >> cfg.get("redirects.freeotp.url") Ok(Base(String("https://freeotp.github.io/"))) >> cfg.get("redirects.freeotp.permanent") Ok(Base(Bool(false))) Avoiding unnecessary repetition ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. include:: api-snip-08.inc .. code-block:: cfg-body :caption: logging.cfg (partial) appenders: { file: { layout: 'brief', append: false, charset: 'UTF-8' level: 'INFO', filename: 'run/server.log', append: true, }, error: { layout: 'brief', append: false, charset: 'UTF-8' level: 'ERROR', filename: 'run/server-errors.log', }, debug: { layout: 'brief', append: false, charset: 'UTF-8' level: 'DEBUG', filename: 'run/server-debug.log', } } This portion could be rewritten as: .. code-block:: cfg-body :caption: logging.cfg (partial) 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', } } .. include:: api-snip-06.inc .. code-block:: rust >> cfg.get("logging.appenders.file.level") Ok(Base(String("INFO"))) >> cfg.get("logging.appenders.file.layout") Ok(Base(String("brief"))) >> cfg.get("logging.appenders.file.append") Ok(Base(Bool(true))) >> cfg.get("logging.appenders.file.filename") Ok(Base(String("run/server.log"))) >> cfg.get("logging.appenders.error.append") Ok(Base(Bool(false))) >> cfg.get("logging.appenders.error.filename") Ok(Base(String("run/server-errors.log"))) .. include:: api-snip-09.inc .. code-block:: cfg-body :caption: logging.cfg (partial) defs: { base_appender: { layout: 'brief', append: false, charset: 'UTF-8' } log_prefix: 'run/', }, layouts: { brief: { pattern: '%d [%t] %p %c - %m%n' } }, 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 +++++ These are the common types you'll need to be aware of when using CFG in Rust: * enum :enum:`ScalarValue` -- this encapsulates the elements described in the "Scalar values" bullet point in the section on :ref:`elements`. * enum :enum:`Value` -- this builds on ``ScalarValue`` and adds the "Mappings" and "Lists" bullet points in the section on :ref:`elements`. * enum :enum:`RecognizerError` -- this encapsulates the different low-level errors which can be returned when parsing CFG text. * enum :enum:`ConfigError` -- this encapsulates high-level errors which can be returned when loading or querying a configuration. * struct :struct:`Location` -- this is the line-and-column location in CFG source which can be returned with errors to help locate the source of problems. * struct :struct:`Config` -- this represents an individual configuration or sub-configuration. Enums +++++ .. enum:: ScalarValue This represents scalar values such as integers, strings and others. .. member:: Null Represents the ``null`` value. .. member:: Bool(bool) Represents a Boolean value. .. member:: String(String) Represents a string value. .. member:: Integer(i64) Represents an integer value. .. member:: Float(f64) Represents a floating-point value. .. member:: Complex(Complex64) Represents a complex-number value. .. member:: Date(NaiveDate) Represents a date value. This has no timezone information. .. member:: DateTime(DateTime) Represents a date/time value including timezone information expressed as a duration (offset) from UTC. .. member:: Identifier(String) Represents an identifier. This is treated differently to a string literal, in that it can be used as a variable name to look up in a context mapping provided to the configuration. .. enum:: Value This builds upon the :enum:`ScalarValue` to encompass list, mapping and nested configurations types. .. member:: Base(ScalarValue) This represents one of the scalar values described above. .. member:: List(Vec) This represents a list of values. .. member:: Mapping(HashMap) This represents a mapping of strings to values. .. member:: Config(Config) This represents a nested configuration. .. enum:: RecognizerError This represents the various errors which can be returned from the low-level API. .. member:: Io(Location) This represents an I/O error. The location indicates where it happened. .. member:: UnexpectedCharacter(char, Location) An unexpected character was seen in the input, at the start of a lexical token. The character and its location are returned. .. member:: InvalidCharacter(char, Location) An invalid character was seen in the input, while parsing a lexical token. The character and its location are returned. .. member:: InvalidNumber(Location) An error was encountered when parsing a number. The location of the error is returned. .. member:: UnterminatedString(Location) An unterminated string was detected. The location of the string is returned. .. member:: InvalidString(Location) An invalid string was detected. The location of the string is returned. .. member:: UnexpectedToken(String, String, Location) An unexpected token was encountered when parsing the input. The kind of token expected, the kind seen and the location are returned. .. member:: ValueExpected(String, Location) A value was expected, but instead some other kind of token was seen. The unexpected token and its location are returned. .. member:: AtomExpected(String, Location) An "atom" (such as an identifier or number) was expected, but not seen. The unexpected token that was seen and its location are returned. .. member:: KeyExpected(String, Location) A mapping key (either an identifier or a string literal) was expected, but something else was seen. The unexpected token and its location are returned. .. member:: KeyValueSeparatorExpected(String, Location) A mapping key-value separator (either a colon or an equals sign) was expected, but something else was seen. The unexpected token and its location are returned. .. member:: ContainerExpected(String, Location) A container (list or mapping) was expected, but something else was seen. The unexpected token and its location are returned. .. member:: InvalidEscapeSequence(usize, Location) An invalid escape sequence was detected when parsing a string. The position of the error and the location of the string are returned. .. member:: UnexpectedListSize(Location) A list element with an unexpected size was found. This can happen if a list with more than one element is seen when parsing an array index -- multidimensional arrays are allowed by the syntax but not currently supported. .. member:: TrailingText(Location) This error is returned if trailing text is found when parsing a path. The location of the trailing text is returned. .. enum:: ConfigError This represents the various errors which can be returned from the high-level API. .. member:: SyntaxError(RecognizerError) This error will be returned if there is a syntax error detected while parsing the source text of a configuration. The underlying parsing error is returned. .. member:: NotLoaded This error is returned if a configuration is queried without anything having been loaded into it. .. member:: MappingExpected Although a configuration file can contain a list or a mapping, a top- level configuration must be a mapping. If it isn't, this error is returned. .. member:: StringExpected(Location) This error is returned if a string value is expected, but not found. For example, the inclusion operator ``@`` expects a string operand, which is interpreted as the path name of a file to be included as a sub-configuration. .. member:: DuplicateKey(String, Location, Location) This error is returned if a duplicate key is seen when processing a mapping, unless the ``no_duplicates`` setting has been set to ``false``. .. member:: BadFilePath(String) This error is returned if a string (e.g. representing a configuration to be included) is not a valid file path. .. member:: FileNotFound(String) This error is returned if a string (e.g. representing a configuration to be included) does not correspond to an existing file, even if valid as a file path. .. member:: FileReadFailed(String) This error is returned if a file could not be opened for reading (e.g. because of insufficient privileges). .. member:: NoContext This error is returned if an identifier is encountered when processing a configuration, but no context has been provided where a corresponding value can be looked up. .. member:: UnknownVariable(String, Location) This error is returned if an identifier is encountered when processing a configuration, but is not present as a key in the provided context. .. member:: NotPresent(String, Option) This error is returned if a key is not present in the configuration. The key or path element is returned, along with an error location if the error occurred while traversing a path. .. member:: InvalidPath(RecognizerError) This error is returned if a string does not represent a valid path when it should do so. The underlying parsing error is returned. .. member:: InvalidPathOperand(Location) This error is returned when a path operand is incompatible with the current path contents (e.g. a string operand for a list, or an integer operand for a mapping). The location of the offending path element is returned. .. member:: IndexOutOfRange(i64, Location) This error is returned when an integer operand for a list is not within the permitted range. The offending index and the location where it occurs in the path are returned. .. member:: EvaluationFailed(Location) This is returned if an evaluation failed, e.g. because of the incompatible types of operators and their operands. .. member:: ConversionError(String) This is returned if a special value string can't be converted to a value. .. member:: CircularReferenceError(Vec<(String, Location)>) This is returned if a reference leads to an infinite recursion, whether directly or indirectly. A list of the references which lead to the recursion (along with their locations) is returned. Structs +++++++ .. struct:: Location This represents a location in the source text for a configuration. .. member:: line: i32 This represents a line number in the source (the first line is 1). .. member:: column: i32 This represents a column number in the source (the first column is 1). .. struct:: Config This represents a single configuration. .. member:: no_duplicates: bool Whether keys are allowed to be duplicated in mappings. This defaults to ``true`` if not provided -- a :enum:`ConfigError` is raised if a duplicate key is seen. If ``false``, then if a duplicate key is seen, its value silently replaces the value associated with the earlier appearance of the same key. .. cssclass:: class-members-heading Constructors .. function:: new() -> Self Constructs an instance with no actual configuration loaded. A call to :func:`load` or :func:`load_from_file` would be needed to actually make a usable instance. .. function:: from_file(file_path: &str) -> Result Construct this instance with the configuration read from a file named by the provided ``file_path``. .. cssclass:: class-members-heading Methods .. function:: add_include(&mut self, dir: &str) Add a directory ``dir`` to the search path used for included files. .. function:: load_from_file(&mut self, file_path: &str) -> Result<(), ConfigError> Load this instance with the configuration read from the file at the provided ``file_path``. .. function:: load(&mut self, r: Box) -> Result<(), ConfigError> Load this instance with the configuration read from the provided reader ``r``. .. function:: set_context(&mut self, context: HashMap) Set the given hashmap ``context`` as the place to look up identifiers encountered in the configuration. .. function:: contains_key(&self, key: &str) -> bool Return ``true`` if ``key`` is in the configuration, else ``false``. .. function:: get(&self, key: &str) -> Result Get a value by key or path from the configuration. The value of ``key`` can be an identifier or a path. .. function:: as_mapping(&self, unwrap_configs: bool) -> Result, ConfigError> Return the configuration as a mapping, evaluating every element of the configuration. If ``unwrap_configs`` is ``true``, nested configurations are also converted to hashmaps, otherwise they are returned as is.