# 2段階生成による `resolve.rs` 完全削除
## GCC との対比
GCC は `.c` を `.o` にコンパイルしてから `.o` 同士をリンクする。
各 `.o` は他のシンボルの**中身を知らなくても**名前だけで参照できる。
mandolin も同じ構造が取れる:
| Phase 1: `.c` → `.o` (シンボル定義) | Phase 1: `components/schemas` → Rust 型定義 |
| Phase 2: リンク (シンボル参照を解決) | Phase 2: `paths` → オペレーション (型名を参照) |
| `extern void printf(...)` | `$ref: #/components/schemas/Point` |
| リンカが `printf` の実体を見つける | テンプレートが `"Point"` という名前を使う |
現在の `resolve.rs` は「`printf` の実装コードを呼び出し元に全部コピーペーストする」
ことに相当しており、そもそも不要な処理だった。
---
## 現在のフロー vs 提案
```
【現在】
openapi.json
→ resolve.rs ($refを全部インライン展開)
→ テンプレート (インライン済みJSONを走査)
→ struct PathsFooGetSchema, struct PathsBarPostSchema ... ← 同内容が増殖
【提案】
openapi.json
→ lib.rs (serde_json::to_value そのまま、$ref展開なし)
→ テンプレート Phase 1: components/schemas を走査 → struct Point, struct Line ...
→ テンプレート Phase 2: paths を走査 → 型名として "Point" を参照するだけ
→ struct Point が1個、参照箇所は x: Point
```
---
## 変更 1: `resolve.rs` を削除
`resolve.rs` の役割は 2 つだった:
1. `$ref` のインライン展開 → **Phase 1 で不要になる**
2. デフォルトサーバの補完 → **`lib.rs` に移動できる(数行)**
`lib.rs` の変更:
```rust
pub fn environment(mut spec: OpenAPI) -> Result<minijinja::Environment<'static>, minijinja::Error> {
// デフォルトサーバ補完 (resolve.rs から移動)
if spec.servers.is_empty() {
spec.servers.push(openapiv3::Server {
url: "/api".to_string(),
..Default::default()
});
}
// $ref展開なし、そのままシリアライズ
let value = serde_json::to_value(&spec).unwrap();
let mut env = minijinja::Environment::new();
// ... フィルタ・キャッシュ登録 (変更なし)
env.add_global("spec", minijinja::Value::from_serialize(&value));
Ok(env)
}
```
`resolve` モジュールの `use`、`mod resolve`、`ResolvedOpenAPI` がすべて消える。
---
## 変更 2: テンプレートを 2 段階に分割
### Phase 1: スキーマのコンパイル
`spec.components.schemas` を明示的に走査し、すべての名前付き型を先に出力する。
```jinja
{# ===== Phase 1: スキーマ定義 ===== #}
{%- if spec.components and spec.components.schemas %}
{%- for schema_name, schema in spec.components.schemas|items %}
{{IDENTIFIED_SCHEMA("#/components/schemas/"+schema_name, schema)}}
{%- endfor %}
{%- endif %}
{{ IDENTIFIED_SCHEMA_DRAIN() }} {# Phase 1 で発見されたインラインスキーマ #}
{# ===== Phase 2: オペレーション定義 ===== #}
...
{%- endfor %}
{%- endfor %}
{{ IDENTIFIED_SCHEMA_DRAIN() }} {# Phase 2 で発見されたインラインスキーマ #}
```
Rust は前方宣言不要なので出力順序は問わない。
ただし Phase 1 を先に出すことで可読性が上がる。
### `SCHEMA` マクロの簡略化
`$ref` を受け取ったら中身を見ずに型名だけ取り出す。
`include_pointer` も `IDENTIFIED_SCHEMA` も呼ばない。
```jinja
{%- macro SCHEMA(pointer, schema) -%}
{%- elif schema.type == "object" %}
{%- if schema.additionalProperties %}HashMap<String,{{SCHEMA(pointer+"/additionalProperties",schema.additionalProperties)}}>
{%- else %}{{SCHEMA_NAME(pointer)}}
{%- endif %}
{%- elif schema.type == "integer" %}i32
{%- elif schema.type == "string" %}String
{%- elif schema.type == "number" %}f64
{%- elif schema.type == "boolean" %}bool
{%- else %}u8
{%- endif %}
{%- endmacro -%}
```
`schema["$ref"]` が `"#/components/schemas/Point"` なら `ref_name` フィルタが `"Point"` を返す。
Phase 1 でその struct はすでに生成済みなので追加処理不要。
### `ref_name` フィルタ(Rust 側で追加)
```rust
// filter.rs に追加(数行)
pub fn ref_name(ref_path: &str) -> String {
ref_path
.rsplit('/')
.next()
.unwrap_or(ref_path)
.to_pascal_case()
}
```
`"#/components/schemas/my_point"` → `"MyPoint"`
---
## 変更 3: パラメータ等の `$ref` 対応
`openapiv3` の `operation.parameters` は `Vec<ReferenceOr<Parameter>>` であり、
`$ref` で書かれたパラメータは `{"$ref":"#/components/parameters/PetId"}` のまま届く。
テンプレートが `parameter.name` を参照すると未定義になる。
解決策: `deref` フィルタを Rust 側に追加する。
```rust
// lib.rs 内、include_pointer と同様にクロージャでキャプチャ
let spec_for_deref = value.clone();
env.add_filter(
"deref",
move |v: minijinja::Value| -> Result<minijinja::Value, minijinja::Error> {
if let Ok(ref_path) = v.get_attr("$ref") {
if let Some(s) = ref_path.as_str() {
let path = s.strip_prefix("#/").unwrap_or(s);
let mut cur = &spec_for_deref;
for seg in path.split('/') {
let decoded = seg.replace("~1", "/").replace("~0", "~");
cur = cur.get(&decoded).ok_or_else(|| {
minijinja::Error::new(minijinja::ErrorKind::InvalidOperation,
format!("deref: not found: {s}"))
})?;
}
return Ok(minijinja::Value::from_serialize(cur));
}
}
Ok(v)
},
);
```
テンプレートでは:
```jinja
{%- for param_or_ref in operation.parameters %}
{%- endfor %}
```
`$ref` でも直接記述でも同じ書き方で処理できる。
これは旧アーキテクチャの `include_ref` フィルタと同等だが、
`include_pointer` と統合して 1 フィルタで済む。
---
## `SCHEMA_NAME` / `schema_push` の扱い
Phase 1 で生成した名前付きスキーマは `$ref` 経由では `ref_name` フィルタで処理される。
`SCHEMA_NAME` → `schema_push` → `IDENTIFIED_SCHEMA` の経路は
**インライン匿名スキーマ専用**になる(例: パスに直接書かれた object)。
キャッシュの役割が明確化される:
| `components/schemas/*` | Phase 1 の明示ループ | 直接 `IDENTIFIED_SCHEMA` を呼ぶ |
| パス内インライン匿名スキーマ | Phase 2 の `SCHEMA_NAME` 呼び出し時 | `schema_push` / `schema_drain` |
`schema_push` は匿名スキーマの重複排除のみに使われ、
名前付きスキーマに対して呼ばれることはなくなる。
---
## 削除・追加されるもの
| `resolve.rs` | **削除** |
| `lib.rs` | デフォルトサーバ補完を吸収、`resolve::` 参照を除去 |
| `filter.rs` | `ref_name` フィルタを追加(数行)、`deref` フィルタを追加 |
| `rust_axum.template` | Phase 1 ループを先頭に追加、`SCHEMA` の `$ref` 分岐を `ref_name` に変更 |
| `schema_cache.rs` | 変更なし(匿名スキーマの重複排除に引き続き必要) |
---
## `20260219-schema-ref.md` との比較
前回の提案(スキーマ `$ref` だけスキップ)と今回の比較:
| `resolve.rs` の変更 | 1行追加(スキップ条件) | **完全削除** |
| `$ref` の処理 | `SCHEMA_NAME` 経由(`include_pointer` が必要) | `ref_name` フィルタのみ(参照解決不要) |
| `schema_push` の用途 | 名前付き + 匿名の両方 | **匿名のみ**(責務が明確) |
| パラメータ `$ref` | 引き続き `resolve.rs` が解決 | `deref` フィルタで対応 |
| 複雑度 | `resolve.rs` の部分的な変更にとどまる | 設計が根本的にシンプルになる |
本提案の方が変更量は多いが、「`$ref` はコンパイル済みシンボルへの参照」という
一貫した概念モデルを持ち、特殊ケース(循環参照検出、スキップ条件)が消える。