mandolin 0.4.7

Input openapi.json/yaml, output server source code in rust.
Documentation
# `$ref` 事前解決の廃止による正規スキーマ名の利用

## 問題

現在の `resolve.rs` はすべての `$ref` をインライン展開してからテンプレートに渡す。
その結果、`#/components/schemas/Point` を 100 箇所から参照しても、
テンプレートが見るスキーマはすべて `$ref` キーを持たない `{"type":"object",...}` になる。

`SCHEMA` マクロはこれを `type == "object"` ブランチで処理し、
`SCHEMA_NAME(現在のポインタ)` を呼ぶ。ポインタはそれぞれ異なる
(例: `#/paths/foo/get/.../schema`、`#/paths/bar/post/.../schema`)ため、
`schema_push` キャッシュはすべてを別エントリと見なし、100 個の struct が生成される。
名前は `PathsFooGetResponsesSchema` のようなパス由来のものになり、
`struct Point` という名前は生成されない。

```
openapi.json の Point への $ref × 100
  → resolve.rs がすべてインライン展開
  → テンプレートが 100 種類のポインタで SCHEMA_NAME を呼ぶ
  → 100 個の異なる struct(内容は同じ、名前が違う)
```

## 提案

`#/components/schemas/` 配下を指す `$ref` は解決しない。
テンプレートに `{"$ref":"#/components/schemas/Point"}` がそのまま届くようにする。

`SCHEMA` マクロの `$ref` ブランチが循環参照のフォールバックから**主経路**になり、
`SCHEMA_NAME("#/components/schemas/Point")` が呼ばれる。
これにより `struct Point` が 1 回だけ生成され、
参照箇所では `x: Point` という型名が使われる。

```
openapi.json の Point への $ref × 100
  → resolve.rs がスキーマ $ref をスキップ
  → テンプレートが 100 回とも SCHEMA_NAME("#/components/schemas/Point") を呼ぶ
  → schema_push がキャッシュ済みと判断し 2 回目以降は IDENTIFIED_SCHEMA をスキップ
  → struct Point が 1 個、参照側は x: Point
```

---

## 変更 1: `resolve.rs` — スキーマ $ref のスキップ

`resolve_recursive` に 1 条件を追加する。

```rust
fn resolve_recursive(node: &mut Value, root: &Value, visiting: &mut HashSet<String>) {
    match node {
        Value::Object(map) => {
            if let Some(Value::String(ref_path)) = map.get("$ref") {
                let ref_path = ref_path.clone();
                // ↓ 追加: スキーマ定義への $ref はインライン展開しない
                if ref_path.starts_with("#/components/schemas/") {
                    return;
                }
                if let Some(resolved) = lookup_pointer(root, &ref_path) {
                    if !visiting.contains(&ref_path) {
                        visiting.insert(ref_path.clone());
                        let mut resolved = resolved.clone();
                        resolve_recursive(&mut resolved, root, visiting);
                        visiting.remove(&ref_path);
                        *node = resolved;
                        return;
                    }
                }
            }
            for value in map.values_mut() {
                resolve_recursive(value, root, visiting);
            }
        }
        Value::Array(arr) => {
            for item in arr.iter_mut() {
                resolve_recursive(item, root, visiting);
            }
        }
        _ => {}
    }
}
```

パラメータ・レスポンスオブジェクト・requestBody への `$ref`
(`#/components/parameters/`、`#/components/responses/` 等)は引き続き解決される。
テンプレートが `parameter.name` や `response.content` を直接参照できる状態を保つため。

---

## 変更 2: `rust_axum.template``SCHEMA` マクロの分岐順序

現在 `schema["$ref"]` は `elif`(循環参照フォールバック)だが、これを先頭の `if` に昇格させる。

**変更前:**
```jinja
{%- macro SCHEMA(pointer,schema) -%}
{%- if schema.type -%}
  ...
{%- elif schema["$ref"] %}{{SCHEMA_NAME(schema["$ref"])}}
{%- else %}u8
{%- endif %}
{%- endmacro -%}
```

