simple_config

Architecture

Design Overview

simple_config is built on top of simple_json, providing a high-level configuration API while leveraging JSON for storage format.

Key Design Decisions

Class Structure

SIMPLE_CONFIG
    │
    ├── Creation
    │   ├── make                    -- Empty config
    │   └── make_with_file          -- Load from file
    │
    ├── Access (dot notation reads)
    │   ├── string_value, integer_value, boolean_value, real_value
    │   ├── *_or_default variants
    │   ├── *_or_env variants
    │   └── string_list, integer_list, real_list
    │
    ├── Section Access
    │   └── section                 -- Returns child SIMPLE_CONFIG
    │
    ├── Modification (top-level only)
    │   ├── set_string, set_integer, set_boolean, set_real
    │   ├── set_section
    │   └── remove
    │
    └── File Operations
        ├── load, merge_file
        ├── save, save_to
        └── to_json, to_json_pretty

Data Storage

Internal Representation

feature {SIMPLE_CONFIG}
    data: SIMPLE_JSON_OBJECT
        -- JSON object holding all configuration

    env: EXECUTION_ENVIRONMENT
        -- Environment variable access

Why SIMPLE_JSON_OBJECT?

Dot Notation Implementation

Path Navigation Algorithm

get_value_at_path (a_path: STRING): detachable SIMPLE_JSON_VALUE
    -- Navigate dot-separated path like "database.host"
local
    l_parts: LIST [STRING]
    l_current: detachable SIMPLE_JSON_OBJECT
do
    if not a_path.has ('.') then
        -- Simple key lookup
        Result := data.item (a_path)
    else
        -- Split and navigate
        l_parts := a_path.split ('.')
        l_current := data
        across l_parts as part loop
            if part is last then
                Result := l_current.item (part)
            else
                -- Navigate deeper
                l_current := l_current.item (part).as_object
            end
        end
    end
end

Why Read-Only Dot Notation?

Setting nested values with dot notation would require:

Instead, use set_section for explicit control over nested structure.

Environment Fallback Design

Priority Chain

string_value_or_env_or_default (key, env_var, default)
    │
    ├─1─> Check config: string_value (key)
    │     └─> If found: return value
    │
    ├─2─> Check environment: env.item (env_var)
    │     └─> If found: return value
    │
    └─3─> Return default

Design Rationale

Integer from Environment

integer_value_or_env (a_key, a_env_var: STRING; a_default: INTEGER): INTEGER
    -- Parse integer from env var string
do
    if has_key (a_key) then
        Result := integer_value (a_key)
    else
        if attached env.item (a_env_var) as e then
            if e.is_integer then
                Result := e.to_integer
            else
                Result := a_default  -- Not parseable
            end
        else
            Result := a_default
        end
    end
end

File Merging Strategy

Merge Algorithm

merge_file (a_file_path: STRING)
    -- Merge another config, overriding existing values
do
    -- Parse other file
    l_other := json.parse_file (a_file_path).as_object

    -- For each key in other, put into self
    across l_other.keys as key loop
        data.put_value (l_other.item (key), key)
    end

    is_modified := True
end

Merge Behavior

Scenario Result
Key exists in both New value overwrites old
Key only in base Preserved
Key only in merged Added
Nested objects Entire object replaced (not deep merged)

Note: Merge is shallow. To deep-merge nested objects, merge at the section level.

Modification Tracking

Purpose

Implementation

set_string (a_key, a_value: STRING)
    do
        data.put_string (a_value, a_key)
        is_modified := True  -- Track change
    ensure
        modified: is_modified
    end

save
    do
        -- Write to file
        l_file.put_string (to_json_pretty)
        is_modified := False  -- Reset flag
    ensure
        not_modified: not is_modified
    end

Section Architecture

Section as Independent Config

section (a_key: STRING): detachable SIMPLE_CONFIG
    -- Get nested section as separate config object
do
    if attached get_value_at_path (a_key) as v and then v.is_object then
        create Result.make
        Result.set_data (v.as_object)
    end
end

Section Independence

Type Coercion

JSON to Eiffel Mapping

JSON Type Eiffel Type Feature
string STRING string_value
number (integer) INTEGER integer_value
number (float) DOUBLE real_value
boolean BOOLEAN boolean_value
array ARRAYED_LIST string_list, etc.
object SIMPLE_CONFIG section

Type Mismatch Handling

If the JSON value doesn't match the requested type:

Use has_key first if you need to distinguish "missing" from "wrong type".

Dependencies

Required Libraries

Dependency Flow

simple_config
    │
    └──> simple_json
             │
             └──> base (EiffelStudio)