mandolin 0.4.7

Input openapi.json/yaml, output server source code in rust.
Documentation
# openapi_lambda360.json の正確な Rust コード生成に向けた分析と解決策

## 概要

`openapi/openapi_lambda360.json` を mandolin で処理すると、現在は `anyOf` / `allOf` / `discriminator` に対応していないため、生成された Rust コードが型安全でなく実用に耐えない状態になる。本ドキュメントでは問題を分類し、それぞれの解決策をテンプレート側の変更として提案する。

---

## 問題の分類

### パターン 1: `anyOf` → 現状 `u8` にフォールバック

**OpenAPI:**
```json
"NumberOrExpr": {
  "anyOf": [
    { "type": "number", "format": "double" },
    { "type": "string" }
  ]
}
```

**現在の生成コード:**
```rust
// SCHEMA マクロが type も $ref も見つけられず else ブランチの u8 になる
pub type NumberOrExpr = u8;  // 実際にはこうはならず、フィールド型として u8 が埋め込まれる
```

**問題の根本:**
`SCHEMA` マクロは `schema.type` の有無を最初にチェックする。`anyOf` スキーマには `type` がないため `schema["$ref"]` チェックに進み、それもないので最後の `else → u8` に落ちる(`templates/rust_axum.template:44-45`)。

**期待するコード:**
```rust
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum NumberOrExpr {
    Number(f64),
    Expr(String),
}
```

---

### パターン 2: 単一要素 `allOf` でフィールド型を指定 → `u8`

**OpenAPI (`RotateNode.deg`):**
```json
"deg": {
  "allOf": [{ "$ref": "#/components/schemas/NumberOrExpr" }],
  "description": "回転角度 (度)"
}
```

これは `$ref` に `description` を付けるための OpenAPI の慣用パターン(`$ref` と兄弟フィールドは仕様上共存不可のため)。

**現在の生成コード:**
```rust
pub r#deg: u8,   // NumberOrExpr であるべき
```

**問題の根本:**
`$ref` の事前解決により、このフィールドのスキーマは以下のようにインライン展開される:
```json
{
  "allOf": [{ "anyOf": [...], "description": "..." }],
  "description": "回転角度 (度)"
}
```
トップレベルに `type` も `$ref` もないため `u8` になる。

**期待するコード:**
```rust
pub r#deg: NumberOrExpr,
```

---

### パターン 3: `discriminator` による多態型 → 単純 struct になる

**OpenAPI:**
```json
"ShapeNode": {
  "type": "object",
  "properties": { "op": { "type": "string" } },
  "discriminator": {
    "propertyName": "op",
    "mapping": {
      "step":      "#/components/schemas/StepNode",
      "union":     "#/components/schemas/UnionShapeNode",
      "intersect": "#/components/schemas/IntersectNode",
      "subtract":  "#/components/schemas/SubtractNode",
      "scale":     "#/components/schemas/ScaleNode",
      "translate": "#/components/schemas/TranslateNode",
      "rotate":    "#/components/schemas/RotateNode",
      "stretch":   "#/components/schemas/StretchNode"
    }
  }
}
```

**現在の生成コード:**
```rust
#[derive(Default, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct PathsShapePostRequestBodyContentApplicationJsonSchema {
    pub r#op: String,
}
// discriminator の mapping は完全に無視される
```

**期待するコード:**
```rust
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(tag = "op")]
pub enum ShapeNode {
    #[serde(rename = "step")]      Step(StepNode),
    #[serde(rename = "union")]     Union(UnionShapeNode),
    #[serde(rename = "intersect")] Intersect(IntersectNode),
    #[serde(rename = "subtract")]  Subtract(SubtractNode),
    #[serde(rename = "scale")]     Scale(ScaleNode),
    #[serde(rename = "translate")] Translate(TranslateNode),
    #[serde(rename = "rotate")]    Rotate(RotateNode),
    #[serde(rename = "stretch")]   Stretch(StretchNode),
}
```

---

### パターン 4: `allOf` による継承 → 親フィールドが欠落

**OpenAPI (`StepNode`):**
```json
"StepNode": {
  "type": "object",
  "required": ["op", "path"],
  "properties": {
    "op": { "type": "string", "enum": ["step"] },
    "path": { "type": "string" },
    "content_hash": { "type": "string" }
  },
  "allOf": [{ "$ref": "#/components/schemas/ShapeNode" }]
}
```

**現在の生成コード:**
```rust
#[derive(Default, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct StepNode {
    pub r#op: String,
    pub r#path: String,
    pub r#content_hash: Option<String>,
}
// allOf の親スキーマ (ShapeNode の properties) はマージされない
```

lambda360 では子スキーマが親フィールド `op` を再宣言しているため表面上は問題ないが、
一般的には親専用フィールドが欠落する。また discriminator との組み合わせで意味が変わる。

