From 629747f6a8429344bfcf13dddc49f224a5072636 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 14 Sep 2026 07:17:06 +0000 Subject: [PATCH 1/2] =?UTF-8?q?ADR-0008:=20Go=20=E3=81=AE=20reader=20?= =?UTF-8?q?=E3=81=8C=E3=82=B3=E3=83=BC=E3=83=89=E3=82=92=E8=AA=AC=E6=98=8E?= =?UTF-8?q?=E6=96=87=E3=81=AB=E7=BD=AE=E3=81=8D=E6=8F=9B=E3=81=88=E3=82=8B?= =?UTF-8?q?=E3=81=AE=E3=82=92=E3=82=84=E3=82=81=E3=82=8B=20(=E8=A6=81?= =?UTF-8?q?=E5=88=A4=E6=96=AD)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 生成された Go クライアントの UnmarshalXML は ONIX のコード値を説明文に 置き換えて返す。生成物を実測したところ、これが 3 つの問題を起こしている。 変換が単射でない。v2 では 117 型 6,314 個の case のうち 72 個、v3 では 209 型 7,688 個のうち 254 個が、他の case と同じ説明文を返す。最悪なのは CurrencyCode で、BYR と BYN がともに `Belarussian Ruble`、AFA と AFN が ともに `Afghani` になる。BYR/BYN は 2016 年のデノミで 10,000:1、AFA/AFN は 2002 年のデノミで 1,000:1 の関係にあるので、金額が復元できない。 未知のコードでドキュメント全体が落ちる。default 節が全型で error を返し、 Read() がそれをそのまま返す。同梱コードリストは v2 が Issue 36、v3 が Issue 52 で、それより新しい Issue のコードを含む正当な ONIX ファイルは 読めない。「後方互換性を保って ONIX に追従する」という価値と衝突する。 ONIX として書き戻せない。MarshalXML は v2/v3 のどこにも実装がないので、 デコードした構造体を encoding/xml で書き出すと要素の中身が説明文になる。 e2e テストは JSON にしか書き出していないためこれを検出しない。 あわせて、TypeScript は同じジェネレータから生成されながらこの変換をせず、 同じファイルから Go は `Afghani`、TypeScript は `AFN` を返す。 この ADR は方針だけで実装を含まない。generated/go は配布物なので、返る値が 変わるのは利用者にとって破壊的変更であり、受理の判断はメンテナに委ねる。 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01P8rZakwAiz1jpViUX34x1Z --- ...uld-not-replace-codes-with-descriptions.md | 142 ++++++++++++++++++ docs/adr/README.md | 1 + 2 files changed, 143 insertions(+) create mode 100644 docs/adr/0008-go-reader-should-not-replace-codes-with-descriptions.md diff --git a/docs/adr/0008-go-reader-should-not-replace-codes-with-descriptions.md b/docs/adr/0008-go-reader-should-not-replace-codes-with-descriptions.md new file mode 100644 index 0000000..2cf8ae4 --- /dev/null +++ b/docs/adr/0008-go-reader-should-not-replace-codes-with-descriptions.md @@ -0,0 +1,142 @@ +# ADR-0008: Go の reader が ONIX コードを説明文に置き換えるのをやめる + +- **ステータス**: Proposed(要判断 — 生成コードの公開 API を壊す変更を伴う) +- **日付**: 2026-09-14 + +## 背景 + +生成された Go クライアントは、ONIX のコード値を**人間可読な説明文に置き換えて**返す。 +`template/go/{v2,v3}/code.mustache` の `UnmarshalXML` がそれを行っている。 + +```go +// UnmarshalXML is unmarshaler from code to human readable description as of defined at codelists. +func (c *CurrencyCode) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + var v string + d.DecodeElement(&v, &start) + switch v { + + // Afghanistan. DEPRECATED, replaced by AFN + case "AFA": + c.Body = `Afghani` + + // Afghanistan (prices normally quoted as integers) + case "AFN": + c.Body = `Afghani` + ... + default: + return fmt.Errorf("undefined code for CurrencyCode has been passed, got [%s]", v) + } +} +``` + +生成物を実測した結果は次のとおり。 + +| | v2 | v3 | +| -------------------------------- | ----- | ----- | +| コード型の数 | 117 | 209 | +| `case` 節(コード値)の総数 | 6,314 | 7,688 | +| **説明文が他と衝突する `case`** | **72**(19 型) | **254**(33 型) | +| `default:` で `error` を返す型 | 117 | 209 | +| `MarshalXML` の実装 | 0 | 0 | + +TypeScript 側は同じジェネレータから生成されているが、**この変換を行わない**。 +`generated/typescript/v2/code.ts` はすべて `export type CurrencyCode = string` で、 +reader は値をそのまま返す(ADR-0007 も参照)。 + +### 観測された具体的な問題 + +**(1) 変換が単射でない。実データが壊れる。** + +ONIX のコードリストには、値は違うが説明文が同一のものがある。`CurrencyCode` が最も悪い。 + +| コード | ONIX 側の注記 | Go が返す値 | +| ------ | ------------- | ----------- | +| `BYR` | "Now replaced by new Belarussian Ruble (BYN): use only for historical prices that pre-date the introduction of the new Belarussian Ruble" | `Belarussian Ruble` | +| `BYN` | "Belarus" | `Belarussian Ruble` | +| `AFA` | "Afghanistan. DEPRECATED, replaced by AFN" | `Afghani` | +| `AFN` | "Afghanistan (prices normally quoted as integers)" | `Afghani` | + +BYR と BYN は 2016 年のデノミで **10,000:1**、AFA と AFN は 2002 年のデノミで **1,000:1** の関係にある。 +つまり `Price` が `100000` で通貨が `Belarussian Ruble` という Go の値からは、 +**それが 100000 BYR(= 10 BYN)なのか 100000 BYN(= 10 億 BYR)なのかを復元できない。** +金額に 10,000 倍の差が出る。 + +言語コードでも `hrv`/`scr` がともに `Croatian`、`scc`/`srp` がともに `Serbian` になる。 + +**(2) 未知のコードでドキュメント全体のパースが失敗する。** + +`default:` 節は全 117(v3 は 209)の型で `error` を返す。`Read()` はこれを +`decoder.Decode()` 経由でそのまま呼び出し元に返すので、**巨大な ONIX ファイルの中に +1 つでも未知のコードがあれば、ファイル全体が読めない。** + +ONIX のコードリストはスキーマ本体とは別に版が上がり、新しいコード値が随時追加される。 +現在同梱しているのは v2 が Issue 36、v3 が Issue 52 で、いずれも実際の最新より古い。 +**より新しい Issue のコードを含む正当な ONIX ファイルは、このクライアントでは読めない。** +これは「ONIX の仕様をなるべく後方互換性を保って追従する」という、このリポジトリの +第一の価値と正面から衝突する。 + +**(3) ONIX として書き戻せない。** + +`MarshalXML` はどこにも実装されていない。デコードした構造体を `encoding/xml` で +書き出すと `Afghani` になり、**妥当な ONIX ではない。** +e2e テストは JSON に書き出しているだけなので、これを検出しない。 + +**(4) 言語ごとに返る値が違う。** + +同じ ONIX ファイルから、Go は `"Afghani"`、TypeScript は `"AFN"` を返す。 +「コード生成の仕組みでなるべくサポートできる言語を増やす」という価値に照らすと、 +言語を増やすたびに「この言語はどちらの流儀か」が増えることになる。 + +## 決定 + +**Go の `UnmarshalXML` がコード値を説明文に置き換えるのをやめ、コード値をそのまま保持する。** +説明文は、値を潰す形ではなく別の経路で提供する。 + +そのうえで、未知のコードはエラーにせず、そのまま通す。 + +この ADR は方針の決定のみで、**実装は含まない**(後述の「結果」を参照)。 + +## 理由 + +- **(1) は正しさの問題であって、好みの問題ではない。** 通貨のデノミを跨いだ金額を + 復元できないのは、このライブラリを使った時点で発生するデータ破壊であり、 + 利用側で回避する手段がない(元のコードはもう構造体に残っていない)。 +- **(2) は後方互換性そのもの。** コードリストの版が上がるたびに既存の利用者のパースが + 壊れる設計は、ONIX を追従するライブラリとして成立しない。「未知のコードは通す」なら、 + 同梱コードリストが古いままでも新しいファイルが読める。 +- コード値を保持する側が可逆で、説明文を保持する側が不可逆。**可逆な方を既定にして、 + 不可逆な変換は利用側が必要なときに呼ぶ**、という向きが自然。 +- TypeScript が既にそうなっており(ADR-0007)、言語間で挙動が揃う。 + +## 検討した他の選択肢 + +- **案 A: 現状維持。** 採らない。(1) のデータ破壊と (2) の後方互換性の破れが残る。 + 既存利用者の API を壊さない、という利点はあるが、壊れているのは API ではなく返る値。 +- **案 B: 置き換えはやめるが、未知のコードは引き続きエラーにする。** 採らない。 + (1) は直るが (2) が残る。同梱コードリストより新しいファイルが読めない状態は変わらない。 +- **案 C: 構造体にコードと説明文の両方のフィールドを持たせる。** + 情報は失われないので (1) は直り、(3) も `MarshalXML` を足せば直せる。 + ただし全コード型の struct 形が変わり、生成物のサイズが説明文の分だけ増える + (v2 の 30,342 行が更に伸びる)。案の採否は実装時に改めて判断する余地がある。 +- **案 D: 説明文を `Description()` メソッドとして生成する。** + 値はコードのまま、説明文は `c.Description()` で取れる。可逆性を保ったまま + 説明文も提供でき、既存の `switch` をそのまま流用できる。**現時点ではこれが有力。** + なお現在の生成物には `Description` という型名もフィールド名も存在しない + (v2 / v3 の `code.go` と `model.go` を確認)ので、今のところ名前は衝突しない。 + ただしフィールド名はスキーマ由来なので、コードリストの版を上げるたびに再確認は要る。 + +## 結果 + +- **これは生成コードの利用者にとって破壊的変更である。** `CurrencyCode` の値として + `"Afghani"` を期待していたコードは `"AFN"` を受け取るようになる。 + `generated/go/{v2,v3}` はこのリポジトリが配布している成果物なので、 + **この ADR を Accepted にするかどうかはメンテナの判断を要する。** + 実装 PR はこの ADR が受理されてから出す。 +- 実装時に併せて対処すべきもの: + - `d.DecodeElement(&v, &start)` の戻り値が捨てられている(テンプレートの 2 箇所、 + 生成物では全コード型)。 + - `MarshalXML` が無いため ONIX として書き戻せない (3)。置き換えをやめれば + `encoding/xml` の既定の挙動で正しく書き出せるようになる。 + - e2e テストが JSON 出力しか見ていないため (3) を検出できない。 +- **見直す条件**: 説明文を値として受け取ることに依存した利用者が実在すると分かった場合は、 + 案 C / 案 D のどちらで説明文を提供するかを、その利用形態に合わせて選び直す。 diff --git a/docs/adr/README.md b/docs/adr/README.md index 27f79d9..39cb011 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -9,6 +9,7 @@ | [0001](0001-record-architecture-decisions.md) | 設計判断を ADR として記録する | Accepted | | [0002](0002-decouple-unit-tests-from-the-vendored-schema.md) | ユニットテストを取得済みスキーマから切り離す | Accepted | | [0003](0003-editeur-schema-acquisition.md) | EDItEUR スキーマの取得方法 | Accepted | +| [0008](0008-go-reader-should-not-replace-codes-with-descriptions.md) | Go の reader が ONIX コードを説明文に置き換えるのをやめる | Proposed | ## 書き方 From 8cfbd2696946d85f35326225154e36d82734411a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 14 Sep 2026 07:34:40 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=E3=83=AC=E3=83=93=E3=83=A5=E3=83=BC?= =?UTF-8?q?=E3=82=92=E5=8F=97=E3=81=91=E3=81=A6=20ADR-0008=20=E3=81=AE?= =?UTF-8?q?=E4=BA=8B=E5=AE=9F=E8=AA=A4=E8=AA=8D=E3=82=92=E8=A8=82=E6=AD=A3?= =?UTF-8?q?=E3=81=97=E3=80=81=E3=82=88=E3=82=8A=E5=BC=B7=E3=81=84=E5=AE=9F?= =?UTF-8?q?=E4=BE=8B=E3=81=AB=E5=B7=AE=E3=81=97=E6=9B=BF=E3=81=88=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit レビューで 5 件の事実誤認を指摘され、いずれも裏を取って確認した。 1. 「v2 の Issue 36 も最新より古い」は誤り。Issue 36 は EDItEUR が ONIX 2.1 向けに出した最後のコードリストで、Issue 37 以降は 2.1 用のリストを含まない。 v2 側は古いのではなく、これ以上新しくならない。後方互換性の議論は v3 (Issue 52 に対して現行 74) にのみ当てはまる。 2. BYR/BYN が衝突するのは v2 だけ。v3 は `(Old) Belarussian Ruble` と `Belarussian Ruble` に書き分けられている。ADR の目玉に据えていた例が v3 で 成立しなかったので、両版で成立する AFA/AFN と RUB/RUR に差し替える。 3. 「生成物のコメントそのまま」と書いた引用が truncate されていた。全文に直す。 4. 「置き換えをやめれば encoding/xml の既定で正しく書き出せる」は、スペース 区切りのコードリスト型 (v2 で 5 型、v3 で 6 型) では成立しない。go1.24.7 で 実測したところ `CountryCodeList{"GB","US"}` は `GBUS` になる。独自の MarshalXML が要る。 5. ADR-0007 は未マージで main に無く、main の reader.ts は fast-xml-parser の 既定で数値変換する。「TypeScript は値をそのまま返す」は成立しない。 code.ts が全 116 型 `= string` である点だけが正しい。 あわせて、レビューが見つけたより強い実例を本文の先頭に据える。case を 1 つも 持たない型 (v2 に 9、v3 に 45) の UnmarshalXML は、どんな値でも必ず error を 返す。属性としてしか使われない型なら UnmarshalXMLAttr が呼ばれるので実害は ないが、要素として使われているものは v3 に 225 フィールドある。筆頭の DtDotNonEmptyString は 152 フィールドで、ONIX 3.0 の必須要素 を含む。 つまり v3 の Go クライアントは実在する ONIX 3.0 ファイルを読めない。 e2e が v2 しか通していないため検出されていなかった。 決定も具体化する。案 D (Description() メソッド) を選び、AGENTS.md の 「既存の出力を置き換えず新しい版として足す」に沿う案 E も選択肢に加えた。 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01P8rZakwAiz1jpViUX34x1Z --- ...uld-not-replace-codes-with-descriptions.md | 177 ++++++++++++------ 1 file changed, 116 insertions(+), 61 deletions(-) diff --git a/docs/adr/0008-go-reader-should-not-replace-codes-with-descriptions.md b/docs/adr/0008-go-reader-should-not-replace-codes-with-descriptions.md index 2cf8ae4..ef2c3fa 100644 --- a/docs/adr/0008-go-reader-should-not-replace-codes-with-descriptions.md +++ b/docs/adr/0008-go-reader-should-not-replace-codes-with-descriptions.md @@ -14,11 +14,9 @@ func (c *CurrencyCode) UnmarshalXML(d *xml.Decoder, start xml.StartElement) erro var v string d.DecodeElement(&v, &start) switch v { - // Afghanistan. DEPRECATED, replaced by AFN case "AFA": c.Body = `Afghani` - // Afghanistan (prices normally quoted as integers) case "AFN": c.Body = `Afghani` @@ -31,99 +29,156 @@ func (c *CurrencyCode) UnmarshalXML(d *xml.Decoder, start xml.StartElement) erro 生成物を実測した結果は次のとおり。 -| | v2 | v3 | -| -------------------------------- | ----- | ----- | -| コード型の数 | 117 | 209 | -| `case` 節(コード値)の総数 | 6,314 | 7,688 | -| **説明文が他と衝突する `case`** | **72**(19 型) | **254**(33 型) | -| `default:` で `error` を返す型 | 117 | 209 | -| `MarshalXML` の実装 | 0 | 0 | +| | v2 | v3 | +| ------------------------------- | ----- | ----- | +| コード型の数 | 117 | 209 | +| `case` 節(コード値)の総数 | 6,314 | 7,688 | +| 説明文が他と衝突する `case` | 72(19 型) | 254(33 型) | +| `default:` で `error` を返す型 | 117 | 209 | +| **`case` を 1 つも持たない型** | **9** | **45** | +| `MarshalXML` の実装 | 0 | 0 | -TypeScript 側は同じジェネレータから生成されているが、**この変換を行わない**。 -`generated/typescript/v2/code.ts` はすべて `export type CurrencyCode = string` で、 -reader は値をそのまま返す(ADR-0007 も参照)。 +### 観測された問題 (1): v3 の Go クライアントは ONIX 3.0 をそもそも読めない -### 観測された具体的な問題 +`case` を 1 つも持たない型の `UnmarshalXML` は、こうなる。 -**(1) 変換が単射でない。実データが壊れる。** +```go +func (c *DtDotNonEmptyString) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + var v string + d.DecodeElement(&v, &start) + switch v { + default: + return fmt.Errorf("undefined code for DtDotNonEmptyString has been passed, got [%s]", v) + } +} +``` + +**どんな値が来ても必ず `error` を返す。** これらはコードリストではなく XSD の +データ型(`dt:NonEmptyString` など)なので、列挙値が存在せず `case` が 0 個になる。 + +属性としてしか使われない型なら `UnmarshalXMLAttr` の方が呼ばれるので実害はない。 +問題は**要素として使われている**ものである。 + +| 版 | `case` 0 個の型が要素として使われているフィールド数 | +| --- | --- | +| v2 | **2**(`ReligiousTextID` ×1、`IntermediaryAvailabilityCode` ×1) | +| v3 | **225**(21 型) | + +v3 の内訳の筆頭は `DtDotNonEmptyString` の **152 フィールド**で、その中には +ONIX 3.0 の必須要素が含まれる。 + +```go +IDValue DtDotNonEmptyString `xml:"b244"` +``` + +`` を含む ONIX 3.0 ファイルは、**値が何であれ必ずパースに失敗する。** +つまり **v3 の Go クライアントは、実在する ONIX 3.0 ファイルを読めない。** +README の「Schema Version 3 with Codes Issue52: OK」は成り立っていない。 + +e2e テストは v2 しか通していない(`e2e/go/main.go` が +`generated/go/v2` を import している)ため、これは検出されていなかった。 + +### 観測された問題 (2): 変換が単射でない ONIX のコードリストには、値は違うが説明文が同一のものがある。`CurrencyCode` が最も悪い。 -| コード | ONIX 側の注記 | Go が返す値 | -| ------ | ------------- | ----------- | -| `BYR` | "Now replaced by new Belarussian Ruble (BYN): use only for historical prices that pre-date the introduction of the new Belarussian Ruble" | `Belarussian Ruble` | -| `BYN` | "Belarus" | `Belarussian Ruble` | +| コード | ONIX 側の注記(v2 の生成物より、全文) | Go が返す値 | +| ------ | ------------------------------------- | ----------- | | `AFA` | "Afghanistan. DEPRECATED, replaced by AFN" | `Afghani` | | `AFN` | "Afghanistan (prices normally quoted as integers)" | `Afghani` | -BYR と BYN は 2016 年のデノミで **10,000:1**、AFA と AFN は 2002 年のデノミで **1,000:1** の関係にある。 -つまり `Price` が `100000` で通貨が `Belarussian Ruble` という Go の値からは、 -**それが 100000 BYR(= 10 BYN)なのか 100000 BYN(= 10 億 BYR)なのかを復元できない。** -金額に 10,000 倍の差が出る。 +AFA と AFN は 2002 年のデノミで **1,000:1** の関係にある。つまり `Price` が +`100000` で通貨が `Afghani` という Go の値からは、**それが 100000 AFA +(= 100 AFN)なのか 100000 AFN(= 1 億 AFA)なのかを復元できない。** +`RUB`/`RUR` も同じ形で衝突する(1998 年のデノミ、1,000:1)。 + +`BYR`/`BYN` は **v2 でのみ**衝突する。v2 はどちらも `Belarussian Ruble` を返すが、 +v3 は `(Old) Belarussian Ruble` / `Belarussian Ruble` と書き分けられているため +衝突しない。**版によって壊れ方が違う**、という点自体がこの設計の脆さを示している。 -言語コードでも `hrv`/`scr` がともに `Croatian`、`scc`/`srp` がともに `Serbian` になる。 +言語コードでも `hrv`/`scr` がともに `Croatian`、`scc`/`srp` がともに `Serbian` になる +(両版)。 -**(2) 未知のコードでドキュメント全体のパースが失敗する。** +### 観測された問題 (3): 未知のコードでドキュメント全体のパースが失敗する `default:` 節は全 117(v3 は 209)の型で `error` を返す。`Read()` はこれを `decoder.Decode()` 経由でそのまま呼び出し元に返すので、**巨大な ONIX ファイルの中に 1 つでも未知のコードがあれば、ファイル全体が読めない。** -ONIX のコードリストはスキーマ本体とは別に版が上がり、新しいコード値が随時追加される。 -現在同梱しているのは v2 が Issue 36、v3 が Issue 52 で、いずれも実際の最新より古い。 -**より新しい Issue のコードを含む正当な ONIX ファイルは、このクライアントでは読めない。** -これは「ONIX の仕様をなるべく後方互換性を保って追従する」という、このリポジトリの -第一の価値と正面から衝突する。 +ただし**版によって意味が違う**点に注意が要る。 + +- **v2**: 同梱の Issue 36 は、EDItEUR が ONIX 2.1 向けに出した**最後のコードリスト**で + ある(Issue 37 以降は 2.1 用のリストを含まない)。したがって v2 側は「古い」のでは + なく、**これ以上新しくならない**。この経路での破綻は起きにくい。 +- **v3**: 同梱の Issue 52 に対して、現行は Issue 74(2026-07-21)。**22 版ぶんの + コード値が未知として扱われる。** ここは「ONIX の仕様をなるべく後方互換性を保って + 追従する」という価値と正面から衝突する。 -**(3) ONIX として書き戻せない。** +### 観測された問題 (4): ONIX として書き戻せない `MarshalXML` はどこにも実装されていない。デコードした構造体を `encoding/xml` で 書き出すと `Afghani` になり、**妥当な ONIX ではない。** e2e テストは JSON に書き出しているだけなので、これを検出しない。 -**(4) 言語ごとに返る値が違う。** +**置き換えをやめるだけでは、これは直りきらない。** スペース区切りのコードリスト型 +(v2 で 5 型、v3 で 6 型)は `[]string` を土台にしているため、`encoding/xml` の既定の +挙動では**繰り返し要素**になり、スペース区切りの単一要素には戻らない。go1.24.7 で実測: -同じ ONIX ファイルから、Go は `"Afghani"`、TypeScript は `"AFN"` を返す。 -「コード生成の仕組みでなるべくサポートできる言語を増やす」という価値に照らすと、 -言語を増やすたびに「この言語はどちらの流儀か」が増えることになる。 +``` +CountryCodeList{"GB", "US"} -> GBUS +``` -## 決定 +これらの型には、置き換えをやめたうえで**独自の `MarshalXML` が要る**。 + +### 観測された問題 (5): 言語ごとに返る値が違う -**Go の `UnmarshalXML` がコード値を説明文に置き換えるのをやめ、コード値をそのまま保持する。** -説明文は、値を潰す形ではなく別の経路で提供する。 +`generated/typescript/v2/code.ts` は全 116 型が `export type X = string` で、 +**Go のような説明文への置き換えはしない。** 同じ ONIX ファイルから +Go は `"Afghani"`、TypeScript は `"AFN"` を返す。 + +(なお TypeScript 側にも別種の値の変換がある。`main` の `reader.ts` は +fast-xml-parser の既定設定で `xml.parse()` を呼んでいるため、数値に見える値を +number に変換してしまう。これは ADR-0007 と #64 で扱っており、本 ADR の対象外。 +ADR-0007 は本 ADR 執筆時点で未マージ。) + +## 決定 -そのうえで、未知のコードはエラーにせず、そのまま通す。 +**Go の `UnmarshalXML` がコード値を説明文に置き換えるのをやめ、コード値をそのまま +保持する。** 未知のコードはエラーにせず通す。説明文は `Description()` メソッドとして +別途生成する(案 D)。 この ADR は方針の決定のみで、**実装は含まない**(後述の「結果」を参照)。 ## 理由 -- **(1) は正しさの問題であって、好みの問題ではない。** 通貨のデノミを跨いだ金額を +- **(1) は「壊れている」で済む話ではなく、v3 のクライアントが機能していない。** + コード値をそのまま返すようにすれば、`case` を 1 つも持たない型は + 「素通しする型」になり、225 フィールドが動き出す。 +- **(2) は正しさの問題であって、好みの問題ではない。** 通貨のデノミを跨いだ金額を 復元できないのは、このライブラリを使った時点で発生するデータ破壊であり、 利用側で回避する手段がない(元のコードはもう構造体に残っていない)。 -- **(2) は後方互換性そのもの。** コードリストの版が上がるたびに既存の利用者のパースが - 壊れる設計は、ONIX を追従するライブラリとして成立しない。「未知のコードは通す」なら、 - 同梱コードリストが古いままでも新しいファイルが読める。 +- **(3) は後方互換性そのもの。** コードリストの版が上がるたびに既存の利用者のパースが + 壊れる設計は、ONIX を追従するライブラリとして成立しない。 - コード値を保持する側が可逆で、説明文を保持する側が不可逆。**可逆な方を既定にして、 不可逆な変換は利用側が必要なときに呼ぶ**、という向きが自然。 -- TypeScript が既にそうなっており(ADR-0007)、言語間で挙動が揃う。 +- 案 D を採るのは、値の形(`string` / `[]string`)を変えずに説明文も提供できるため。 + 現在の生成物には `Description` という型名もフィールド名も存在しない + (v2 / v3 の `code.go` と `model.go` で確認)ので、名前は衝突しない。 + ただしフィールド名はスキーマ由来なので、コードリストの版を上げるたびに再確認が要る。 ## 検討した他の選択肢 -- **案 A: 現状維持。** 採らない。(1) のデータ破壊と (2) の後方互換性の破れが残る。 - 既存利用者の API を壊さない、という利点はあるが、壊れているのは API ではなく返る値。 +- **案 A: 現状維持。** 採らない。v3 が読めない状態が残る。 - **案 B: 置き換えはやめるが、未知のコードは引き続きエラーにする。** 採らない。 - (1) は直るが (2) が残る。同梱コードリストより新しいファイルが読めない状態は変わらない。 -- **案 C: 構造体にコードと説明文の両方のフィールドを持たせる。** - 情報は失われないので (1) は直り、(3) も `MarshalXML` を足せば直せる。 - ただし全コード型の struct 形が変わり、生成物のサイズが説明文の分だけ増える - (v2 の 30,342 行が更に伸びる)。案の採否は実装時に改めて判断する余地がある。 -- **案 D: 説明文を `Description()` メソッドとして生成する。** - 値はコードのまま、説明文は `c.Description()` で取れる。可逆性を保ったまま - 説明文も提供でき、既存の `switch` をそのまま流用できる。**現時点ではこれが有力。** - なお現在の生成物には `Description` という型名もフィールド名も存在しない - (v2 / v3 の `code.go` と `model.go` を確認)ので、今のところ名前は衝突しない。 - ただしフィールド名はスキーマ由来なので、コードリストの版を上げるたびに再確認は要る。 + (1) と (2) は直るが (3) が残り、v3 は Issue 52 より新しいコードで落ち続ける。 +- **案 C: 構造体にコードと説明文の両方のフィールドを持たせる。** 採らない。 + 情報は失われないが、全コード型の struct 形が変わるため利用側の破壊が案 D より大きい。 +- **案 D: 説明文を `Description()` メソッドとして生成する。** **これを採る。** +- **案 E: 既存の `generated/go/v2`・`v3` は据え置き、新しい出力先を足す。** + AGENTS.md の「既存の出力を置き換えず新しい版として足す」方針に沿う案で、 + 既存利用者を一切壊さない。ただし v3 が読めない問題は「新しい方を使ってください」 + でしか解決せず、**壊れた成果物を配布し続けることになる。** + 破壊的変更を避けたい場合の次善策として、メンテナの判断に委ねる。 ## 結果 @@ -135,8 +190,8 @@ e2e テストは JSON に書き出しているだけなので、これを検出 - 実装時に併せて対処すべきもの: - `d.DecodeElement(&v, &start)` の戻り値が捨てられている(テンプレートの 2 箇所、 生成物では全コード型)。 - - `MarshalXML` が無いため ONIX として書き戻せない (3)。置き換えをやめれば - `encoding/xml` の既定の挙動で正しく書き出せるようになる。 - - e2e テストが JSON 出力しか見ていないため (3) を検出できない。 -- **見直す条件**: 説明文を値として受け取ることに依存した利用者が実在すると分かった場合は、 - 案 C / 案 D のどちらで説明文を提供するかを、その利用形態に合わせて選び直す。 + - スペース区切りのコードリスト型に `MarshalXML` が要る (4)。 + - **e2e テストが v2 しか通していない。** v3 を通すケースを足さない限り、 + (1) のような破綻はまた検出されない。 +- **見直す条件**: 説明文を値として受け取ることに依存した利用者が実在すると分かった + 場合は、案 C / 案 E のどちらに寄せるかを、その利用形態に合わせて選び直す。