DEV Community

Cover image for Building Profig - A Config Framework
Amartya Chowdhury
Amartya Chowdhury

Posted on

Building Profig - A Config Framework

I've worked with config files for a long time. Be it for my own tools/projects or for using it for other purposes, I have needed to manage configs manually for a long time. Until I discovered that there exist literal config managers, and that I had been wasting my efforts for absolutely no reason.

I found that there were a bunch of good options available for config managers in JavaScript (NPM). However, the same is not the case for Rust. Here, there are limited options, popular ones being config, config-manager and figment. And so I set out to work on a new project, and here it is:

What is Profig?

Introducing Profig, a config framework, that removes the Con in Config. This is still in very early development stages, and I've only released just v0.1, but I think it's still usable in small and maybe even medium sized projects.

Features

Profig is currently v0.1, and it has the following features:

  • Macros like #[derive(Profig)] and #[profig(...)] to define config schema structs
  • Enforce only certain file formats to be allowed to load data from, using #[profig(format="json,toml")]
  • Every field of the struct will have its own metadata, defined via #[profig(min="..", max="..", default="..", regex=r"..", doc="..")]
  • Data can be loaded from any file using AppConfig::load("filename.ext") (assuming AppConfig is the name of the struct defined), and it returns an object of type AppConfig
  • It validates every field data according to the metadata (min, max, regex etc.) and fills a field with the default value if applicable
  • Sample config files can be generated via AppConfig::sample_config("sample.yaml") using the data available in the metadata of the fields (defined using #[profig(...)])
  • Documentation can be generated automatically using AppConfig::generate_docs("docs.md") which generates a markdown file using the doc metadata of every field
  • Multiple formats: JSON, TOML, YAML (all as their own features)

Usage

All these features can seem confusing and overwhelming at first, even though they aim to greatly enhance DX. So, let's do a bit of practical.

Let's imagine a situation where we need the user to define a config file which contains some data like host name, port number, user email, and the number of worker threads. The user may have a JSON or a YAML file as a config file. Using this data, our Rust code is supposed to authenticate/validate the details and then start a server or do something else, as required. Suppose that we are to build a CLI, and we need the details from a config file, as mentioned. Let's see how simple it is to achieve with Profig.

Before anything else, let us first add the crate to our project. Since we need JSON and YAML configs, we will enable those features while installing.

cargo add profig --features "json,yaml"
Enter fullscreen mode Exit fullscreen mode

First, we define a schema for the config file. To define the schema, we use the attributes provided by Profig as follows:

#[derive(Profig)]
#[profig(format="json, yaml")]
struct HostConfig {
  #[profig(min="4", max="10", doc="Number of worker threads")]
  threads: u32,

  #[profig(default="localhost", doc="Name of the host")]
  hostname: Option<String>,

  #[profig(min="1", max="65536", doc="Port number to listen to")]
  port: u32,

  #[profig(regex=r"^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$", doc="Email address of the user")]
  email: String
}
Enter fullscreen mode Exit fullscreen mode

This is all we need to do in order to define the schema/format of the config files. We specified formats as json and yaml as those are the only ones allowed. Now to read data from a file, say config.yaml, we can do so as follows:

fn main() -> Result<(), Box<dyn std::error::Error>> {
  let confData = HostConfig::load("config.yaml")?;
  // All data can now be accessed as confData.email, confData.host etc
  // because this returns an object of type HostConfig.
  // You can use this data for any further logic implementation
}
Enter fullscreen mode Exit fullscreen mode

And voila! You have successfully loaded and used data from config files!

Sample Config

You can also generate a sample config file in a single line, using the following function:

HostConfig::sample_config("sample.json")?;
Enter fullscreen mode Exit fullscreen mode

This will create the following type of sample file (JSON here):

{
  "threads": 4,
  "hostname": "localhost",
  "port": 1,
  "email": "REQUIRED; must match ^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$"
}
Enter fullscreen mode Exit fullscreen mode

Documentation Generation

That's not all. You can also generate a documentation for your defined config structure in a single line! Just call the following method:

HostConfig::generate_doc("docs.md")?;
Enter fullscreen mode Exit fullscreen mode

And it will generate you a markdown file filled with data like field name, field data type, and a field help/doc line if provided (via the doc="..." value in the field-level metadata).

🛠 Tech Dive

For the bit more nerdy humans like myself, let's talk a bit about the tech. This project uses Rust (no s**t Sherlock), along with:

  • Serde, serde_json for storing and validating the data
  • serde_json, serde_yaml and toml crates to load data from different config file types
  • Procedural macros to make the #[derive(Profig)] and #[profig(...)] attributes to make DX smoother
  • Feature flags for every file format to avoid unnecessarily bloating the crate

This is an architecture/design of the entire working or flow:

profig architecture

Final Notes

All in all, Profig is a config framework that I plan to keep building and improving as time sees. It is in very early stages right now, being just v0.1, so there will be quite some changes before even Profig v1 is launched.

In the meanwhile, I would love any and all feedbacks and advice.

Please do try it for yourself if possible:
🦀 Crates.io
💻 GitHub

For more updates, follow me on:
LINKEDIN
TWITTER

Top comments (0)