**期待するコード:**
`#[serde(tag = "op")]` を使う場合、`op` はタグとして扱われるため struct から除外するか、`#[serde(skip)]` を付ける。discriminator 版 ShapeNode enum が生成されるなら、各バリアント struct から `op` を除外するのが serde の慣習。ただし実用上は `op` を残したまま `#[serde(untagged)]` にする設計もある(後述)。

---

## 解決策: テンプレートの変更

mandolin の設計思想「ロジックはテンプレートに」に従い、すべての修正を `templates/rust_axum.template` に加える。

### 変更 1: `SCHEMA` マクロへの `anyOf` / `allOf` 対応追加

現在のコード(`rust_axum.template:24-47`):
```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.type -%}
  ...(既存のまま)...
{%- elif schema["$ref"] %}{{SCHEMA_NAME(schema["$ref"])}}
{%- elif schema.anyOf %}{{SCHEMA_NAME(pointer)}}
{%- elif schema.allOf %}
  {%- if schema.allOf|length == 1 %}{{SCHEMA(pointer+"/allOf/0", schema.allOf[0])}}
  {%- else %}{{SCHEMA_NAME(pointer)}}
  {%- endif %}
{%- else %}u8
{%- endif %}
{%- endmacro -%}
```

ポイント:
- `anyOf``SCHEMA_NAME` を呼び出し、後述の `IDENTIFIED_SCHEMA` で enum を生成させる
- 単一要素 `allOf` → 内側の要素の型として再帰処理(`$ref` の description 付きパターンに対応)
- 複数要素 `allOf``SCHEMA_NAME` 経由で合成型として生成(継承マージ)

---

### 変更 2: `IDENTIFIED_SCHEMA` マクロへの分岐追加

現在(`rust_axum.template:51-64`)は `type == "object"` か型エイリアスの2択。以下の分岐を先頭に追加する:

```jinja
{%- macro IDENTIFIED_SCHEMA(pointer, schema) %}
{%- if schema.discriminator %}
{# ── パターン 3: discriminator → #[serde(tag)] enum ── #}
#[derive(Clone,Debug,serde::Serialize,serde::Deserialize)]
#[serde(tag = "{{schema.discriminator.propertyName}}")]
pub enum {{SCHEMA_NAME(pointer)}}{
{%- for op_value, ref_path in schema.discriminator.mapping|items %}
    #[serde(rename = "{{op_value}}")]
    {{op_value|to_pascal_case}}({{SCHEMA_NAME(ref_path)}}),
{%- endfor %}
}
{%- elif schema.anyOf %}
{# ── パターン 1: anyOf → #[serde(untagged)] enum ── #}
#[derive(Clone,Debug,serde::Serialize,serde::Deserialize)]
#[serde(untagged)]
pub enum {{SCHEMA_NAME(pointer)}}{
{%- for variant in schema.anyOf %}
    Variant{{loop.index}}({{SCHEMA(pointer+"/anyOf/"+loop.index0|string, variant)}}),
{%- endfor %}
}
{%- elif schema.type == "object" %}
{# ── 既存: struct 生成(allOf の親フィールドをマージ) ── #}
#[derive(Default,Clone,Debug,serde::Serialize,serde::Deserialize)]
pub struct {{SCHEMA_NAME(pointer)}}{{'{'}}
{%- if schema.allOf %}
{%- for parent in schema.allOf %}
{%- for property_key, property in parent.properties|default({})|items %}
{%- if not (schema.properties and property_key in schema.properties) %}
{%- with inner=SCHEMA(pointer+"/allOf/"+loop.index0|string+"/properties/"+property_key, property) %}
    pub r#{{property_key}}:{%- if parent.required and property_key in parent.required %}{{inner}}{%- else %}Option<{{inner}}>{%- endif %},
{%- endwith %}
{%- endif %}
{%- endfor %}
{%- endfor %}
{%- endif %}
{%- for property_key, property in schema.properties|default({})|items %}
{%- with inner=SCHEMA(pointer+"/properties/"+property_key, property) %}
    pub r#{{property_key}}:{%- if schema.required and property_key in schema.required %}{{inner}}{%- else %}Option<{{inner}}>{%- endif %},
{%- endwith %}
{%- endfor %}
}
{%- else %}
pub type {{SCHEMA_NAME(pointer)}}={{SCHEMA(pointer, schema)}};
{%- endif %}
{%- endmacro %}
```

---

### 変更 3: `ShapeNode` を参照するフィールドへの対応

`$ref` → `ShapeNode` は事前解決により discriminator を含む完全なスキーマにインライン展開される。そのまま `SCHEMA` に渡すと `type == "object"` ブランチに入るため、`discriminator` チェックを `type` より先に行う必要がある。

`SCHEMA` マクロの先頭にも追加:
```jinja
{%- macro SCHEMA(pointer,schema) -%}
{%- if schema.discriminator %}{{SCHEMA_NAME(pointer)}}
{%- elif schema.type -%}
  ...
```

