Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ jobs:
name: Test haskell codes
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: haskell/actions/setup@v1
- uses: actions/checkout@v4
- uses: haskell-actions/setup@v2
id: haskell-setup
with:
ghc-version: "8.8.3"
enable-stack: true
stack-version: "2.5.1"
- name: Restore caches
uses: actions/cache@v2
uses: actions/cache@v4
with:
path: |
${{steps.haskell-setup.outputs.stack-path}}
Expand All @@ -22,19 +22,18 @@ jobs:
key: ${{ runner.os }}-${{ hashFiles('**/package.yaml') }}-${{ hashFiles('**/onix.cabal') }}-${{ hashFiles('**/stack.yaml.lock') }}
restore-keys: |
${{ runner.os }}-
- run: ls -lah ${{steps.haskell-setup.outputs.stack-path}}
- run: make test
e2e:
name: Test e2e
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Setup Node.js environment
uses: actions/setup-node@v2.1.4
uses: actions/setup-node@v4
with:
node-version: 12.x
- name: Restore caches
uses: actions/cache@v2
uses: actions/cache@v4
with:
path: |
~/.npm
Expand Down
16 changes: 10 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
TS_FILES := $(shell find ./ -type f -name '*.ts' | grep -v 'node_modules')
HS_FILES := $(shell find ./ -type f -name '*.hs' | grep -v '.stack-work')
BZL := npx bazelisk
BZL_BIN := $(shell npx bazel info bazel-bin)
# Deferred on purpose: `:=` would run bazel on every make invocation, including
# `make test`, which runs in a job with no node_modules and no need for bazel.
BZL_BIN = $(shell $(BZL) info bazel-bin)

generated/go/%: build
stack exec onix-exe -- --schemaVersion $(@F) --language go
Expand All @@ -12,19 +14,21 @@ generated/ts/%: build
debug: build
stack exec --trace -- onix-exe +RTS -xc --RTS --schemaVersion v3 --language go

# The fixtures under fixtures/ are self-contained, so the tests deliberately do
# not depend on the `schema` target: `make schema` downloads the EDItEUR
# archives over the network, and requiring it here made the test suite
# unrunnable whenever editeur.org was unreachable. Keep fixtures from including
# anything outside fixtures/, or this dependency comes back.
# See docs/adr/0002-decouple-unit-tests-from-the-vendored-schema.md
.PHONY: test
test: schema
test:
stack test --trace --fast

.stack-work: $(HS_FILES) package.yaml stack.yaml
stack build --fast

build: schema .stack-work

json: fixtures/20201200.json
fixtures/20201200.json: run
go run github.com/kogai/onix-codegen/go/helper

WORKSPACE: go.mod
$(BZL) run //:gazelle -- update-repos -from_file=go.mod

Expand Down
16 changes: 0 additions & 16 deletions WORKSPACE
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,6 @@ http_archive(
url = "https://www.editeur.org/files/ONIX%203/ONIX_BookProduct_XSD_schema+codes_Issue_52.zip",
)

http_archive(
name = "build_bazel_rules_nodejs",
sha256 = "dd4dc46066e2ce034cba0c81aa3e862b27e8e8d95871f567359f7a534cccb666",
urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/3.1.0/rules_nodejs-3.1.0.tar.gz"],
)

# The npm_install rule runs yarn anytime the package.json or package-lock.json file changes.
# It also extracts any Bazel rules distributed in an npm package.
load("@build_bazel_rules_nodejs//:index.bzl", "npm_install")

npm_install(
name = "npm",
package_json = "//:package.json",
package_lock_json = "//:package-lock.json",
)

