state-engine 0.1.5

Declarative state data management system for process
Documentation
# Architecture

## index

- provided modules (ライブラリ提供モジュール)
  1. State

-  required modules (ライブラリ要求モジュール*)
  1. InMemoryClient
  2. KVSClient
  3. DbClient
  4. EnvClient
  5. HttpClient
  6. FileClient

- internal modules (内部モジュール)
  1. core::Manifest
  2. Store
  3. Load
  4. u64(fixed_bits.rs)
  5. Pools & Maps(pool.rs)
  6. parser.rs
  7. LogFormat

*: いずれもoptional(必須ではない)

---

## Ports

ライブラリの外部向けインターフェース定義modules

1. Provided Port

**State** がライブラリ唯一の公開APIです。

manifest YAMLの`_store`/`_load`定義に従い、`get()` / `set()` / `delete()` / `exists()`操作を提供するmoduleです。`get()`はkey missをトリガーに`_load`定義に基づいて自動ロードを試みます。`set()`は自動ロードを引き起こしません。`delete()`はストアとインスタンスキャッシュ両方から削除します。`exists()`は自動ロードを引き起こさずにkey存在確認を行います。

StateはYAML I/Oを担います。`FileClient`経由でmanifestファイルを読み込み、初回アクセス時に`core::Manifest`へparseします。`core::Manifest`はno_stdの内部structで、全bitレコードデータを所有しdecode/find/build_configクエリを提供します。`_store`/`_load`/`_state`メタデータは親から子へ継承され、子が上書きできます。

2. Required Ports

ライブラリ動作時にimpl実装が必要なmoduleのtraits

  1. **InMemoryClient**
    - 必要なメソッド: `get()`/`set()`/`delete()`
    - 渡される引数: `"key": Manifestの_{store,load}.key:の値`
    - 想定対象ストア: ローカルプロセスメモリ
    - インスタンスメモリのState::cacheにて、_store.clientの値に依らず、キャッシュが常にされている点に留意して下さい。
  2. **KVSClient**
    - 必要なメソッド: `get()`/`set()`/`delete()`
    - traitシグネチャ:
      - `fn get(&self, key: &str) -> Option<String>`
      - `fn set(&self, key: &str, value: String, ttl: Option<u64>) -> bool`
      - `fn delete(&self, key: &str) -> bool`
    - 渡される引数: `"key": Manifestの_{store,load}.key:の値`, `ttl: Manifestの_{store,load}.ttl:の値(オプション)`
    - 想定対象ストア: Key-Valueストア(Redis等)
    - **重要**: KVSClientはString型のみを扱う(プリミティブ型)。State層がserialize/deserializeを実行:
      - **serialize**: 全ての値 → JSON文字列(型情報を保持: Number/String/Bool/Null/Array/Object)
      - **deserialize**: JSON文字列 → Value(型を正確に復元)
      - KVSにはJSON文字列としてデータを保存。JSON形式でKVSネイティブ型に依存せず型情報を保持。
  3. **DbClient**
    - 必要なメソッド: `get()`/`set()`/`delete()`
    - traitシグネチャ:
      - `fn get(&self, connection: &Value, table: &str, columns: &[&str], where_clause: Option<&str>) -> Option<Vec<HashMap<String, Value>>>`
      - `fn set(&self, connection: &Value, table: &str, values: &HashMap<String, Value>, where_clause: Option<&str>) -> bool`
      - `fn delete(&self, connection: &Value, table: &str, where_clause: Option<&str>) -> bool`
    - 渡される引数: `"connection": YAML記載の_{load}.connection:の値`, `"table": YAML記載の_{load}.table:の値`, `"columns": YAML記載の_{load}.map.*:の値`, `"where_clause": YAML記載の_{load}.where:の値`
    - 想定対象ストア: SQLデータベース
    - _load.client: のみに使用対応
  4. **EnvClient**
    - 必要なメソッド: `get()`/`set()`/`delete()`
    - 渡される引数: `"key": Manifestの_{load}.map.*:の値`
    - 想定対象ストア: 環境変数
    - _load.client: のみに使用対応
  5. **HttpClient**
    - 必要なメソッド: `get()`/`set()`/`delete()`
    - traitシグネチャ:
      - `fn get(&self, url: &str, headers: Option<&HashMap<String, String>>) -> Option<Value>`
      - `fn set(&self, url: &str, body: Value, headers: Option<&HashMap<String, String>>) -> bool`
      - `fn delete(&self, url: &str, headers: Option<&HashMap<String, String>>) -> bool`
    - 渡される引数: `"url": YAML記載の_{store,load}.url:の値`, `"headers": YAML記載の_{store,load}.headers:の値`
    - 想定対象ストア: HTTPエンドポイント
    - _store/_load両方に使用対応
  6. **FileClient**
    - 必要なメソッド: `get()`/`set()`/`delete()`
    - traitシグネチャ:
      - `fn get(&self, key: &str) -> Option<String>`
      - `fn set(&self, key: &str, value: String) -> bool`
      - `fn delete(&self, key: &str) -> bool`
    - 渡される引数: `"key": Manifestの_{store,load}.key:の値`
    - 想定対象ストア: ファイルI/O
    - デフォルト実装 `DefaultFileClient` を内蔵(std::fsベース)
    - _store/_load両方に使用対応
    - **StateがmanifestのYAML読み込みに常時使用する**

