# discriminator を持つ親スキーマのインライン enum 生成
## 現状と問題点
lambda360 の `ShapeNode` は OpenAPI の `discriminator` を持つ多態型だが、
現在のテンプレートはこれを平凡な struct として生成している。
```rust
// 現在の出力(誤)
pub struct ShapeNode {
pub r#op: String,
}
```
期待する出力は、`op` を内部タグとした Rust の enum(TypeScript はユニオン型)。
## 目標とする出力
### Rust
```rust
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(tag = "op")]
pub enum ShapeNode {
#[serde(rename = "step")]
Step {
path: String,
content_hash: Option<String>,
},
#[serde(rename = "union")]
Union {
a: Box<ShapeNode>,
b: Box<ShapeNode>,
},
#[serde(rename = "rotate")]
Rotate {
shape: Box<ShapeNode>,
axis: Vec<NumberOrExpr>,
deg: NumberOrExpr,
},
// ...
}
```
- `#[serde(tag = "op")]` で JSON の `op` フィールドをタグとする
- 各バリアントのフィールドはタグフィールド (`op`) を**除いた**子スキーマのプロパティ
- 自己参照フィールド (`$ref` → ShapeNode) は `Box<ShapeNode>` に包む
### TypeScript
```typescript
type ShapeNode =
| StepNode
| UnionShapeNode
| IntersectNode
| SubtractNode
| ScaleNode
| TranslateNode
| RotateNode
| StretchNode;
```
TypeScript は再帰型を直接扱えるので Box 不要。
## 実装方針
### 1. IDENTIFIED_SCHEMA マクロに discriminator ブランチを追加
`schema.type == "object" and schema.discriminator` の場合は struct ではなく enum を生成する。
```jinja
{%- if schema.type == "object" and schema.discriminator %}
{# 内部タグ付き enum #}
#[derive(Clone,Debug,serde::Serialize,serde::Deserialize)]
#[serde(tag="{{schema.discriminator.propertyName}}")]
pub enum {{SCHEMA_NAME(pointer)}}{
#[serde(rename="{{disc_key}}")]
{{disc_key|to_pascal_case}}{
{%- for prop_key, prop in child.properties|default({})|items %}
{%- if prop_key != schema.discriminator.propertyName %} {# タグフィールドをスキップ #}
pub r#{{prop_key}}: ...,
{%- endif %}
{%- endfor %}
},
{%- endfor %}
}
```
### 2. タグフィールドのスキップ
`discriminator.propertyName` (= `"op"`) と一致するプロパティを各バリアントのフィールド列挙からスキップする。
こうすることで `#[serde(tag = "op")]` との衝突(シリアライズ時の重複キー)を避けられる。
### 3. 再帰フィールドの Box 包み
SCHEMA マクロにて、`$ref` が指す先のスキーマに `discriminator` がある場合は `Box<T>` を出力する。
```jinja
{%- if schema["$ref"] %}
{%- endif %}
```
### 4. 子スキーマ (StepNode 等) の扱い
`StepNode`, `RotateNode` 等の子スキーマは引き続き独立した struct として生成する。
lambda360 の文脈ではこれらは enum バリアント内に展開されるため未使用になるが、
削除ロジックは不要(将来の独立利用に備え残す)。
### 5. TypeScript の IDENTIFIED_SCHEMA
discriminator があれば mapping の値を `ref_name` でユニオンとして並べる。
```jinja
{%- if schema.type == "object" and schema.discriminator %}
type {{SCHEMA_NAME(pointer)}} =
{%- endfor %};
```
## 懸念事項
### child schema の必須フィールド判定
子スキーマのプロパティをインライン展開するとき、`required` 配列も子スキーマのものを参照する必要がある。
現在の `IDENTIFIED_SCHEMA` は `schema.required` を使っているため、
discriminator バリアント生成時は `child.required` を参照するよう分岐が必要。
### allOf による継承フィールドは無視
`StepNode` 等は `allOf: [ShapeNode]` を持つが、これは「ShapeNode の一種」という宣言であり
テンプレート側では追加フィールドは不要(ShapeNode enum がタグで吸収するため)。
`allOf` で追加されるフィールドは現状通り無視する。
### Default 実装
`Default` が必要なケース(req.body の Default など)に備え、
mapping の最初のバリアントを `Default` で返す実装を追加する。
```rust
impl Default for ShapeNode {
fn default() -> Self {
Self::Step { path: Default::default(), content_hash: Default::default() }
}
}
```
ただしバリアントのフィールドが確定してから生成する必要がある。
## 影響範囲
- `templates/rust_axum.template`: SCHEMA マクロ(Box 包み)、IDENTIFIED_SCHEMA マクロ(enum 生成)
- `templates/typescript_hono.template`: IDENTIFIED_SCHEMA マクロ(union 型)
- `src/` の変更は不要