Skip to content

Commit 91ca1eb

Browse files
bordeuxclaude
andcommitted
feat: add object manipulation functions
Implement comprehensive object manipulation functions for working with nested data structures: New Functions: - object_merge(obj1, obj2) - Deep merge two objects recursively - object_get(object, path) - Get nested value by dot-separated path - object_set(object, path, value) - Set nested value, creates intermediate objects - object_keys(object) - Extract all keys as array - object_values(object) - Extract all values as array - object_has_key(object, key) - Check if object has specific key Implementation Details: - Deep merge with recursive helper function - Dot-separated path notation (e.g., "a.b.c") - Array index access support in paths (e.g., "items.0") - Automatic creation of intermediate objects in object_set - Returns undefined for missing paths (safe access) - Comprehensive error handling for type mismatches Files Added: - src/functions/object.rs (370 lines) - Complete implementation with docs - tests/test_object_functions.rs (452 lines) - 30 comprehensive tests Files Modified: - src/functions/mod.rs - Registered 6 object functions - README.md - Added "Object Manipulation Functions" section with examples - TODO.md - Marked all 6 object functions as complete Tests: - 30 new tests covering all functions and edge cases - Tests include: simple/nested/deep nested objects, arrays, empty objects - Type error handling, missing key scenarios, roundtrip operations - All 515+ tests passing Use Cases: - Configuration merging (base + environment-specific overrides) - Safe nested value access without panics - Dynamic configuration building - Validation of required configuration keys - Feature flag checking 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent ea693fa commit 91ca1eb

5 files changed

Lines changed: 1157 additions & 8 deletions

File tree

README.md

