GenConfig
=========
Aims to provide less-painful C/C++ app configuration support.

Motivation
----------
Created because all of the config systems I have used (or created) in the past had too many pain points, like:
- Requiring things to be defined in multiple places
- Not using standard, human-friendly configuration formats
- Lacking validation of basic types and structure
- Not supporting hierarchical data structures, arrays, strings very well
- Requiring tedious structure maintenance
- Requiring accessor calls to get at simple data fields
- Not supporting configuration deltas
This system attempts to alleviate those pain points and make configuration suck less:
- Automatically generates structure definition code from an input specification, which is easy to write
- Loads application configuration from [TOML](https://toml.io/en/) format, which is standardized and human-friendly
- Validates user configuration matches expected structure and fields are of expected type
- Support hierarchical structures of basic types: `bool`, `int`, `float`, `enum`, strings (UTF-8), and arrays
- Minimizes development effort: just define the setting once in a spec file and the rest is handled
- Usable in C/C++, without having to do any lookups or extra validation: just read from a `struct`
- Supports saving configuration deltas
How it Works
------------
- You define the options you want for your app in a spec file
- You run `gen_config.py` to generate a header file with `struct config` definition
- User runs your app, providing their settings in a `.toml` file matching your specification
- Your code calls a function to load, parse, and store the config file to the `config` structure
- Your code accesses the config by just reading from and writing to the `config` struct
- Your code changes `config` structure in response to some user action
- Your code calls a function to save the config delta to the user's `.toml` config file
- You retain a bit more hair that you might have ripped out using/creating another configuration system
- Your app makes lots of money and you send some my way because you support open-source development. Thanks, that's nice of you.
Requirements
------------
- [toml++](https://marzer.github.io/tomlplusplus/), which is used to parse config file
- Compiler that supports C++17
Example
-------
This is the specification file that defines your config options:
```yaml
company:
name: string
headquarters:
state: string
city: string
products:
type: array
items:
name:
type: string
default: New Product
price: float
inventory: int
international_shipping:
type: bool
default: true
suppliers:
type: array
items: string
category:
type: enum
values: ['fruit', 'vegetable', 'beverage', 'explosive']
```
After running `gen_config.py`, this C code is generated automatically.
```C
enum CONFIG_COMPANY_PRODUCTS_CATEGORY {
CONFIG_COMPANY_PRODUCTS_CATEGORY_FRUIT,
CONFIG_COMPANY_PRODUCTS_CATEGORY_VEGETABLE,
CONFIG_COMPANY_PRODUCTS_CATEGORY_BEVERAGE,
CONFIG_COMPANY_PRODUCTS_CATEGORY_EXPLOSIVE,
CONFIG_COMPANY_PRODUCTS_CATEGORY__COUNT
};
struct config {
struct company {
const char *name;
struct headquarters {
const char *state;
const char *city;
} headquarters;
struct products {
const char *name;
float price;
int inventory;
bool international_shipping;
const char **suppliers;
unsigned int suppliers_count;
enum CONFIG_COMPANY_PRODUCTS_CATEGORY category;
} *products;
unsigned int products_count;
} company;
};
```
Additionally, a corresponding `CNode` tree is created, that's used to support everything:
```cpp
CNode config_tree =
ctab("config", {
ctab("company", {
cstring(
offsetof(struct config, company.name),
"name", ""),
ctab("headquarters", {
cstring(
offsetof(struct config, company.headquarters.state),
"state", ""),
cstring(
offsetof(struct config, company.headquarters.city),
"city", "")
}),
carray(
offsetof(struct config, company.products),
offsetof(struct config, company.products_count),
sizeof(struct config::company::products),
"products",
ctab("", {
cstring(
offsetof(struct config::company::products, name),
"name", "New Product"),
cnumber(
offsetof(struct config::company::products, price),
"price", 0.0),
cinteger(
offsetof(struct config::company::products, inventory),
"inventory", 0),
cbool(
offsetof(struct config::company::products, international_shipping),
"international_shipping", true),
carray(
offsetof(struct config::company::products, suppliers),
offsetof(struct config::company::products, suppliers_count),
sizeof(((struct config::company::products *){0})->suppliers[0]),
"suppliers",
cstring(
0, "", "")
),
cenum(
offsetof(struct config::company::products, category),
"category", {"fruit", "vegetable", "beverage", "explosive"}, "fruit")
})
)
})
});
```
Config file to be loaded at runtime, in [TOML](https://toml.io/en/) format:
```toml
[company]
name = 'Acme Corp'
products = [
{ name = 'Apple', price = 1.2, inventory = 100, suppliers = ['Midwest Orchard', 'Tasty Apples Inc.'], category = 'fruit' },
{ name = 'TNT', price = 50, inventory = 1000, category = 'explosive', international_shipping = false },
]
[company.headquarters]
city = 'Phoenix'
state = 'Arizona'
```
Loading the config basically looks like:
```cpp
#include // Required for basic parsing
#include // Required data structure for config mgmt
#define DEFINE_CONFIG_TREE
#include "config.h"
// Load config from user file
struct config s;
auto toml_table = toml::parse_file("config.toml");
config_tree.update_from_table(toml_table);
config_tree.store_to_struct(&s);
```
Then when you are ready to save the config again:
```cpp
// Update config tree from structure if you have modified it
config_tree.update_from_struct(&s);
// Save config
FILE *f = fopen("config.toml", "wb");
fprintf(f, "%s", config_tree.generate_delta_toml().c_str());
```