fn canonicalize_jcs_from_str(json: &str) -> Result<String, String> {
    // 1) まず JSON として解釈できることを保証（ここで構文エラーは落とす）
    // 2) RFC8785(JCS)で canonical 文字列へ
    let v: serde_json::Value = serde_json::from_str(json).map_err(|e| e.to_string())?;
    serde_jcs::to_string(&v).map_err(|e| format!("{e:?}"))
}

fn canonicalize_jcs_from_shape(shape: &ShapeNode) -> Result<String, String> {
    serde_jcs::to_string(shape).map_err(|e| format!("{e:?}"))
}

fn main() {
    // 1) StepNode
    let j1 = r#"
    {
      "op": "step",
      "path": "s3://lambda360/parts/PA-001-DF7.STEP",
      "content_hash": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
    }
    "#;

    // 2) TranslateNode
    let j2 = r#"
    {
      "op": "translate",
      "shape": {
        "op": "step",
        "path": "s3://lambda360/parts/PA-001-DF7.STEP",
        "content_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
      },
      "xyz": [10, 0, -5]
    }
    "#;

    // 3) SubtractNode（NumberOrExpr に文字列式も混ぜる）
    let j3 = r#"
    {
      "op": "subtract",
      "a": {
        "op": "union",
        "a": {
          "op": "step",
          "path": "s3://lambda360/parts/base.STEP",
          "content_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
        },
        "b": {
          "op": "translate",
          "shape": {
            "op": "step",
            "path": "s3://lambda360/parts/boss.STEP",
            "content_hash": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
          },
          "xyz": ["$dx", 0, 0]
        }
      },
      "b": {
        "op": "rotate",
        "shape": {
          "op": "step",
          "path": "s3://lambda360/parts/hole.STEP",
          "content_hash": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
        },
        "axis": [0, 0, 1],
        "deg": "$angleDeg"
      }
    }
    "#;

    let cases = [("case1_step", j1), ("case2_translate", j2), ("case3_subtract", j3)];

    for (name, old_json) in cases {
        println!("=== {name} ===");

        // before: 入力JSONをRFC8785(JCS)で正規化
        let old_canon = canonicalize_jcs_from_str(old_json)
            .unwrap_or_else(|e| panic!("canonicalize(before) failed: {e}"));

        // parse: ShapeNodeへデシリアライズ（ここで「パースが正しい」ことを検証）
        let shape: ShapeNode = serde_json::from_str(old_json)
            .unwrap_or_else(|e| panic!("parse ShapeNode failed: {e}"));

        // after: ShapeNodeをRFC8785(JCS)で正規化シリアライズ
        let new_canon = canonicalize_jcs_from_shape(&shape)
            .unwrap_or_else(|e| panic!("canonicalize(after) failed: {e}"));

        // “情報が変化していない”ことを canonical JSON 文字列で保証
        assert_eq!(
            old_canon, new_canon,
            "JCS mismatch: before != after\n--- before(JCS) ---\n{old_canon}\n--- after(JCS) ---\n{new_canon}\n"
        );

        println!("OK: JCS(before)==JCS(after)");
        // 必要ならダンプ
        // println!("{old_canon}");
    }

    println!("All cases passed.");
}
