Rand Stats

Configuration

zef:FCO

TITLE

Configuration Module Documentation

SUBTITLE

Effortlessly manage configurations in Raku applications

This documentation provides a comprehensive guide on using the Configuration module for Raku. The module is designed to simplify the management of configuration data in Raku applications, making it easier to define, use, and update configurations as needed.

Build Status SparrowCI

Note: The module is in the early stages of development.

SYNOPSIS

This documentation covers the following aspects of the Configuration module:

CONFIGURATION DEFINITION

To define your application's configuration structure, create a Raku class that does the Configuration::Node role. This class will specify the fields and default values for your configuration.

Example: Defining a Configuration Class

use v6.d;

class RootConfig {
    has Int $.a;
    has Int $.b = $!a * 2;
    has Int $.c = $!b * 3;
}

use Configuration RootConfig;

WRITING CONFIGURATION FILES

Configuration files are written in Raku, allowing you to leverage Raku's syntax for setting configuration values.

Example: Creating a Configuration File (my-conf.rakuconfig)

use Test1Config;

config {
    .a = 1;
    .c = 42;
}

UTILIZING CONFIGURATIONS IN YOUR PROGRAM

To use the defined configurations in your Raku application, simply use the module where the configuration was defined and call the appropriate functions to access the configuration data.

Example: Accessing Configuration in a Raku Program

use Test1Config;

say await config-run :file<examples/test1.rakuconfig>;

UNDERSTANDING CONFIGURATION BUILDERS

When utilizing the config function to define or modify configurations, it's crucial to understand that the object you interact with is not directly the configuration object itself. Instead, this object is a specialized builder object, designed to accumulate configuration data before creating the final configuration object.

The Role of Configuration Builders

For each configuration class you define, the Configuration module automatically generates a corresponding builder class. This builder class is named by appending the word "Builder" to the name of your configuration class. The primary purpose of the builder is to provide a flexible and error-resistant way to gather configuration data.

Accumulating Configuration Data

The builder class contains read-write (rw) methods corresponding to each attribute defined in your configuration class. These methods are used to set or modify the values of the configuration attributes in a staged manner. By calling these methods, you accumulate the data needed to instantiate the actual configuration object.

Here's how the process works:

Example: Using a Configuration Builder

Consider you have a configuration class named AppConfig. The corresponding builder class would be AppConfigBuilder.

class AppConfig {
    has Str $.api_key;
    has Int $.timeout;
}

# Using the builder to set configuration
config {
    .api_key = 'your_api_key_here';
    .timeout = 300;
}

In this example, .api_key and .timeout are methods of the AppConfigBuilder object provided to the block by the config function. These methods set the values that will be used to instantiate AppConfig with the provided values.

This builder pattern allows for a clear separation between the definition of configuration data and its usage, enabling more complex configurations to be defined in an intuitive and error-resistant manner.

HANDLING MISSING CONFIGURATION FILES WITH DEFAULT VALUES

The Configuration module is designed to ensure maximum uptime and resilience for your application by employing a robust defaulting mechanism. In situations where a configuration file is expected but does not exist, the module gracefully defaults to using predefined values specified within your configuration class. This feature guarantees that your application can start and run even in the absence of an external configuration file.

Default Value Mechanism

Upon initialization, if the Configuration module does not find the specified configuration file, it does not halt the application or throw an error. Instead, it proceeds to instantiate the configuration object using the default values declared in the configuration class. This behavior is critical for maintaining application operability, especially in new deployments or environments where the configuration file may not yet be set up.

Example: Specifying and Utilizing Default Values

class AppConfig {
    has Str $.api_key = 'default_api_key';
    has Int $.timeout = 30;
}

use Configuration AppConfig;

In this example, AppConfig specifies default values for api_key and timeout. If the Configuration module does not find an external configuration file upon application start, it will create an AppConfig object with these default values. Consequently, the application remains functional and uses these defaults as its operational parameters.

Advantages of Using Default Values

This defaulting mechanism underscores the Configuration module's design philosophy of resilience and ease of use, ensuring that applications remain robust and user-friendly across various deployment scenarios.

DYNAMIC CONFIGURATION RELOADING

The Configuration module supports dynamic reloading of configurations, allowing your application to respond to changes in configuration without restarting.

Example: Reloading Configuration on File Change

use Test1Config;

react whenever config-run :file<./my-conf.rakuconfig>, :watch {
    say "Configuration changed: { .raku }";
}

Example: Reloading Configuration on Signal

use Test1Config;

react whenever config-run :file<./my-conf.rakuconfig>, :signal(SIGUSR1) {
    say "Configuration changed: { .raku }";
}

USING DEFAULT CONFIGURATION FILE PATHS

When the file path for a configuration is not explicitly specified, the Configuration module intelligently searches for configuration files in default locations. This feature simplifies the configuration management process by automatically detecting and using configuration files based on standardized naming conventions and common directory locations.

The module follows a hierarchical approach to search for configuration files in the following order:

Example: Implicit Configuration File Usage

use Test1Config;

# No file path is specified; the module automatically searches for `app.rakuconfig` in default locations.
say await config-run;