## State

### State::get("filename.node")

指定されたノードが表すステート(state obj)を参照し、値またはcollectionを返却する。

戻り値: `Result<Option<Value>, StateError>`

**動作フロー:**
1. `called_keys` チェック(再帰・上限検出)
2. `FileClient`経由でmanifestファイルをロード(未ロード時のみ)
3. `core::Manifest::find()` → key_idx 取得
4. **state_values (インスタンスキャッシュ) をチェック** ← 最優先
5. `core::Manifest::get_meta()` → MetaIndices 取得
6. `_load.client == State` の場合はストアをスキップ。それ以外: ストア (KVS/InMemoryClient) から取得
7. **miss時、`Load::handle()` で自動ロード**
8. `Ok(Some(value))` / `Ok(None)` / `Err(StateError)` を返却

**自動ロード:**
- 指定されたノードのステートキーがmissした場合、`Load::handle()` で自動取得を試みる
- `Load::handle()` がエラーの場合、`Err(StateError::LoadFailed(LoadError))` を返す

**_state.typeについての注意:**
```yaml
tenant_id:
  _state:
    type: integer  # メタデータのみ - 検証/キャストは未実装
```

`_state.type`フィールドは現在メタデータのみで、State操作では強制されません。

---

### State::set("filename.node", value, ttl)

指定されたノードが表すステートに値をセットする。

戻り値: `Result<bool, StateError>`

**動作:**
- 永続ストア (KVS/InMemoryClient) に保存
- state_values (インスタンスキャッシュ) にも保存
- ストアがKVSの場合、TTLを設定可能

**TTL動作:**
- `ttl` 引数が指定された → 指定値を使用
- `ttl` 引数なし、YAMLに `_store.ttl` あり → YAMLのデフォルト値を使用
- `ttl` 引数なし、YAMLに `_store.ttl` なし → 現在の値を維持

---

### State::delete("filename.node")

指定されたノードが表す {key:value} レコードを削除する。

戻り値: `Result<bool, StateError>`

**動作:**
- 永続ストア (KVS/InMemoryClient) から削除
- state_values (インスタンスキャッシュ) からも削除
- 削除後、そのノードは miss を示す

---

### State::exists("filename.node")

自動ロードをトリガーせずに、キーの存在確認を行う。

戻り値: `Result<bool, StateError>`

**動作:**
- 最初に state_values (インスタンスキャッシュ) をチェック
- 次に永続ストア (KVS/InMemoryClient) をチェック
- **自動ロードをトリガーしない** (`get()` とは異なる)
- 存在する場合 `Ok(true)`、それ以外 `Ok(false)` を返す

**get() との比較:**
- `get()`: 値を返す、miss時に自動ロードをトリガー
- `exists()`: 真偽値を返す、自動ロードは決してトリガーしない

---

## Load::handle()

`State::get()` が値をmissした際、`_load` 設定に従ってデータを取得する。

**クライアント種別:**
- `Env` - 環境変数からロード
- `Db` - データベースからロード
- `KVS` - KVSからロード
- `InMemory` - プロセスメモリからロード
- `Http` - HTTPエンドポイントからロード
- `File` - ファイルからロード
- `State` - 別のStateキーを参照(Load::handle()を呼ばない)

**State clientの特殊動作:**
```yaml
tenant_id:
  _load:
    client: State
    key: ${org_id}  # State::get("cache.user.org_id")を直接返す
```

`_load.client: State` の場合、`Load::handle()` は呼ばれず、`_load.key` の値(プレースホルダー解決済み)が直接返される。