---

## 注意点

### `#[serde(tag)]``Default` の非互換

`#[serde(tag = "op")]` を付けた enum は `Default` を自動導出できない。
`OPERATION_RESPONSE` マクロや `Default::default()` を使っている箇所が影響を受ける。

対処案:
- discriminator enum には `Default` を導出しない
- `impl Default for ShapeNode` を手動で生成(最初のバリアントをデフォルトにする)

```jinja
impl Default for {{SCHEMA_NAME(pointer)}}{
    fn default() -> Self {
        // 最初のバリアントをデフォルトとする
        {%- for op_value, ref_path in schema.discriminator.mapping|items %}
        Self::{{op_value|to_pascal_case}}(Default::default())
        {%- break %}
        {%- endfor %}
    }
}
```

ただし各バリアント型(StepNode等)自身が `Default` を実装している必要がある。

### `$ref` 事前解決と pointer の一貫性

discriminator の `mapping` に含まれる `ref_path`(例: `"#/components/schemas/StepNode"`)は事前解決後も元の `$ref` パスとして保持されているわけではなく、テンプレートに渡されるのはインライン展開後の値のみ。

→ `discriminator.mapping` の値はそのまま `SCHEMA_NAME` に渡せる JSON Pointer 形式なので問題ない。`schema_push` キャッシュはポインタをキーとして使うため、`#/components/schemas/StepNode` を `SCHEMA_NAME` に渡せば正しく名前解決される。

### 循環参照

`ShapeNode` → `StepNode` → `ShapeNode`(のchildren)という循環は `resolve.rs` の `$ref` 事前解決でブロックされ、`$ref` マーカーが残る。そのため無限展開は起きないが、テンプレート内で `$ref` が残ったままのスキーマを処理する際は `schema["$ref"]` チェックが必要(現在も対応済み)。

---

## 実装優先度

| 優先度 | 問題 | 影響範囲 |
|--------|------|---------|
|| `anyOf` → enum 生成(パターン 1) | `NumberOrExpr` 型が完全に壊れる |
|| 単一要素 `allOf` → 型の透過(パターン 2) |`*Node.deg/axis/xyz` フィールドが `u8` になる |
|| `discriminator` → tagged enum(パターン 3) | `ShapeNode` の多態デシリアライズが不可能 |
|| `allOf` 継承マージ(パターン 4) | lambda360 では子が親フィールドを再宣言しているため表面上は動くが、一般性に欠ける |

---

## 参考: 正しく生成されるべきコード(抜粋)

```rust
// NumberOrExpr: anyOf → untagged enum
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum NumberOrExpr {
    Variant1(f64),
    Variant2(String),
}

// ShapeNode: discriminator → tagged enum
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(tag = "op")]
pub enum ShapeNode {
    #[serde(rename = "step")]      Step(StepNode),
    #[serde(rename = "union")]     Union(UnionShapeNode),
    #[serde(rename = "intersect")] Intersect(IntersectNode),
    #[serde(rename = "subtract")]  Subtract(SubtractNode),
    #[serde(rename = "scale")]     Scale(ScaleNode),
    #[serde(rename = "translate")] Translate(TranslateNode),
    #[serde(rename = "rotate")]    Rotate(RotateNode),
    #[serde(rename = "stretch")]   Stretch(StretchNode),
}

// StepNode: allOf の親フィールドを自身の properties でカバー済み
#[derive(Default, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct StepNode {
    pub r#op: String,               // enum とも合わせて冗長だが明示的
    pub r#path: String,
    pub r#content_hash: Option<String>,
}

// RotateNode: allOf フィールドが正しく型解決される
#[derive(Default, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct RotateNode {
    pub r#op: String,
    pub r#shape: Box<ShapeNode>,    // 再帰型は Box が必要
    pub r#axis: Vec<NumberOrExpr>,
    pub r#deg: NumberOrExpr,        // 単一要素 allOf が正しく解決される
}

// ShapeComputeRequest: body が正しく ShapeNode になる
#[derive(Debug)]
pub struct ShapeComputeRequest {
    pub body: ShapeNode,
    pub request: axum::http::Request<axum::body::Body>,
}
```

### 再帰型 (`Box`) について

`ShapeNode` が `RotateNode.shape` フィールドの型として使われ、`RotateNode` が `ShapeNode` のバリアントになる場合、Rust のサイズ計算が循環して無限になるため `Box<ShapeNode>` が必要になる。
テンプレートでこれを自動検出するには循環グラフの検出が必要で難易度が高い。
現実的な対処として、`ShapeNode` のような再帰的な discriminator 型のバリアント内フィールドは `Box<T>` を生成するか、`$ref` が残っている(事前解決が止まった)ことをシグナルにして `Box` を付与する方法が考えられる。