MONITORING SPECIFIC CONFIGURATION VALUE CHANGES WITH CONFIG-SUPPLY

The config-supply function in the Configuration module is a standout feature for applications needing to monitor and react to changes in specific configuration values in real-time. This approach is invaluable for creating highly responsive and adaptable applications that depend on dynamic configuration data.

By returning a Supply that emits updates whenever the monitored configuration value changes, config-supply facilitates a reactive programming model. This enables developers to specify precisely which configuration values to observe and to define actions that should occur in response to changes in these values.

Focused Monitoring with config-supply

The purpose of config-supply is to offer a targeted and efficient way to watch individual configuration values. This is especially useful in complex applications where certain features or behaviors are controlled by specific configuration settings, and updates to these settings need to be handled promptly.

Example: Reacting to Changes in a Specific Configuration Value

use Test1Config;

# Reacting to changes in the `.a` configuration value
config-supply(*.a).tap: {
    say ".a has changed: ", $_
};

This example highlights the use of config-supply to monitor changes to the .a configuration value. By tapping into the supply, the application can execute a block of code—in this case, logging the change—whenever .a is updated.

Integration with Reactive Programming Patterns

config-supply integrates seamlessly with Raku's reactive programming constructs (react and whenever), allowing for elegant and powerful event-driven programming based on configuration changes.

Example: Dynamic Behavior Adjustment Based on Configuration Changes

use Test1Config;

react {
    # Dynamically adjust behavior based on changes to the `.a` value
    whenever config-supply(*.a) -> $new-value {
        say ".a has changed to: $new-value";
    }
}

CONFIGURATION ERROR HANDLING AND RESILIENCE

A key feature of the Configuration module is its robust error handling mechanism during runtime configuration changes. When the module detects an error in the configuration—such as invalid data types, missing required fields, or any condition that violates the configuration schema—it is designed to issue a warning rather than terminating the application. This approach ensures that your application remains operational, continuing with the last known correct configuration.

Graceful Error Management

The Configuration module adopts a non-intrusive error management strategy to maximize application uptime and resilience. In scenarios where a runtime configuration change introduces errors:

Benefits of This Approach

This error handling strategy offers several benefits:

By prioritizing continuity and stability, the Configuration module helps maintain the integrity of your application's runtime environment, even in the face of configuration errors. This design choice reflects a commitment to production-grade resilience and operability.

TESTING WITH CONFIGURATION ADJUSTMENTS

A common use case for dynamic configuration in testing involves feature toggles, which enable or disable application features during runtime. This example demonstrates testing a function, access-new-feature, which behaves differently based on the feature_flag setting in the AppConfig class.

AppConfig Definition

Define the AppConfig class with a feature_flag attribute. This boolean attribute determines whether a new feature is accessible within the application.

class AppConfig {
    has Bool $.feature_flag = False; # Default: Feature is off
}

Dynamic Configuration Setup for Testing Feature Toggles

We'll dynamically adjust the feature_flag setting to test the application's behavior when the new feature is turned on and off, ensuring that the application responds correctly in both scenarios.

use Test;
use YourConfigurationModule;

# Test the `access-new-feature` function with the feature enabled
set-config -> {
    config {
        .feature_flag = True; # Enable the new feature
    }
};

is-deeply
    access-new-feature(),
    expected-output-with-feature-enabled(),
    "Feature enabled - Validates access-new-feature with the feature toggle on";

# Test the `access-new-feature` function with the feature disabled
set-config -> {
    config {
        .feature_flag = False; # Ensure the feature is disabled
    }
};

is-deeply
    access-new-feature(),
    expected-output-with-feature-disabled(),
    "Feature disabled - Validates access-new-feature with the feature toggle off";

In this example, set-config is used to temporarily modify the application's configuration before each test. By toggling the feature_flag, we simulate conditions where the new feature is both available and unavailable. Each test case then verifies that access-new-feature performs as expected under these conditions.

Benefits of Testing Feature Toggles

Testing with dynamic configuration adjustments, especially for feature toggles, is essential for validating application behavior under different feature states. This approach enhances the ability to deliver reliable features and facilitates a smoother transition from development to production.

RETRIEVING THE CURRENT CONFIGURATION WITH GET-CONFIG

The get-config function is a straightforward way to access the current value of your application's configuration. This function returns the current configuration object, allowing for immediate access to its properties without monitoring for changes.

Example: Accessing Current Configuration Values

use Test1Config;

# Retrieve the current configuration
my $current-config = get-config();

say "Current configuration: ", $current-config.raku;

OBTAINING CONFIGURATION WITHOUT A SUPPLY WITH SINGLE-CONFIG-RUN

While config-run provides a Supply that emits configuration changes over time, single-config-run is designed to return the configuration object a single time. This function is useful when you only need to read the configuration once and do not require a reactive setup to monitor for changes.

Example: Using Single-Config-Run to Access Configuration

use Test1Config;

# Obtain the configuration a single time
my $config = single-config-run();

say "Configuration obtained once: ", $config.raku;

By incorporating these functions, developers are equipped with flexible tools for managing configuration according to the needs of their application, whether it's accessing the current configuration state, reacting to changes in real-time, or obtaining the configuration once without further monitoring.