Lines changed: 328 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ A fast and simple command-line template rendering tool using [MiniJinja](https:/
2727
- [Path Manipulation Functions](#path-manipulation-functions)
2828
- [Data Parsing Functions](#data-parsing-functions)
2929
- [Data Serialization Functions](#data-serialization-functions)
30+
- [Object Manipulation Functions](#object-manipulation-functions)
3031
- [Validation Functions](#validation-functions)
3132
- [Debugging & Development Functions](#debugging--development-functions)
3233
- [Advanced Examples](#advanced-examples)
@@ -47,9 +48,8 @@ Get started in 30 seconds:
4748
# Or use Docker to copy the binary (recommended for CI/CD):
4849
# Create a Dockerfile to extract the binary
4950
cat > Dockerfile << 'EOF'
50-
FROM ghcr.io/bordeux/tmpltool:latest AS tmpltool
5151
FROM alpine:latest
52-
COPY --from=tmpltool /tmpltool /usr/local/bin/tmpltool
52+
COPY --from=ghcr.io/bordeux/tmpltool:latest /tmpltool /usr/local/bin/tmpltool
5353
EOF
5454

5555
docker build -t myapp .
@@ -69,6 +69,7 @@ tmpltool greeting.tmpl
6969
- **Filesystem**: Read files, check existence, list directories, glob patterns, file info, path manipulation
7070
- **Data Parsing**: Parse and read JSON, YAML, TOML files
7171
- **Data Serialization**: Convert objects to JSON, YAML, TOML strings with pretty-printing options
72+
- **Object Manipulation**: Deep merge, get/set nested values by path, extract keys/values, check key existence
7273
- **Validation**: Validate emails, URLs, IPs, UUIDs, regex matching
7374
- **System & Network**: Get hostname, username, directories, IP addresses, DNS resolution, port availability
7475
- **Debugging & Development**: Debug output, type checking, assertions, warnings, error handling
@@ -2045,6 +2046,331 @@ port = 5432
20452046
{{ to_toml(object=env_config) }}
20462047
```
20472048
2049+
### Object Manipulation Functions
2050+
2051+
Work with objects (maps/dictionaries) to merge, access nested values, and inspect structure. These functions are essential for complex configuration generation and data transformation.
2052+
2053+
#### `object_merge(obj1, obj2)`
2054+
2055+
Deep merge two objects. When keys conflict, values from `obj2` override values from `obj1`. Nested objects are merged recursively.
2056+
2057+
**Arguments:**
2058+
- `obj1` (required) - First object (base)
2059+
- `obj2` (required) - Second object (overlay, takes precedence)
2060+
2061+
**Returns:** New object with merged values
2062+
2063+
**Examples:**
2064+
```jinja
2065+
{# Simple merge #}
2066+
{% set base = {"a": 1, "b": 2} %}
2067+
{% set overlay = {"c": 3, "d": 4} %}
2068+
{% set merged = object_merge(obj1=base, obj2=overlay) %}
2069+
{{ to_json(object=merged) }}
2070+
{# Output: {"a":1,"b":2,"c":3,"d":4} #}
2071+
2072+
{# Override values #}
2073+
{% set defaults = {"host": "localhost", "port": 8080, "debug": false} %}
2074+
{% set custom = {"port": 3000, "debug": true} %}
2075+
{% set config = object_merge(obj1=defaults, obj2=custom) %}
2076+
{{ to_json(object=config) }}
2077+
{# Output: {"host":"localhost","port":3000,"debug":true} #}
2078+
2079+
{# Deep merge nested objects #}
2080+
{% set base_config = {
2081+
"server": {"host": "localhost", "port": 8080},
2082+
"database": {"host": "db.local", "port": 5432}
2083+
} %}
2084+
{% set env_overrides = {
2085+
"server": {"port": 9000, "ssl": true},
2086+
"cache": {"enabled": true}
2087+
} %}
2088+
{% set final_config = object_merge(obj1=base_config, obj2=env_overrides) %}
2089+
{{ to_json(object=final_config, pretty=true) }}
2090+
{# Output:
2091+
{
2092+
"server": {
2093+
"host": "localhost",
2094+
"port": 9000,
2095+
"ssl": true
2096+
},
2097+
"database": {
2098+
"host": "db.local",
2099+
"port": 5432
2100+
},
2101+
"cache": {
2102+
"enabled": true
2103+
}
2104+
}
2105+
#}
2106+
```
2107+
2108+
#### `object_get(object, path)`
2109+
2110+
Get nested value from an object using dot-separated path notation. Supports accessing nested objects and array indices.
2111+
2112+
**Arguments:**
2113+
- `object` (required) - Object to query
2114+
- `path` (required) - Dot-separated path (e.g., "a.b.c" or "items.0")
2115+
2116+
**Returns:** Value at the specified path, or undefined if not found
2117+
2118+
**Examples:**
2119+
```jinja
2120+
{# Simple property access #}
2121+
{% set config = {"host": "localhost", "port": 8080} %}
2122+
{{ object_get(object=config, path="host") }}
2123+
{# Output: localhost #}
2124+
2125+
{# Nested property access #}
2126+
{% set config = {
2127+
"server": {
2128+
"database": {
2129+
"host": "db.example.com",
2130+
"port": 5432
2131+
}
2132+
}
2133+
} %}
2134+
{{ object_get(object=config, path="server.database.host") }}
2135+
{# Output: db.example.com #}
2136+
2137+
{# Array index access #}
2138+
{% set data = {"items": [10, 20, 30, 40]} %}
2139+
{{ object_get(object=data, path="items.1") }}
2140+
{# Output: 20 #}
2141+
2142+
{# Safe access with default fallback #}
2143+
{% set config = {"server": {"host": "localhost"}} %}
2144+
{% set port = object_get(object=config, path="server.port") %}
2145+
{% if port is undefined %}
2146+
Port not configured, using default: 8080
2147+
{% else %}
2148+
Port: {{ port }}
2149+
{% endif %}
2150+
2151+
{# Complex nested access #}
2152+
{% set k8s_config = {
2153+
"spec": {
2154+
"template": {
2155+
"spec": {
2156+
"containers": [
2157+
{"name": "app", "image": "myapp:latest"}
2158+
]
2159+
}
2160+
}
2161+
}
2162+
} %}
2163+
{{ object_get(object=k8s_config, path="spec.template.spec.containers.0.image") }}
2164+
{# Output: myapp:latest #}
2165+
```
2166+
2167+
#### `object_set(object, path, value)`
2168+
2169+
Set nested value in an object using dot-separated path notation. Creates intermediate objects as needed.
2170+
2171+
**Arguments:**
2172+
- `object` (required) - Object to modify
2173+
- `path` (required) - Dot-separated path (e.g., "a.b.c")
2174+
- `value` (required) - Value to set
2175+
2176+
**Returns:** New object with the value set at the specified path
2177+
2178+
**Examples:**
2179+
```jinja
2180+
{# Simple property set #}
2181+
{% set config = {"host": "localhost"} %}
2182+
{% set updated = object_set(object=config, path="port", value=8080) %}
2183+
{{ to_json(object=updated) }}
2184+
{# Output: {"host":"localhost","port":8080} #}
2185+
2186+
{# Set nested property #}
2187+
{% set config = {"server": {"host": "localhost"}} %}
2188+
{% set updated = object_set(object=config, path="server.port", value=8080) %}
2189+
{{ to_json(object=updated) }}
2190+
{# Output: {"server":{"host":"localhost","port":8080}} #}
2191+
2192+
{# Create nested path automatically #}
2193+
{% set config = {} %}
2194+
{% set updated = object_set(object=config, path="database.primary.host", value="db1.example.com") %}
2195+
{{ to_json(object=updated, pretty=true) }}
2196+
{# Output:
2197+
{
2198+
"database": {
2199+
"primary": {
2200+
"host": "db1.example.com"
2201+
}
2202+
}
2203+
}
2204+
#}
2205+
2206+
{# Build configuration step by step #}
2207+
{% set config = {} %}
2208+
{% set config = object_set(object=config, path="server.host", value=get_env(name="HOST", default="0.0.0.0")) %}
2209+
{% set config = object_set(object=config, path="server.port", value=get_env(name="PORT", default="8080") | int) %}
2210+
{% set config = object_set(object=config, path="database.url", value=get_env(name="DATABASE_URL")) %}
2211+
{{ to_json(object=config, pretty=true) }}
2212+
```
2213+
2214+
#### `object_keys(object)`
2215+
2216+
Get all keys from an object as an array.
2217+
2218+
**Arguments:**
2219+
- `object` (required) - Object to get keys from
2220+
2221+
**Returns:** Array of string keys
2222+
2223+
**Examples:**
2224+
```jinja
2225+
{# Get all keys #}
2226+
{% set config = {"host": "localhost", "port": 8080, "debug": true} %}
2227+
{% set keys = object_keys(object=config) %}
2228+
{{ to_json(object=keys) }}
2229+
{# Output: ["host","port","debug"] #}
2230+
2231+
{# Iterate over keys #}
2232+
{% set config = {"host": "localhost", "port": 8080, "debug": true} %}
2233+
Configuration keys:
2234+
{% for key in object_keys(object=config) %}
2235+
- {{ key }}
2236+
{% endfor %}
2237+
{# Output:
2238+
Configuration keys:
2239+
- host
2240+
- port
2241+
- debug
2242+
#}
2243+
2244+
{# Dynamic configuration display #}
2245+
{% set config = {
2246+
"SERVER_HOST": "localhost",
2247+
"SERVER_PORT": 8080,
2248+
"DATABASE_URL": "postgres://localhost/mydb"
2249+
} %}
2250+
# Environment Variables
2251+
{% for key in object_keys(object=config) %}
2252+
{{ key }}={{ config[key] }}
2253+
{% endfor %}
2254+
```
2255+
2256+
#### `object_values(object)`
2257+
2258+
Get all values from an object as an array.
2259+
2260+
**Arguments:**
2261+
- `object` (required) - Object to get values from
2262+
2263+
**Returns:** Array of values
2264+
2265+
**Examples:**
2266+
```jinja
2267+
{# Get all values #}
2268+
{% set config = {"a": 1, "b": 2, "c": 3} %}
2269+
{% set values = object_values(object=config) %}
2270+
{{ to_json(object=values) }}
2271+
{# Output: [1,2,3] #}
2272+
2273+
{# Process all values #}
2274+
{% set ports = {"http": 80, "https": 443, "app": 8080} %}
2275+
Open ports:
2276+
{% for port in object_values(object=ports) %}
2277+
- {{ port }}
2278+
{% endfor %}
2279+
{# Output:
2280+
Open ports:
2281+
- 80
2282+
- 443
2283+
- 8080
2284+
#}
2285+
2286+
{# Mixed type values #}
2287+
{% set config = {"str": "hello", "num": 42, "bool": true} %}
2288+
{% for value in object_values(object=config) %}
2289+
Value: {{ value }} (type: {{ type_of(value=value) }})
2290+
{% endfor %}
2291+
```
2292+
2293+
#### `object_has_key(object, key)`
2294+
2295+
Check if an object has a specific key.
2296+
2297+
**Arguments:**
2298+
- `object` (required) - Object to check
2299+
- `key` (required) - Key to check for
2300+
2301+
**Returns:** Boolean - true if key exists, false otherwise
2302+
2303+
**Examples:**
2304+
```jinja
2305+
{# Simple key check #}
2306+
{% set config = {"host": "localhost", "port": 8080} %}
2307+
{{ object_has_key(object=config, key="host") }}
2308+
{# Output: true #}
2309+
2310+
{{ object_has_key(object=config, key="database") }}
2311+
{# Output: false #}
2312+
2313+
{# Conditional configuration #}
2314+
{% set config = {"host": "localhost", "port": 8080} %}
2315+
{% if object_has_key(object=config, key="debug") %}
2316+
Debug mode: {{ config.debug }}
2317+
{% else %}
2318+
Debug mode not configured (using default: false)
2319+
{% endif %}
2320+
2321+
{# Validate required fields #}
2322+
{% set config = read_json_file(path="config.json") %}
2323+
{% set required_keys = ["host", "port", "database_url"] %}
2324+
{% for key in required_keys %}
2325+
{% if not object_has_key(object=config, key=key) %}
2326+
ERROR: Missing required configuration key: {{ key }}
2327+
{% endif %}
2328+
{% endfor %}
2329+
2330+
{# Feature flags #}
2331+
{% set features = {"api": true, "websockets": true} %}
2332+
{% if object_has_key(object=features, key="websockets") and features.websockets %}
2333+
WebSocket support enabled
2334+
{% endif %}
2335+
```
2336+
2337+
**Practical Example - Configuration Merging:**
2338+
```jinja
2339+
{# Load base configuration #}
2340+
{% set base_config = read_json_file(path="config.base.json") %}
2341+
2342+
{# Load environment-specific overrides #}
2343+
{% set env = get_env(name="ENVIRONMENT", default="development") %}
2344+
{% set env_config_path = "config." ~ env ~ ".json" %}
2345+
2346+
{% if file_exists(path=env_config_path) %}
2347+
{% set env_config = read_json_file(path=env_config_path) %}
2348+
{% set config = object_merge(obj1=base_config, obj2=env_config) %}
2349+
{% else %}
2350+
{% set config = base_config %}
2351+
{% endif %}
2352+
2353+
{# Apply environment variable overrides #}
2354+
{% if get_env(name="DATABASE_URL") %}
2355+
{% set config = object_set(object=config, path="database.url", value=get_env(name="DATABASE_URL")) %}
2356+
{% endif %}
2357+
2358+
{% if get_env(name="PORT") %}
2359+
{% set config = object_set(object=config, path="server.port", value=get_env(name="PORT") | int) %}
2360+
{% endif %}
2361+
2362+
{# Validate required keys #}
2363+
{% set required = ["server.host", "server.port", "database.url"] %}
2364+
{% for key_path in required %}
2365+
{% if object_get(object=config, path=key_path) is undefined %}
2366+
ERROR: Missing required configuration: {{ key_path }}
2367+
{% endif %}
2368+
{% endfor %}
2369+
2370+
{# Output final configuration #}
2371+
{{ to_json(object=config, pretty=true) }}
2372+
```
2373+
20482374
### System & Network Functions
20492375
20502376
Access system information and perform network operations.

TODO.md

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@ This document contains ideas for new functions and features to make tmpltool mor
6060
- [x] `to_yaml(object)` - Convert object to YAML string
6161
- [x] `to_toml(object)` - Convert object to TOML string
6262

63+
### ✅ Object Manipulation
64+
- [x] `object_merge(obj1, obj2)` - Deep merge two objects
65+
- [x] `object_get(object, path)` - Get nested value by path
66+
- [x] `object_set(object, path, value)` - Set nested value by path
67+
- [x] `object_keys(object)` - Get object keys as array
68+
- [x] `object_values(object)` - Get object values as array
69+
- [x] `object_has_key(object, key)` - Check if object has key
70+
6371
### ✅ Validation
6472
- [x] `is_email(string)` - Validate email format
6573
- [x] `is_url(string)` - Validate URL format
@@ -177,12 +185,12 @@ This document contains ideas for new functions and features to make tmpltool mor
177185
- [x] `to_toml(object)` - Convert object to TOML string
178186

179187
**Object Functions:**
180-
- [ ] `object_merge(obj1, obj2)` - Deep merge two objects
181-
- [ ] `object_get(object, path)` - Get nested value by path (e.g., "a.b.c")
182-
- [ ] `object_set(object, path, value)` - Set nested value by path
183-
- [ ] `object_keys(object)` - Get object keys as array
184-
- [ ] `object_values(object)` - Get object values as array
185-
- [ ] `object_has_key(object, key)` - Check if object has key
188+
- [x] `object_merge(obj1, obj2)` - Deep merge two objects
189+
- [x] `object_get(object, path)` - Get nested value by path (e.g., "a.b.c")
190+
- [x] `object_set(object, path, value)` - Set nested value by path
191+
- [x] `object_keys(object)` - Get object keys as array
192+
- [x] `object_values(object)` - Get object values as array
193+
- [x] `object_has_key(object, key)` - Check if object has key
186194

187195
**Array Functions:**
188196
- [ ] `array_sort_by(array, key)` - Sort array by object key

0 commit comments

Comments
 (0)