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
- JSON Storage: Industry-standard format, human-readable, well-supported
- Dot Notation: Intuitive nested access without complex path objects
- Environment Fallback: 12-factor app compatibility
- Immutable Keys: Keys are strings, values are typed
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?
- Native JSON support for file I/O
- Already handles type coercion
- Pretty-printing built-in
- Validated JSON output
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:
- Creating intermediate objects that don't exist
- Deciding behavior when path partially exists
- Complex error handling for type mismatches
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
- Config First: File values take precedence for predictability
- Environment Override: Production can override without file changes
- Safe Defaults: Application always has a fallback value
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
- Know when to prompt "Save changes?"
- Prevent accidental data loss
- Enable dirty-checking patterns
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
- Each section is a full SIMPLE_CONFIG instance
- Can be saved independently
- Can be passed to subsystems
- Changes to section do NOT affect parent (copy semantics)
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:
string_value: Returns Voidinteger_value: Returns 0boolean_value: Returns Falsereal_value: Returns 0.0
Use has_key first if you need to distinguish "missing" from "wrong type".
Dependencies
Required Libraries
- simple_json: JSON parsing, object manipulation, pretty printing
- base: STRING, ARRAYED_LIST, EXECUTION_ENVIRONMENT
Dependency Flow
simple_config
│
└──> simple_json
│
└──> base (EiffelStudio)