http_archive(
name = "io_bazel_rules_go",
sha256 = "207fad3e6689135c5d8713e5a17ba9d1290238f47b9ba545b63d9303406209c6",
Expand Down
26 changes: 26 additions & 0 deletions docs/adr/0000-template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# ADR-0000: タイトル

- **ステータス**: Proposed | Accepted | Superseded by ADR-XXXX
- **日付**: YYYY-MM-DD

## 背景

どういう状況で、何が問題になっているか。観測された事実 (エラーメッセージ、計測値、
外部サービスの挙動) を具体的に書く。

## 決定

何を決めたか。

## 理由

なぜその選択肢を選んだか。

## 検討した他の選択肢

- **案 A**: 内容と、採らなかった理由
- **案 B**: 内容と、採らなかった理由

## 結果

この決定によって何が変わるか。新しく発生する制約や、将来見直すべき条件も書く。
52 changes: 52 additions & 0 deletions docs/adr/0001-record-architecture-decisions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# ADR-0001: 設計判断を ADR として記録する

- **ステータス**: Accepted
- **日付**: 2026-09-13

## 背景

onix-codegen には、コードを読むだけでは意図が復元できない判断がいくつもある。例えば次のようなもの。

- スキーマの版を `v2` / `v3` というディレクトリで分け、既存の出力を置き換えない
- 言語固有の知識をテンプレートに閉じ込め、`src/` を言語非依存に保つ
- 対応していない XSD 構造は暗黙に無視せず `unimplemented` / `unreachable` で落とす

これらはいずれも「後方互換性を保ちながら ONIX 仕様に追従する」「サポート言語を増やしやすくする」という
このリポジトリの価値 (AGENTS.md) から導かれているが、根拠がコミットログに散っているため、
後から来た人が「なぜこうなっているのか」を再構成しづらい。結果として、良かれと思った変更が
その判断を静かに壊すことがある。

外部依存の扱いも同じ問題を抱えている。EDItEUR の配布物、Bazel、Stackage のスナップショットは
どれも外部の都合で壊れるが、「なぜこのバージョンに固定しているのか」が残っていないと、
更新のたびに同じ調査をやり直すことになる。

## 決定

設計判断を `docs/adr/` 配下の ADR として記録する。フォーマットは MADR を簡略化したもので、
`0000-template.md` を雛形とする。連番は一度振ったら変えず、判断が変わったときは既存の ADR を
書き換えるのではなく、新しい ADR を書いて古いものを `Superseded` にする。

AGENTS.md には、どういう判断を ADR にすべきかの基準を書き、両者を相互に参照させる。

## 理由

ADR は「決定そのもの」ではなく「決定に至った制約」を残せる点が、コメントやコミットメッセージより優れている。
このリポジトリで重要なのは、まさにその制約 (EDItEUR の配布形態、生成物の互換性、テンプレートの責務分担) が
時間とともに変わることなので、当時の前提ごと記録に残せる形式が要る。

Markdown をリポジトリ内に置くのは、スキーマや生成物と同じコミットで変更履歴が追えるため。
外部の Wiki やイシューに書くと、コードとの対応が切れる。

## 検討した他の選択肢

- **AGENTS.md / README.md に全部書く**: 判断の履歴が積み上がると読みものとして破綻する。
AGENTS.md は「今どうすべきか」を書く場所として保ち、「なぜそうなったか」は ADR に分ける。
- **GitHub Issues / Discussions に残す**: 検索性はあるが、リポジトリを clone しただけでは読めず、
コードとの対応も切れる。オフラインで完結しない。
- **記録しない (現状維持)**: 調査のやり直しと、意図しない互換性破壊が続く。

## 結果

- 新しい設計判断、および後から言語化された既知の判断は ADR に書く。
- ADR を追加したら `docs/adr/README.md` の一覧も更新する。
- ADR はレビュー対象になる。判断に異論があれば、実装ではなく ADR に対してコメントできる。
106 changes: 106 additions & 0 deletions docs/adr/0002-decouple-unit-tests-from-the-vendored-schema.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# ADR-0002: ユニットテストを取得済みスキーマから切り離す

- **ステータス**: Accepted
- **日付**: 2026-09-13

## 背景

Makefile のテストターゲットは `schema` に依存していた。

```make
.PHONY: test
test: schema
stack test --trace --fast
```

`schema` ターゲットは Bazel の `http_archive` 経由で EDItEUR の zip をダウンロードし、
`schema/v2` / `schema/v3` に展開する。つまり `make test` は毎回 editeur.org への
ネットワークアクセスを要求していた。

2026-09-13 時点で、この取得が機能しなくなった。

```
WARNING: Download from https://www.editeur.org/files/ONIX%202.1/ONIX_for_Books_Release2-1_rev03_schema+codes_Issue_36.zip
failed: UnrecoverableHttpException GET returned 202 Accepted
ERROR: An error occurred during the fetch of repository 'org_editeur_v2'
make: *** [Makefile:35: schema/v2] Error 1
```

その結果、`Test haskell codes` ジョブはテストを 1 件も実行しないまま失敗する。
取得できない理由そのものは ADR-0003 で扱う。

### この依存は本物だった

当初、この依存は単なる過剰指定だと考えた。テストコードが直接読むのは
`fixtures/test_*.xsd` だけで、`./schema` という文字列は `src/Lib.hs` の `schemaRoot`
にしか現れないからである。しかしこれは誤りだった。

`fixtures/test_mixed_html.xsd` が、EDItEUR の配布物を直接 include していた。

```xml
<xs:include schemaLocation="../schema/v2/ONIX_XHTML_Subset.xsd" />
```

`Xsd.getSchema` は include を再帰的にたどり (`src/Xsd.hs` の `go` / `goInclude`)、
相対パスを `combineURIs` でローカルパスに解決して `Text.XML.readFile` で読む。
ファイルが無ければ IOException で落ちる。このフィクスチャは
`test/TestMixed.hs` と `test/TestModel.hs` の両方から読まれており、`schema/` は
`.gitignore` されているので、clean checkout では必ず存在しない。

つまり `test: schema` は正しい依存だった。git の履歴もそれを裏づけている。
`test: schema` を追加したコミット (9352123) は、リポジトリに置かれていた
`2_1_rev03_schema/` を削除し、このフィクスチャの include を `../schema/v2/` に
向け直した、まさにそのコミットである。

## 決定

依存を消すのではなく、**依存の対象をリポジトリ内に移す**。

1. `fixtures/test_mixed_html_xhtml_subset.xsd` を追加する。`test_mixed_html.xsd` が
`ref` している 40 個の要素名だけを宣言した、ONIX_XHTML_Subset.xsd の代替物である。
2. `test_mixed_html.xsd` の include をそちらに向ける。
3. そのうえで `test` ターゲットから `schema` 依存を外す。

`build` ターゲットの `schema` 依存は残す。実際にコードを生成するにはスキーマの実体が
要るため、こちらは今も本物の依存である。

## 理由

代替物は、テストが実際に検証している性質を保つように書いた。
`test/TestModel.hs` は、このフィクスチャを読んだうえで `Model.collectElements` が
空になることを表明している。`collectElements` は `complexMixed = False` の要素だけを
拾うフィルタなので、この表明の意味は「XHTML の要素がモデルに漏れてこない」ことである。
XHTML の内容要素はいずれも mixed content なので、代替物でも全要素を
`<xs:complexType mixed="true" />` として宣言した。要素名の集合が実物と一致していること、
フィクスチャ側の `ref` 40 個すべてに宣言が対応することは機械的に確認した。

代替物で足りるのは、テストがこのファイルから必要としているのが**要素の宣言の存在と
mixed であること**だけだからである。ONIX_XHTML_Subset.xsd の完全な内容 (属性、
コンテンツモデルの詳細) は、ここで検証されている性質に寄与していない。

そのうえで、テストを外部サービスの可用性から切り離す価値は大きい。パーサのバグを
直したいときに editeur.org の状態に左右されるのは、依存の向きとして誤っている。

なお、この変更は 202 の問題そのものを解決しない。コード生成と、生成物を最新スキーマへ
追従させる作業は依然としてダウンロードを必要とする (ADR-0003)。

## 検討した他の選択肢

- **`test: schema` を残す**: 事実としては正しい依存なので、これは筋が通っている。
ただし、たった 1 ファイルの XSD のためにテスト全体を外部サービスに縛り続けることになる。
- **ONIX_XHTML_Subset.xsd をそのまま `fixtures/` に取り込む**: 代替物より忠実だが、
EDItEUR の配布物の再配布にあたる。ライセンスの判断が要るため、テストを通すためだけに
踏み込むべきではない (ADR-0003 と同じ理由)。代替物は自前で書いたものなのでこの問題がない。
- **include を単に削除する**: `Annotation` 以外に要素が無くなるので `collectElements` は
空になり、表明は通ってしまう。だが「XHTML 要素が漏れてこない」ことを何も検証しなくなり、
テストが意味を失う。**採らない。**

## 結果

- `make test` はネットワークなしで実行できる。CI の Haskell ジョブは editeur.org に依存しない。
- `fixtures/test_mixed_html_xhtml_subset.xsd` は実物の代替物である。実物側の構造が変わって
テストの前提が動くことはあり得るので、スキーマの版を上げるときはこのファイルも見直す。
- コード生成 (`make build`、`make generated/...`) は引き続きダウンロードを必要とする。
- 新しいテストを書くときは `fixtures/` に最小の XSD を足す、という既存の慣習が
そのまま「テストを外部依存から切り離す」ことにもなる。この性質は維持する。
フィクスチャから `fixtures/` の外を include しないこと。
80 changes: 80 additions & 0 deletions docs/adr/0004-drop-rules-nodejs-from-the-e2e-test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# ADR-0004: e2e スナップショット比較から rules_nodejs を外す

- **ステータス**: Accepted
- **日付**: 2026-09-13

## 背景

`build_bazel_rules_nodejs` 3.1.0 は WORKSPACE の依存として宣言されていたが、
実際に使われていたのは次の 2 箇所だけだった。

```
WORKSPACE:18 name = "build_bazel_rules_nodejs",
WORKSPACE:25 load("@build_bazel_rules_nodejs//:index.bzl", "npm_install")
e2e/go/BUILD.bazel:2 load("@build_bazel_rules_nodejs//:index.bzl", "generated_file_test")
```

このうち `npm_install` が定義する `@npm` リポジトリは、**どの BUILD ターゲットからも
参照されていない**。`//e2e/go:snapshot_test` は Go のバイナリと JSON ファイルしか使わない。
つまり実質的な用途は `generated_file_test` ただ 1 つだった。

この依存はライブラリ更新の妨げにもなっていた。上げ先を順に見ると、行き止まりになっている。

- **4.7.0 / 5.8.5**: `generated_file_test` は存在する。ただし 5.8.5 の
`SUPPORTED_BAZEL_VERSIONS` は `["4.2.2", "5.0.0"]` で、現行の `.bazelversion` (3.7.0) を
含まない。つまり Bazel 側も同時に上げないと使えない。
- **6.x**: rules_nodejs が大きく整理され、ルート `index.bzl` ごと無くなった。
`generated_file_test` は現行版には存在しない。

したがって「rules_nodejs だけ上げる」は成立せず、最新まで上げるならどのみち比較の仕組みを
書き換えることになる。書き換えたうえで依存だけが残る。

なお、Bazel 自体の起動に使っている `npx bazelisk` は npm の devDependency であって
rules_nodejs とは無関係なので、この判断の影響を受けない。

## 決定

`generated_file_test` を、外部ルールに依存しない `sh_test` (`e2e/go/snapshot_test.sh`) に
置き換え、`build_bazel_rules_nodejs` を WORKSPACE から削除する。

スクリプトは生成された JSON とコミット済みスナップショットを `diff -u` で比較し、
差分があれば差分そのものを表示して失敗する。

## 理由

比較の中身は「2 つのファイルが同一か」でしかない。そのために外部リポジトリを 1 つ
丸ごと取得するのは釣り合っていない。`diff` は Bazel のバージョンにも依存しないので、
今後 Bazel を上げるときにこの部分が障害にならない。

`bazel_skylib` の `diff_test` に置き換える案もあったが、それは依存を別の依存に
入れ替えるだけで、しかも skylib のどのバージョンが Bazel 3.7.0 と組み合わせられるかを
この環境では検証できない (`releases.bazel.build` に到達できない)。検証できない選択肢を
2 つ抱えるより、依存を減らして 1 つに絞るほうがよい。

失敗時の出力はむしろ改善する。`generated_file_test` は不一致の事実を報告するだけだったが、
`diff -u` は何がどう違うかを出す。スナップショットの更新手順もスクリプトの
コメントに書いた。

## 検討した他の選択肢

- **rules_nodejs を 5.8.5 に上げる**: `generated_file_test` はまだあるが、
対応 Bazel が 4.2.2 / 5.0.0 なので Bazel も同時に上げる必要があり、変更が連鎖する。
- **rules_nodejs を 6.x に上げる**: `generated_file_test` が無いので、
どのみち比較の仕組みを書き換えることになる。書き換えたうえで依存が残るだけ損。
- **`bazel_skylib` の `diff_test` を使う**: 標準的で堅い。ただし上記のとおり、
依存を入れ替えるだけで減らず、バージョン組み合わせを検証できない。
- **`npm_install` だけ残す**: 参照するターゲットが無いので、残す理由が無い。

## 結果

- WORKSPACE の外部依存が 1 つ減り、Bazel のバージョンを上げるときの制約も 1 つ減る。
- `package.json` / `package-lock.json` は Bazel のビルドグラフから完全に外れた。
これらが影響するのは `npx bazelisk` の取得だけになる。
- スナップショットの更新は手作業になった (`bazel build //e2e/go:snapshot` の出力を
`fixtures/20201200.json` にコピー)。手順は `e2e/go/snapshot_test.sh` の冒頭にある。
あわせて Makefile の `json` ターゲットを削除した。存在しない `run` ターゲットに依存し、
実在しない import path (`.../go/helper`、実体は `e2e/go`) を叩く二重に壊れた状態で、
更新手段として機能していなかった。
- 旧 `generated_file_test` は `src` と `generated` の指定が逆で、付随する
`:snapshot_test.update` はスナップショットではなく生成物側を書き換えようとしていた。
比較そのものは対称なので検証は機能していたが、更新経路は元から使えなかった。
Loading
Loading