simple_config

User Guide

Introduction

simple_config provides JSON-based configuration file management for Eiffel applications. It features dot-notation path access for nested values, environment variable fallback, and config file merging.

Installation

1. Set Environment Variable

export SIMPLE_EIFFEL=/d/prod

2. Add to ECF

<library name="simple_config" location="$SIMPLE_EIFFEL/simple_config/simple_config.ecf"/>

3. Dependencies

simple_config uses simple_json internally. With SIMPLE_EIFFEL set, all dependencies are automatically resolved.

Configuration Files

JSON Format

Configuration files are standard JSON:

{
    "app": {
        "name": "MyApplication",
        "version": "1.0.0",
        "debug": false
    },
    "database": {
        "host": "localhost",
        "port": 5432,
        "name": "mydb",
        "pool_size": 10
    },
    "features": {
        "caching": true,
        "logging": true,
        "metrics": false
    },
    "allowed_hosts": ["localhost", "127.0.0.1", "example.com"]
}

Dot Notation

Access nested values using dot notation:

local
    config: SIMPLE_CONFIG
do
    create config.make_with_file ("config.json")

    -- Direct access with dot notation
    print (config.string_value ("database.host"))  -- "localhost"
    print (config.integer_value ("database.port")) -- 5432
    print (config.boolean_value ("features.caching")) -- True
end

Loading Configuration

From File

local
    config: SIMPLE_CONFIG
do
    -- Load on creation
    create config.make_with_file ("config.json")
end

Empty Configuration

local
    config: SIMPLE_CONFIG
do
    -- Create empty, build programmatically
    create config.make
    config.set_string ("app.name", "MyApp")
    config.set_integer ("port", 8080)
end

Reloading

-- Reload from disk (useful for hot-reload)
config.load

Accessing Values

String Values

local
    config: SIMPLE_CONFIG
    host: STRING
do
    create config.make_with_file ("config.json")

    -- Basic access (may be Void)
    if attached config.string_value ("database.host") as h then
        host := h
    end

    -- With default value
    host := config.string_value_or_default ("database.host", "localhost")

    -- From config or environment variable
    host := config.string_value_or_env ("database.host", "DB_HOST")

    -- Config, then env, then default
    host := config.string_value_or_env_or_default ("database.host", "DB_HOST", "localhost")
end

Integer Values

local
    port: INTEGER
do
    -- Basic (0 if not found)
    port := config.integer_value ("database.port")

    -- With default
    port := config.integer_value_or_default ("database.port", 5432)

    -- From config or env or default
    port := config.integer_value_or_env ("database.port", "DB_PORT", 5432)
end

Boolean Values

local
    debug_mode: BOOLEAN
do
    -- Basic (False if not found)
    debug_mode := config.boolean_value ("app.debug")

    -- With default
    debug_mode := config.boolean_value_or_default ("app.debug", False)
end

Real/Double Values

local
    timeout: DOUBLE
do
    -- Basic (0.0 if not found)
    timeout := config.real_value ("network.timeout")

    -- With default
    timeout := config.real_value_or_default ("network.timeout", 30.0)
end

Array Values

local
    hosts: ARRAYED_LIST [STRING]
    ports: ARRAYED_LIST [INTEGER]
    thresholds: ARRAYED_LIST [DOUBLE]
do
    -- String arrays
    hosts := config.string_list ("allowed_hosts")
    across hosts as h loop
        print (h + "%N")
    end

    -- Integer arrays
    ports := config.integer_list ("service.ports")

    -- Real arrays
    thresholds := config.real_list ("metrics.thresholds")
end

Working with Sections

Getting Sections

local
    config, db_config: SIMPLE_CONFIG
do
    create config.make_with_file ("config.json")

    -- Get nested section as separate config
    if attached config.section ("database") as db then
        -- Access values directly without prefix
        print (db.string_value ("host"))
        print (db.integer_value ("port"))
    end
end

Deep Sections

-- With config like: {"logging": {"handlers": {"file": {"path": "/var/log"}}}}
if attached config.section ("logging.handlers.file") as file_handler then
    print (file_handler.string_value ("path"))
end

Environment Variable Fallback

Priority Chain

The _or_env features check in order:

  1. Configuration file value
  2. Environment variable
  3. Default value (if provided)
local
    config: SIMPLE_CONFIG
    db_host: STRING
do
    create config.make_with_file ("config.json")

    -- If "database.host" exists in config, use it
    -- Otherwise, check $DB_HOST environment variable
    -- Finally, use "localhost" as default
    db_host := config.string_value_or_env_or_default (
        "database.host",
        "DB_HOST",
        "localhost"
    )
end

Use Cases

Modifying Configuration

Setting Values

local
    config: SIMPLE_CONFIG
do
    create config.make

    -- Set various types
    config.set_string ("app.name", "MyApp")
    config.set_integer ("port", 8080)
    config.set_boolean ("debug", True)
    config.set_real ("timeout", 30.5)

    -- Check modification status
    if config.is_modified then
        print ("Config has unsaved changes%N")
    end
end

Setting Nested Sections

local
    config, db_section: SIMPLE_CONFIG
do
    create config.make

    -- Create a section
    create db_section.make
    db_section.set_string ("host", "localhost")
    db_section.set_integer ("port", 5432)

    -- Add section to main config
    config.set_section ("database", db_section)
end

Removing Values

-- Remove a key
config.remove ("debug")

Saving Configuration

Save to Original File

local
    config: SIMPLE_CONFIG
do
    create config.make_with_file ("config.json")
    config.set_boolean ("debug", True)

    -- Save back to config.json
    config.save
end

Save to New File

-- Save to a different file
config.save_to ("config.production.json")

Export as JSON String

-- Compact JSON
print (config.to_json)

-- Pretty-printed JSON
print (config.to_json_pretty)

Merging Configuration Files

Environment Overrides

local
    config: SIMPLE_CONFIG
do
    -- Load base configuration
    create config.make_with_file ("config.json")

    -- Merge environment-specific overrides
    config.merge_file ("config.production.json")

    -- Values from production override base values
end

Merge Strategy

The merge operation:

Checking for Keys

local
    config: SIMPLE_CONFIG
do
    create config.make_with_file ("config.json")

    -- Check if key exists (supports dot notation)
    if config.has_key ("database.host") then
        print ("Database host is configured%N")
    end

    if not config.has_key ("cache.enabled") then
        print ("Cache not configured, using defaults%N")
    end
end

Best Practices

1. Use Environment Fallback for Sensitive Data

-- Never store passwords in config files
password := config.string_value_or_env_or_default (
    "database.password",
    "DB_PASSWORD",
    ""
)

2. Provide Sensible Defaults

-- Always have fallback values for optional settings
timeout := config.integer_value_or_default ("network.timeout", 30)
retries := config.integer_value_or_default ("network.retries", 3)

3. Validate Required Configuration

if not config.has_key ("database.host") then
    print ("Error: database.host is required%N")
    die (1)
end

4. Use Sections for Organization

-- Pass sections to subsystems
if attached config.section ("database") as db_config then
    database.configure (db_config)
end