**変更後:**
```jinja
{%- macro SCHEMA(pointer,schema) -%}
{%- if schema["$ref"] %}{{SCHEMA_NAME(schema["$ref"])}}
{%- elif schema.type -%}
  ...
{%- else %}u8
{%- endif %}
{%- endmacro -%}
```

`$ref` が届いた時点で `SCHEMA_NAME(ref_path)` を呼び、
`ref_path` がキャッシュになければ `IDENTIFIED_SCHEMA` でその名前の struct を生成する。
2 回目以降は `schema_push` がキャッシュ済みと判断して生成をスキップし、型名だけ返す。

---

## Before / After

### OpenAPI

```json
"components": {
  "schemas": {
    "Point": {
      "type": "object",
      "properties": {
        "x": {"type": "integer", "format": "int32"},
        "y": {"type": "integer", "format": "int32"},
        "z": {"type": "integer", "format": "int32"}
      }
    }
  }
},
"paths": {
  "/a": {"get": {"responses": {"200": {"content": {"application/json":
    {"schema": {"$ref": "#/components/schemas/Point"}}}}}}},
  "/b": {"post": {"requestBody": {"content": {"application/json":
    {"schema": {"$ref": "#/components/schemas/Point"}}}}}}
}
```

### 変更前(現在)

```rust
#[derive(Default,Clone,Debug,serde::Serialize,serde::Deserialize)]
pub struct PathsAGetResponses200ContentApplicationJsonSchema {
    pub r#x: Option<i32>,
    pub r#y: Option<i32>,
    pub r#z: Option<i32>,
}

#[derive(Default,Clone,Debug,serde::Serialize,serde::Deserialize)]
pub struct PathsBPostRequestBodyContentApplicationJsonSchema {
    pub r#x: Option<i32>,
    pub r#y: Option<i32>,
    pub r#z: Option<i32>,
}
// struct Point は生成されない
```

### 変更後(提案)

```rust
#[derive(Default,Clone,Debug,serde::Serialize,serde::Deserialize)]
pub struct Point {
    pub r#x: Option<i32>,
    pub r#y: Option<i32>,
    pub r#z: Option<i32>,
}
// GET /a のレスポンスも POST /b のボディも Point を参照するだけ
```

---

## 波及範囲

### `resolve.rs` のテスト

`test_resolve_simple_ref` は `$ref` が解決されインライン化されることを確認している。
`#/components/schemas/` への `$ref` はスキップされるようになるため、このテストを修正するか
対象を `#/components/parameters/` など非スキーマ `$ref` に変える。

### `test_circular_ref_preserved` は影響なし

循環参照は `#/components/schemas/` 配下なので、今後は「スキップ」によって
`$ref` が残る(以前は循環検出によって残っていた)。結果は同じ。

### `SCHEMA` マクロの `discriminator` 対応(`20260219-lambda360.md` 変更 3)

lambda360 ドキュメントでは `SCHEMA` の先頭に `discriminator` チェックを追加する案があった。
今回の変更で `$ref` を先頭に置くため、追加順は以下にする:

```jinja
{%- macro SCHEMA(pointer,schema) -%}
{%- if schema["$ref"] %}{{SCHEMA_NAME(schema["$ref"])}}
{%- elif schema.discriminator %}{{SCHEMA_NAME(pointer)}}
{%- elif schema.type -%}
  ...
```

### `allOf` の単一 `$ref` パターン

```json
"deg": {"allOf": [{"$ref": "#/components/schemas/NumberOrExpr"}], "description": "..."}
```

`resolve.rs` がスキップした結果、このフィールドのスキーマは
`{"allOf":[{"$ref":"#/components/schemas/NumberOrExpr"}]}` のまま届く。
`SCHEMA` マクロが `schema.allOf` ブランチ(lambda360 ドキュメント 変更 1)で
`schema.allOf[0]` を再帰処理すると `schema["$ref"]` が見つかり `NumberOrExpr` が返る。
事前解決があった場合と同じ結果になり、追加対応は不要。