state-engine 0.1.5

Declarative state data management system for process
Documentation
# YAML Extended DSL guide

## terms

- `meta keys`: keys prefixed with `_`, along with all keys nested beneath them
- `field keys`: keys that are not meta keys
- `leaf keys`: keys that hold a value instead of child keys
- `value`: a leaf key's value; equals null when omitted in YAML
- `path`: dot-separated key names leading from a start key to the target key
- `qualified path`: a path starting with `filename.`, uniquely identifying a key across all files
- `placeholder`: notation in the form `${path}` that references the result of `State::get()` for the specified key
- `template`: notation that embeds one or more placeholders into a string, such as `"user:${user_id}"`

## rules

- YAML document separators (`---`) are not supported
- `placeholder` and `template` are only valid inside values

## Basic Structure

```yaml
field_key:
  _state: # Data type definition (optional)
  _store: # Where to save (required at root, inherited by children)
  _load:  # Where to load from (optional)
```

## Core Concepts

### 1. meta key inheritance

Each field key inherits parent's meta keys, and can override:

```yaml
_store:
  client: KVS
  key: "root:${id}"

user:
  _store:
    key: "user:${sso_user_id}"  # Override only key, inherit client: KVS

  tenant_id:
    # Inherits _store from parent (client: KVS, key: user:${sso_user_id})
```

### 2. Placeholder Resolution

State engine resolves `${...}` by calling `State::get()`:

```yaml
tenant:
  _load:
    table: "tenants"
    where: "id=${user.tenant_id}"  # → State::get("user.tenant_id")
```

**Placeholder shorthand:**

Whether a path is absolute or relative is determined by whether it contains `.`:

- No `.` → relative path, automatically qualified to `filename.ancestors.path` at parse time
- Contains `.` → treated as absolute path, used as-is

```yaml
# Inside user.tenant_id in cache.yml
key: "${org_id}"            # → cache.user.org_id (relative)
key: "${cache.user.org_id}" # → cache.user.org_id (absolute, same result)
key: "${session.sso_user_id}" # → session.sso_user_id (cross-file reference)
```

**Limitation:** The shorthand (relative path) cannot contain `.`, so to reference a child of a sibling node, use a fully qualified path:

```yaml
# NG: treated as absolute path, KeyNotFound (no filename prefix)
key: "${user.id}"       # → State::get("user.id")

# OK: use fully qualified path
key: "${cache.user.id}" # → State::get("cache.user.id")
```

### 3. Client Types

**For _store** (where to save):
```yaml
_store:
  client: InMemory  # Process memory
  client: KVS       # Redis, Memcached
  client: HTTP      # HTTP endpoint
```

**For _load** (where to load from):
```yaml
_load:
  client: State     # Reference another State key
  client: InMemory  # Process memory
  client: Env       # Environment variables
  client: KVS       # Redis, Memcached
  client: Db        # Database
  client: HTTP      # HTTP endpoint
```

You must implement an adapter for each client you use (see Required Ports).

#### Client-Specific Parameters

**_store.client: InMemory**
```yaml
_store:
  client: InMemory
  key: "session:${token}"            # (string) Storage key (placeholders allowed)
```

**_load.client: Env**
```yaml
_load:
  client: Env
  map:                               # (object, required) Environment variable mapping
    yaml_key: "ENV_VAR_NAME"
```

**_load.client: State**
```yaml
_load:
  client: State
  key: "${org_id}"                   # (string) Reference to another state key
```

**_store.client: KVS**
```yaml
_store:
  client: KVS
  key: "user:${id}"                  # (string) Storage key (placeholders allowed)
  ttl: 3600                          # (integer, optional) TTL in seconds
```

**_load.client: Db**
```yaml
_load:
  client: Db
  connection: ${connection.tenant}  # (Value) Connection config object or reference
  table: "users"                    # (string) Table name
  where: "id=${user.id}"            # (string, optional) WHERE clause
  map:                               # (object, required) Column mapping
    yaml_key: "db_column"
```

**_store.client: HTTP / _load.client: HTTP**
```yaml
_store:
  client: HTTP
  url: "https://api.example.com/state/${id}"  # (string) Endpoint URL (placeholders allowed)
  headers:                                     # (object, optional) Request headers
    Authorization: "Bearer ${token}"

_load:
  client: HTTP
  url: "https://api.example.com/data/${id}"   # (string) Endpoint URL (placeholders allowed)
  headers:                                     # (object, optional) Request headers
    Authorization: "Bearer ${token}"
  map:                                         # (object, optional) Field extraction from response
    yaml_key: "response_field"
```

## State Methods

**State::get(key)** -> `Result<Option<Value>, StateError>`
- Retrieves value from instance cache / store
- Triggers auto-load on miss if `_load` is defined
- Returns `Ok(Some(value))` on hit, `Ok(None)` on miss with no load, `Err` on error

**State::set(key, value, ttl)** -> `Result<bool, StateError>`
- Saves value to persistent store and instance cache
- Does NOT trigger auto-load
- TTL parameter is optional (KVS only)

**State::delete(key)** -> `Result<bool, StateError>`
- Removes key from both persistent store and instance cache
- Key will show as miss after deletion

**State::exists(key)** -> `Result<bool, StateError>`
- Checks if key exists without triggering auto-load
- Returns `Ok(true/false)`
- Lightweight existence check for conditional logic