# `json.rs` 設計書
*[English](json.en.md)*
`src/json.rs` に対応する設計書。[architecture.md](architecture.md) が定義するフェーズ5「JSON生成(返却)」を担う。分析・解決が完了した [`model::Workbook`](model/workbook.md) を、フロントエンド描画に必要な `row_span` / `col_span` などの属性を含むJSONへシリアライズする(要求仕様書5章)。
## 責務・スコープ
- [`model::Workbook`](model/workbook.md) を、`row_span`/`col_span` や値の種別タグを含むJSONへシリアライズする
- [`Sheet::iter_cells`](model/sheet.md)(起点セルのみを走査)が返すイテレータから1セルずつ直接シリアライザへ書き出し、シート全体分の中間 `Vec` をヒープ上に構築しない([PR #10 レビュー](https://github.com/MinamiyamaKotaro/xlsxparser/pull/10#pullrequestreview-4949223332)を反映してオープンクエスチョン5を解決。要求仕様書が主眼とする「方眼紙Excel」規模のシートに対するピークメモリ抑制)
- [`Sheet::merged_region_at`](model/sheet.md) を用いて `row_span`/`col_span` を算出し、結合セルの仮想セル座標をJSON出力へ含めない(要求仕様書3.2、5章の実装)
- [`CellValue`](model/cell.md) の各バリアントに応じて、JSON上の値と種別タグ(`type: "number" | "text" | "boolean" | "error" | "dateTime"`。値を持たない、または表現不能な値は `"empty"`)を出力する
- **含まない責務**: モデルデータの解決・検証そのもの(`resolve/`。本ファイルに到達する時点で `Workbook` は全フェーズの検証を通過済みの正常データのみを保持する)、`to_json_writer` に渡す `Write` 実装そのものの用意(ファイルオープン・HTTPレスポンスの確保等は呼び出し側の責務)
## 主要な型・関数(案)
```rust
use crate::error::Error;
use crate::model::cell::{Cell, CellRef, CellValue};
use crate::model::sheet::{Sheet, SheetVisibility};
use crate::model::workbook::Workbook;
use serde::ser::{SerializeSeq, SerializeStruct};
use serde::{Serialize, Serializer};
use std::io::Write;
/// `workbook` をJSONとして `writer` へストリーミング出力する。`cells` 配列の
/// 各要素は [`Sheet::iter_cells`](model/sheet.md) から都度1件ずつ変換・
/// 書き出され、シート全体分の `Vec<JsonCell>` を中間バッファとして保持しない。
/// `writer` が例えば `BufWriter<File>` であれば、追加メモリ使用量は
/// 処理中のセル1件分(O(1))に抑えられる。
pub fn to_json_writer<W: Write>(workbook: &Workbook, writer: W) -> Result<(), Error> {
let json_workbook = JsonWorkbook { workbook };
serde_json::to_writer(writer, &json_workbook)
.map_err(|source| Error::JsonSerialize { source: Box::new(source) })
}
/// `to_json_writer` をインメモリの `Vec<u8>` に対して呼び出す簡易版。
/// 出力全体を1つの `String` として保持する必要があるため、追加メモリ使用量は
/// 出力サイズに比例したO(n)となる(`to_json_writer` のO(1)とは異なる点に
/// 注意。ファイル・HTTPレスポンス等へ直接書き出せる場合は `to_json_writer`
/// を使うことを推奨する)。
pub fn to_json_string(workbook: &Workbook) -> Result<String, Error> {
let mut buf = Vec::new();
to_json_writer(workbook, &mut buf)?;
// serde_jsonは常に妥当なUTF-8を出力することが保証されているため、
// ここでの変換は理論上失敗しないが、ライブラリ内部で`unwrap`/`expect`を
// 使わない方針(error.mdエラー処理方針)に従い`Result`のまま扱う。
String::from_utf8(buf).map_err(|source| Error::JsonSerialize { source: Box::new(source) })
}
/// `model::Workbook` への借用ラッパー。値を所有せず、`Serialize` 実装内で
/// 都度モデルを走査することでストリーミングを実現する(このファイルの
/// 外へは公開しない)。
struct JsonWorkbook<'a> {
workbook: &'a Workbook,
}
impl<'a> Serialize for JsonWorkbook<'a> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut state = serializer.serialize_struct("Workbook", 1)?;
state.serialize_field("sheets", &SheetSeq { workbook: self.workbook })?;
state.end()
}
}
struct SheetSeq<'a> {
workbook: &'a Workbook,
}
impl<'a> Serialize for SheetSeq<'a> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let sheets = self.workbook.sheets();
let mut seq = serializer.serialize_seq(Some(sheets.len()))?;
for sheet in sheets {
seq.serialize_element(&JsonSheet { sheet })?;
}
seq.end()
}
}
struct JsonSheet<'a> {
sheet: &'a Sheet,
}
impl<'a> Serialize for JsonSheet<'a> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut state = serializer.serialize_struct("Sheet", 5)?;
state.serialize_field("name", &self.sheet.name)?;
state.serialize_field("visibility", visibility_tag(self.sheet.visibility))?;
state.serialize_field("maxRow", &self.sheet.max_row)?;
state.serialize_field("maxCol", &self.sheet.max_col)?;
state.serialize_field("cells", &CellSeq { sheet: self.sheet })?;
state.end()
}
}
/// [`Sheet::iter_cells`](model/sheet.md) から1セルずつ `JsonCell` へ変換し
/// つつ直接シリアライザへ書き出す。中間の `Vec<JsonCell>` を構築しない
/// (オープンクエスチョン5を解決した設計の中核)。
struct CellSeq<'a> {
sheet: &'a Sheet,
}
impl<'a> Serialize for CellSeq<'a> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
// Sheet::iter_cellsは`impl Iterator`としてのみ公開されておりサイズ
// ヒントを保証しないため`None`とする(`ExactSizeIterator`をpublic
// APIとして約束するかはmodel/sheet.md側の設計判断に委ねる。
// オープンクエスチョン5参照)。
let mut seq = serializer.serialize_seq(None)?;
for (cell_ref, cell) in self.sheet.iter_cells() {
seq.serialize_element(&cell_to_json(self.sheet, cell_ref, cell))?;
}
seq.end()
}
}
/// 1セル分の変換結果。`CellSeq::serialize` がストリームの1要素として
/// 都度生成する、短命な値(呼び出し元へは公開しない)。
#[derive(Debug, Serialize)]
struct JsonCell {
row: u32,
col: u32,
value: JsonCellValue,
/// 1(結合されていない)の場合はフィールド自体を省略する。
#[serde(rename = "rowSpan", skip_serializing_if = "is_one")]
row_span: u32,
#[serde(rename = "colSpan", skip_serializing_if = "is_one")]
col_span: u32,
// フォント・塗りつぶし等のスタイル出力は ResolvedStyle の拡張待ち
// (オープンクエスチョン4参照)。
}
fn is_one(n: &u32) -> bool {
*n == 1
}
/// 種別タグ付きの値表現。`#[serde(tag = "type", content = "value")]` により
/// `{"type": "number", "value": 42.0}` の形でシリアライズされる。
#[derive(Debug, Serialize)]
#[serde(tag = "type", content = "value", rename_all = "camelCase")]
enum JsonCellValue {
Number(f64),
/// 具体的な文字列表現(ISO 8601等)は model::DateTimeValue の型確定後に
/// 決定する(オープンクエスチョン3参照)。
DateTime(String),
Text(std::sync::Arc<str>),
Boolean(bool),
Error(String),
/// 値を持たない(書式のみの)セル、または非有限浮動小数点数
/// (後述)などJSONで表現不能な値のフォールバック先。
Empty,
}
fn cell_to_json(sheet: &Sheet, cell_ref: CellRef, cell: &Cell) -> JsonCell {
let (row_span, col_span) = sheet
.merged_region_at(cell_ref)
.map(|r| (r.row_span(), r.col_span()))
.unwrap_or((1, 1));
JsonCell {
row: cell_ref.row,
col: cell_ref.col,
value: cell_value_to_json(cell.value.as_ref()),
row_span,
col_span,
}
}
fn cell_value_to_json(value: Option<&CellValue>) -> JsonCellValue {
match value {
None => JsonCellValue::Empty,
Some(CellValue::Number(n)) if n.is_finite() => JsonCellValue::Number(*n),
// NaN/Infinityを0.0へ静かに置き換えると、下流の集計処理が
// 「正常に0と評価された」値と区別できず誤った集計結果を招きうる
// (会計・業務システム向けという要求仕様書1章の利用文脈を踏まえ、
// PR #10レビュー指摘を反映してオープンクエスチョン2を解決)。
// 値なし(Empty/JSON上のnull相当)へフォールバックし、フロント
// エンドが「値が存在しない」ものとして安全に扱えるようにする。
Some(CellValue::Number(_)) => JsonCellValue::Empty,
Some(CellValue::DateTime(dt)) => JsonCellValue::DateTime(format_date_time(dt)),
Some(CellValue::Text(s)) => JsonCellValue::Text(s.clone()),
Some(CellValue::Boolean(b)) => JsonCellValue::Boolean(*b),
Some(CellValue::Error(e)) => JsonCellValue::Error(e.clone()),
}
}
/// `model::DateTimeValue`(未確定、[model/cell.md オープンクエスチョン4](model/cell.md)参照)
/// を文字列表現へ変換する。具体的なフォーマットは同型の確定と合わせて決定する。
fn format_date_time(dt: &crate::model::cell::DateTimeValue) -> String {
let _ = dt;
unimplemented!()
}
fn visibility_tag(v: SheetVisibility) -> &'static str {
match v {
SheetVisibility::Visible => "visible",
SheetVisibility::Hidden => "hidden",
SheetVisibility::VeryHidden => "veryHidden",
}
}
```
## 依存関係
- 依存先: [`model/workbook.rs`](model/workbook.md)(`Workbook`)、[`model/sheet.rs`](model/sheet.md)(`Sheet::iter_cells`, `Sheet::merged_region_at`, `SheetVisibility`)、[`model/cell.rs`](model/cell.md)(`Cell`, `CellRef`, `CellValue`, `DateTimeValue`)、[`error.rs`](error.md)(`Error::JsonSerialize`。ストリーミング書き込み時のI/O・シリアライズ失敗を表現するため新設。[PR #10 レビュー](https://github.com/MinamiyamaKotaro/xlsxparser/pull/10#pullrequestreview-4949223332)を踏まえた設計変更に伴い追加)、外部クレート `serde`(`Serialize` の手動・導出実装)・`serde_json`(`to_writer` によるストリーミングシリアライズ)。`serde` は `rc` フィーチャの有効化が必要(実装時に判明: `CellValue::Text` の `Arc<str>` フィールドは、このフィーチャを有効にしないと `Serialize` を実装しない。serde は `Rc`/`Arc` のシリアライズを既定では無効にしており、これは共有データが独立したシリアライズ呼び出しごとに黙って複製されてしまう落とし穴を避けるための設計)。
- 依存元: `lib.rs`(`Workbook` から明示的に呼び出す。[pipeline.md オープンクエスチョン1](pipeline.md) 参照。`pipeline.rs` の `run` 自体からは呼ばれない)
`JsonWorkbook` / `SheetSeq` / `JsonSheet` / `CellSeq` はいずれもモデルへの借用(`&'a Workbook` / `&'a Sheet`)のみを保持し、値を所有しない。`Serialize` 実装は呼び出された時点で初めてモデルを走査するため、`serde_json::to_writer` が内部で行う逐次的なシリアライズ呼び出しと自然に噛み合い、シート全体・ブック全体を表す中間データ構造をヒープ上に一切構築しない。
`model::Workbook` / `Sheet` / `Cell` に直接 `#[derive(Serialize)]` を付与せず、本ファイル専用の借用ラッパー型へ変換してからシリアライズする設計とした。理由は [error.md](error.md) が `Error::XmlParse::source` を型消去して `quick-xml` をパブリック依存にしないようにした設計判断と同じ構造で、`model/` に `serde` への依存を持ち込むと `serde` の破壊的変更が `model/` 側の型定義に波及しうる。ラッパー層を挟むことで、`model/` は architecture.md の方針どおり「XMLパースや解決ロジックに依存しない純粋なデータ構造」のまま保たれ、JSON出力の具体的なフィールド名・形状を `model/` の型定義から独立して変更できる。
## エラー処理方針
- `to_json_writer` / `to_json_string` は `Result<_, Error>` を返す。`serde_json::to_writer` は非有限浮動小数点数(`NaN`/`Infinity`)を検知すると `Err` を返す仕様だが、本ファイルは `cell_value_to_json` の時点で非有限な `f64` を必ず `JsonCellValue::Empty` へ変換してから `serde_json` へ渡すため、この経路でのエラーは実質的に発生しない。それでも `Result` を返す設計としているのは、(1) `writer` が `File` やネットワークソケット等I/Oを伴う実装の場合、書き込み自体が失敗しうるため、(2) ライブラリ内部で `unwrap`/`expect` を使わない([error.md エラー処理方針](error.md))という既存方針を守るためである
- `serde_json::Error` は具体的な型を `Error` のフィールドへ直接置かず `Box<dyn std::error::Error + Send + Sync + 'static>` として型消去した新設バリアント `Error::JsonSerialize` へ包む。理由は [error.md](error.md) の `XmlParse::source` と同一で、`serde_json` をパブリック依存にしないため
- 唯一JSONが表現できない値である `f64` の `NaN`/`Infinity`(`CellValue::Number` が理論上保持しうる)は、`Err` を返して呼び出し元にドキュメント全体のシリアライズを諦めさせるのではなく、当該セルのみ `JsonCellValue::Empty`(JSON上の `null` 相当)へフォールバックし、処理を継続する。`0.0` のような有効な数値へ静かに置き換えないのは、会計・業務システム向けという要求仕様書1章の利用文脈において「計算失敗・未定義値」と「正常に評価されたゼロ」が下流の集計処理で区別できなくなることを避けるため([PR #10 レビュー](https://github.com/MinamiyamaKotaro/xlsxparser/pull/10#pullrequestreview-4949223332)を反映)
## テスト方針
- 単一シート・単一セル(数値)を持つ `Workbook` が `to_json_string` で期待するJSON文字列へ変換されることの確認
- 結合セルを持つシートで、起点セルの `rowSpan`/`colSpan` が正しく算出され、仮想セル座標が `cells` 配列に含まれないことの確認(`Sheet::iter_cells` が起点セルのみを返す設計との結線)
- 結合されていない通常セルで `rowSpan`/`colSpan` フィールドがシリアライズ結果から省略される(`skip_serializing_if`)ことの確認
- `CellValue` の各バリアント(`Number`/`Text`/`Boolean`/`Error`)が対応する `type` タグと `value` で正しくシリアライズされることの確認
- `value: None`(書式のみのセル)が `type: "empty"` としてシリアライズされることの確認
- **`CellValue::Number(f64::NAN)` / `CellValue::Number(f64::INFINITY)` を持つセルが `Err` を返さず `type: "empty"` として出力されることの確認**(PR #10 レビューで追加したフォールバック仕様の回帰テスト観点。従来の `0.0` へのフォールバックとの違いを明示するテスト)
- 可視性が `Hidden`/`VeryHidden` のシートを含む `Workbook` で、全シートが `visibility` フィールド付きで出力に含まれることの確認([model/workbook.md](model/workbook.md) の「非表示シートも全て含める」方針との結線)
- 0シートの `Workbook` が `{"sheets": []}` として正しくシリアライズされることの確認
- **多数のセルを持つシートに対し `to_json_writer` を呼び出した際、`Sheet` 自体のメモリ使用量に対して追加のヒープ確保が有意に増加しない(`JsonCell` のVec化が行われていない)ことを検証する回帰テスト**(PR #10 レビューで指摘されたピークメモリ抑制の設計意図を裏付けるテスト。具体的な検証手法はメモリプロファイリングツールの選定と合わせて実装時に確定させる)
- `to_json_writer` に書き込み途中で失敗する `Write` 実装(テスト用のモック)を渡した場合に `Error::JsonSerialize` が伝播することの確認
## 未決事項 / オープンクエスチョン
1. ~~JSON構造における値の種別タグ付けの是非~~ → **解決**: `{"type": "number", "value": 42}` のようなタグ付き表現を維持する([PR #10 レビュー](https://github.com/MinamiyamaKotaro/xlsxparser/pull/10#pullrequestreview-4949223332)を反映)。タグを外してネイティブなJSON型のみで出力すると、`dateTime` と単なる文字列(`Text`)をフロントエンド側が区別できず日付ピッカー等の適用に文字列解析が必要になること、`error`(数式エラー値)を通常の文字列と区別してグリッド上で警告表示するといった制御ができなくなること、TypeScript側でタグ付きユニオン型(Discriminated Union)による型安全なクライアント実装ができなくなることが理由。
2. ~~非有限浮動小数点数(`NaN`/`Infinity`)のフォールバック値~~ → **解決**: `0.0` ではなく `JsonCellValue::Empty`(`null` 相当)へフォールバックする([PR #10 レビュー](https://github.com/MinamiyamaKotaro/xlsxparser/pull/10#pullrequestreview-4949223332)を反映)。詳細はエラー処理方針参照。
3. **`DateTime` の文字列表現形式**: [model/cell.md オープンクエスチョン4](model/cell.md) の `DateTimeValue` 型確定と連動して未確定。ISO 8601(例: `"2024-01-01T00:00:00"`)を軸に検討するが、日付のみ・時刻のみのセルの扱い(Excelは日付/時刻の精度を型として区別しない)は要検討。**実装時の暫定対応**: `DateTimeValue` は現時点でも[model/cell.md](model/cell.md)通りデータを持たないプレースホルダーのユニット構造体のままであり、上記ドラフトの `format_date_time` は実データを持たないため実際の値を生成できない。これを `unimplemented!()` のまま残すと、`resolve/style.rs` の `serial_to_date_time` が範囲内の数値セルに対して既に実際に(中身は空だが)`DateTimeValue` を返している以上、解決済みの日付/時刻セルを含む `Workbook` を `to_json_writer` に渡した瞬間にpanicしてしまい、「信頼できない入力に到達しうるコードではpanicしない」というクレート全体の方針に反する。そのため `format_date_time` は削除し、`cell_value_to_json` の `CellValue::DateTime(_)` 分岐は、表現不能な `Number` と同様に `JsonCellValue::Empty` へフォールバックするようにした(実データに基づかない文字列を捏造しない)。`JsonCellValue::DateTime(String)` バリアント自体は残してあり(変換経路からは構築されないが、シリアライズ形状はテスト `date_time_value_currently_serializes_as_empty` で固定している)、`DateTimeValue` が実データを持つようになった際にすぐ配線できるようにしている。
4. **スタイル情報のJSON出力**: [model/style.md オープンクエスチョン1](model/style.md) と同一の論点。`ResolvedStyle` が具体的なフォント/塗りつぶし/罫線フィールドを持つまで `JsonCell` にスタイル出力フィールドを追加できない。
5. ~~一括構築によるピークメモリの抑制~~ → **解決**: `Vec<JsonCell>` を事前構築せず、`Sheet::iter_cells` から得たイテレータを `CellSeq::serialize` 内で直接 `serde::ser::SerializeSeq` へ流し込むストリーミング設計に変更した([PR #10 レビュー](https://github.com/MinamiyamaKotaro/xlsxparser/pull/10#pullrequestreview-4949223332)を反映)。ただし `to_json_string`(内部で `Vec<u8>` バッファを使う簡易版)自体は出力サイズに比例したO(n)のメモリを要する点は変わらない。真にO(1)の追加メモリで完結させたい呼び出し元は `to_json_writer` に `BufWriter<File>` 等の実際のI/O先を渡す必要がある。また `Sheet::iter_cells` が `ExactSizeIterator` を保証しないため `serialize_seq` の要素数ヒントを `None` としている点(JSON出力自体の正しさには影響しないが、一部のシリアライザ実装で軽微な最適化機会を逃す)は、[model/sheet.md](model/sheet.md) 側で `ExactSizeIterator` を公開APIとして約束するかどうかの検討課題として残る。
6. **`to_json_writer`/`to_json_string` と `lib.rs` の公開APIとの関係**: `Workbook` を返す `parse_workbook` とは別に、本関数群を `lib.rs` がどう公開するかは [pipeline.md オープンクエスチョン1](pipeline.md) と連動し、`lib.rs` の設計時に確定させる。