**設計ルール:**
- `_load` なし → 自動ロードなし、`Ok(None)` を返す
- `_load.client` なし → 自動ロードなし、`Ok(None)` を返す
- `_load.client: State``_load.key` の値を直接使用(Load::handle()を呼ばない)
- その他のclient → `Load::handle()` で自動ロード

**再帰深度制限:**
- `max_recursion = 20`
- `called_keys: HashSet<String>` で処理中のキーを管理
- 上限超過または同一キーの再帰検出時に `Err(StateError::RecursionLimitExceeded)` を返す

---

## State::get() 詳細フロー

```
1. called_keys チェック(再帰・上限検出)
   2. FileClient経由でmanifestファイルをロード(未ロード時のみ)
   3. core::Manifest::find() → key_idx 取得
   4. ★ state_values をチェック (key_idx) ← 最優先
   if find_state_value(key_idx).is_some() { return Ok(Some(value)); }
   5. core::Manifest::get_meta() → MetaIndices 取得
   6. _load.client == State の場合はストアをスキップ
   それ以外: ストア (KVS/InMemoryClient) から取得
   ↓
7. miss時、自動ロード
   ├─→ build_config() でプレースホルダーを解決
   ├─→ Load::handle(config)
   │    ├─→ client: Db → DbClient::get()
   │    ├─→ client: KVS → KVSClient::get()
   │    ├─→ client: Env → EnvClient::get()
   │    ├─→ client: InMemory → InMemoryClient::get()
   │    ├─→ client: Http → HttpClient::get()
   │    └─→ client: File → FileClient::get()
   ├─→ 永続ストアに保存
   └─→ state_values に保存
   8. Ok(Some(value)) / Ok(None) / Err(StateError) を返却
```

---

## state_values (インスタンスメモリキャッシュ)

State構造体は、永続ストア(KVS/InMemoryClient)とは別に、インスタンスレベルのキャッシュ(`state_values: StateValueList`)を保持します。

**重要:** これはInMemoryClientではありません。Stateインスタンス自体の変数です。

**目的:**
1. **同一リクエスト内での重複`State::get()`呼び出しを高速化**
2. **KVS/InMemoryClientへのアクセス回数を削減**
3. **重複ロードを回避する設計**(同じキーを複数回ロードしない)

**インデックス:**
- `key_idx: u16` — KeyList上のグローバルユニークなindex をキーとして保存
- 永続ストアのキー文字列ではなく、key_idxで引く設計

**保存タイミング:**
- `State::get()`でストアまたはロードから取得成功時
- `State::set()`時

**削除タイミング:**
- `State::delete()`時

**ライフサイクル:**
- Stateインスタンス生成: 空
- State稼働中: 蓄積
- Stateインスタンス破棄: 破棄(メモリ解放)

---

## プレースホルダー解決ルール

`${}` 内のパスは **parse時に qualified path へ変換済み**。State実行時に変換処理は行わない。

**parse時の qualify ルール(`qualify_path()`):**
- パスに `.` を含む場合 → 絶対パスとみなしそのまま使用
- `.` を含まない場合 → `filename.ancestors.path` に変換

**例(`cache.yml` の `user._load.where` 内 `${tenant_id}`):**
```
qualify_path("tenant_id", "cache", ["user"])
→ "cache.user.tenant_id"
```

**State実行時のプレースホルダー解決(`resolve_value_to_string()`):**
- path_map から qualified path を取り出し
- `State::get(qualified_path)` を呼んで値を取得

---

## error case

**ManifestError:**
- `FileNotFound` — manifestディレクトリにファイルが見つからない
- `AmbiguousFile` — manifestDir内に拡張子違いの同名ファイルが2つ存在する(`.yml`と`.yaml`)。ドット区切りを階層表現とするため拡張子を無視し、区別できない。同拡張子の同名ファイルはOSレベルでの非許容を想定。
- `ParseError` — YAMLのパース失敗

**LoadError:**
- `ClientNotConfigured` — 必要なclient(Env/KVS/DB/HTTP/File)がStateに未設定
- `ConfigMissing(String)` — manifest内に必須のconfigキー(key/url/table/map/connection)が欠落
- `NotFound(String)` — clientの呼び出しは成功したがデータが存在しなかった
- `ParseError(String)` — clientレスポンスのJSONパースエラー

**StoreError:**
- `ClientNotConfigured` — 必要なclient(KVS/InMemory/HTTP/File)がStateに未設定
- `ConfigMissing(String)` — manifest内に必須のconfigキー(key/url/client)が欠落
- `SerializeError(String)` — JSONシリアライズエラー
- `UnsupportedClient(u64)` — configに未対応のclient idが指定された