AIと一緒に仕様駆動開発をやってみた話

はじめに

「AIにコードを書かせる」のはもう当たり前になってきましたが、今回試したのは少し違うアプローチです。コードを書かせる前に、AIと一緒に仕様を固めるというものです。

Claude Code を使って RSS フィードの自動翻訳・要約システムを作りながら、「ブレインストーミング → 設計ドキュメント → 実装計画 → サブエージェント実装」という一連のサイクルを体験しました。

今回の一番の成果物は、実はシステム本体ではありません。このサイクルを自動的に推し進めてくれる CLAUDE.md が育ったことです。


何を作ったか

rss-feed-translator という、海外 IT ニュースを毎日自動で日本語に翻訳・要約して RSS フィードとして配信するシステムです。

  • 対象フィード: Ars Technica・Hacker News・DEV Community・InfoQ
  • 翻訳: Google Translate REST API(1日15,000文字上限)
  • AI 要約: Claude Haiku(Amazon Bedrock)
  • インフラ: GitHub Actions + GitHub Pages だけ。サーバーなし

フィードリーダー(Feedly など)で購読すると、英語記事が日本語タイトル+AI要約つきで流れてくる、というものです。


開発の流れ

1. ブレインストーミング

まず AI と対話しながら要件を固めます。AI が一問一答で質問してくる感じです。

Q: 翻訳するのはタイトルだけ?本文も?
Q: コスト上限はどう管理する?
Q: 差分更新(キャッシュ)は必要?

面白いのが、設計が承認されるまで AI がコードを書かないという制約です。「とりあえず実装してみよう」という衝動を AI 側で抑えてくれます。

2. 設計ドキュメント(spec)

ブレインストーミングの結果を Markdown に出力します。アーキテクチャ図(Mermaid)、関数シグネチャ、エラーハンドリング方針、テスト方針まで全部入った 44KB のドキュメントができました。

AI のコンテキストがリセットされても、このドキュメントを読めば「なぜこうなっているか」がすぐわかります。

3. 実装計画(plan)

設計ドキュメントからタスク単位の実装計画を生成します。各タスクには実際のテストコードとコマンドが入っていて、プレースホルダー(「TODO: 実装する」みたいなやつ)は禁止です。

4. サブエージェント実装

各タスクに対して:

  1. 実装エージェントを白紙の状態で起動
  2. 実装完了後、レビューエージェントが仕様準拠+コード品質を審査
  3. 問題があれば修正エージェントを起動して再レビュー
  4. 全タスク完了後、最終レビューエージェントがブランチ全体を審査

このフローで今回自動検出されたのが:

  • javascript: スキーム URL によるXSS脆弱性(URL サニタイズ漏れ)
  • テストのアサーションが不完全な箇所
  • GitHub Actions の git add --ignore-unmatch 誤用(これは git rm 専用オプションでした)

機能追加もすべて同じサイクルで

初回リリース後も、機能追加のたびに同じサイクルを回しました。

DailyBudget(翻訳コスト管理)

Google Translate は従量課金なので、1日あたりの翻訳文字数に上限を設ける DailyBudget クラスを追加しました。これも独立した「ブレスト → spec → plan → 実装」サイクルで進め、設計ドキュメント(docs/superpowers/specs/2026-06-30-daily-char-budget.md)が残っています。

途中で Google Cloud SDK から REST API 直接呼び出しに切り替えるという判断もありましたが、その理由(依存を減らすため)も設計ドキュメントに記録されています。

InfoQ フィードの追加

当初3フィードだったところに InfoQ を追加しました。フィード追加は設定ファイルの変更だけで済むように設計されていたので、ここはコード変更なしで完了しました。

翻訳・要約レビューレポート

「翻訳・要約の品質を目視確認したい」という気持ちが出てきたので、HTML レビューレポート機能を追加しました。GitHub Pages に自動公開され、最新10件の各記事について「原文タイトル・翻訳タイトル・原文説明・翻訳説明・AI要約」を並べて確認できます。

これも同じサイクルで進め、実装計画(docs/superpowers/plans/2026-07-01-translation-review-report.md)が残っています。


今回の最大の成果物:CLAUDE.md

システムが動くようになって気づいたのですが、一番価値があったのはコードではなく CLAUDE.md の中身が育ったことでした。

CLAUDE.md の土台

CLAUDE.md は Claude Code がプロジェクトを開くたびに読み込む、いわば「AIへの指示書」です。

今回のベースにしたのは、西見 公宏さんの 『実践 Claude Code 入門―現場で活用するためのAIコーディングの思考法』 で紹介されている CLAUDE.md の構成です。そこに Superpowers(Claude Code 向けのスキルプラグイン集)を組み合わせることで、仕様駆動開発のフローを AI が自律的に推し進めてくれる構成になっています。

📖 実践 Claude Code 入門 (Amazon) — 西見 公宏 著

  • 『実践 Claude Code 入門』 → 開発プロセス・ドキュメント管理・チェックリストの骨格
  • Superpowersbrainstorming / writing-plans / subagent-driven-development などのスキルを呼び出す仕組み

実際に書かれているルールの例

## 開発プロセス

1. 設計 — brainstorming で設計し docs/superpowers/specs/ に保存
2. 実装計画 — writing-plans で計画を作成し docs/superpowers/plans/ に保存
3. 実装 — subagent-driven-development でタスクを順に実装
4. 品質チェック — pytest / ruff / mypy
## ドキュメント記載ルール

コード変更と同じコミットで設計ドキュメント・用語定義・READMEを更新する。後回し禁止。

このチェックリストのおかげで「実装したけどドキュメントが古いまま」という状態が自然と防止されます。

CLAUDE.md 自体が何度も更新された

面白いのは、CLAUDE.md 自体もプロジェクトを通じてどんどん更新されたことです。コミット履歴を見ると、こんな変更が積み重なっています。

docs(claude): require doc updates in same commit as code changes
docs(claude): add guideline for grouping table of contents
docs(claude): add guideline for grouped section headings in design docs
docs(claude): add checklist items for doc grouping and section hierarchy
docs(claude): add tech stack, dev tools, and constraints to spec checklist
docs(claude): add styling conventions and individual checklist items
docs(claude): restructure and clean up CLAUDE.md
docs(claude): update README checklist to reflect FastAPI-style conventions

「ドキュメントの目次はグループ見出しでまとめる」「コード変更と同じコミットでドキュメントも更新する(後回し禁止)」「README は FastAPI スタイルで書く」……こうしたルールが、実際に困った瞬間に一つずつ追記されています。

README もこうして育った

README も開発を通じて段階的に洗練されました。

  1. 最初はシンプルなリンク集
  2. CI/CD バッジと Python バージョンバッジを追加
  3. SVG ロゴを作成して中央揃えで配置
  4. FastAPI スタイル(ロゴ → キャッチコピー → バッジ → 特長一覧)に全面刷新

各段階の判断が CLAUDE.md に蓄積されているので、次のプロジェクトでも同じ品質の README を最初から作れます。

CLAUDE.md に書くときの観点

「今後誰かが読むかもしれない」ではなく、「この AI が次のセッションで同じ判断を再現できるか」 という観点で書きます。それが自然と、実用的で具体的な記述になります。


まとめ

AI 仕様駆動開発を体験してみて思ったのは、「AI が変えたのはドキュメントを書くコスト」だということです。

仕様が正確なら AI は正確に実装して正確にレビューします。でも以前は「個人プロジェクトにそこまで丁寧な spec は書かないよ」となりがちでした。AI がその作成コストを下げたことで、仕様駆動開発の恩恵を個人プロジェクトでも受けられるようになりました。

土台として『実践 Claude Code 入門』の CLAUDE.md と Superpowers のスキルがあったことで、ゼロから試行錯誤しなくてよかったのも大きかったです。この組み合わせはかなりおすすめです。

そして CLAUDE.md が育つほど、次のプロジェクトでも同じ品質のフローが再現できます。コードは使い捨てになりますが、CLAUDE.md に蓄積されたルールは資産として残ります。開発のたびに AI との協働プロトコルが洗練されていく感覚、これが今回一番おもしろかったことです。


今後

今回、作成したCLAUDE.md がどれくらい機能するか検証し、ひきつづき、CLAUDE.md を育てていきたいです。 Superpowersと競合するgithubのSpec Kitも評価したい。 今回、Superpowersが生成した仕様書を正式な成果物として残すことを意識してやりましたが、そういった点では、Spec Kitの方が向いているようです。ちなみに、Superpowersが生成したドキュメントがサブエージェントの関係で分割できず、長大な内容なってしまったことが残念に感じています。


リポジトリ: rss-feed-translator

参考: - 📖 実践 Claude Code 入門 — 西見 公宏 著(技術評論社) - 🐙 Superpowers for Claude Code

AWS Lambda (Python 3.12) で cryptography の ImportError にハマった話と解決策

zappa frameworkを使用したアプリを MacOS でパッケージして、AWS Lambda にデプロイしようとしました。

デプロイ自体は成功しましたが、実行時に ImportError: PyO3 modules compiled for CPython 3.8 or older may only be initialized once per interpreter process というエラーが発生し、アプリが起動しませんでした。(はじめは、NoneType object is not callable としか出てなかったが、根本原因はこのimportErrorでした。)

この原因は、cryptography のバイナリが Lambda と互換性がなかったことでした。

具体的には、 cryptography は ネイティブ 拡張を含んでおり、ビルドを実行した環境向けのネイティブ拡張を取得していたようです。端的にいうと、M2(arm)向けのcryptographyが取得されていました。一方、AWS Lambda は、x86 Linuxでした。

さいわい、CI環境が、x86 Linuxだっため、CI環境でビルドすることで解決しました。

今後、Lambda (Python) でネイティブ拡張を含むライブラリ (cryptography, numpy, pandas 等) を使うときは、ビルド環境と実行環境を合わせることが鉄則です。

OracleのDockerコンテナ初回起動が遅すぎる問題とその対処法

OracleのDockerコンテナの初回起動が非常に遅いという問題は、すでに広く知られています。

この遅さの原因は、コンテナの初回作成時にOracleの初期セットアップ処理が実行されるためです。

一度この初期セットアップを済ませてしまえば、以降の起動はそこまで遅くはなりません。

ただし、「コンテナの削除・再作成を頻繁に行う場合」は話が変わってきます。

たとえば、初回に、自動でDB定義(スキーマ)の流し込みをしており、開発初期などでDB定義が頻繁に変わる場合や、CI環境で毎回初期状態のDBを立ち上げるようなケースでは、この初回の遅延が大きなストレスになります。

この「初回起動が非常に遅い」という問題には、すでにワークアラウンドがあり、それがpre-builtイメージの利用です。

参考:

kagamihoge.hatenablog.com

ただし、pre-builtでも課題があった

実際にpre-builtしたイメージを試してみたところ、DB定義(スキーマ)の流し込みが実行されないという問題に直面しました。

なぜ実行されないのかというと、スクリプトのフックがOracleの初期セットアップ処理(runOracle.sh)の中で呼び出されており、この処理自体がpre-builtイメージではスキップされてしまうためです。

自前でスクリプトを流し込む方法

この問題に対して、私は自前でDB定義(スキーマ)の流し込む処理を追加することにしました。

以下がその構成です。

Dockerfile

FROM ***/oracledb-pre-built:1.0.0

USER root
COPY entrypoint.sh /opt/oracle/entrypoint.sh
RUN chmod +x /opt/oracle/entrypoint.sh
USER oracle

ENTRYPOINT ["/opt/oracle/entrypoint.sh"]

entrypoint.sh

#!/bin/bash
set -e

INIT_MARKER="/opt/oracle/oradata/.init_done"
HEALTH_MARKER="/opt/oracle/oradata/.healthy"

# Oracle起動
bash /opt/oracle/runOracle.sh --nowait

if [ ! -f "$INIT_MARKER" ]; then
  echo "🚀 初回セットアップを実行します..."
  bash /opt/oracle/runUserScripts.sh /opt/oracle/scripts/setup
  touch "$INIT_MARKER"
  echo "✅ 初回セットアップ完了"
else
  echo "🔁 すでに初期化済みのため、セットアップをスキップします。"
fi

touch "$HEALTH_MARKER"

# ログをtailしてコンテナを保持
echo "以下、alert.logのtail出力:"
tail -f "$ORACLE_BASE"/diag/rdbms/*/*/trace/alert*.log &
childPID=$!
wait $childPID

docker-compose.yaml

services:
  db:
    container_name: db
    build:
      context: ./
      dockerfile: Dockerfile
    ports:
      - "${DOCKER_DB_PORT}:1521"
      - "5500:5500"
    environment:
      ORACLE_SID: ${DOCKER_DB_SID}
      ORACLE_PDB: ${DOCKER_DB_PDB}
      ORACLE_PWD: ${DOCKER_DB_PASSWORD}
      ORACLE_CHARACTERSET: JA16SJIS
      ORACLE_USER: ${DOCKER_DB_USER}
    volumes:
      - ./schema:/opt/oracle/scripts/setup
    healthcheck:
      test: [
        "CMD-SHELL",
        "test -f /opt/oracle/oradata/.healthy && /opt/oracle/checkDBStatus.sh"
      ]
      interval: 2s

MinecraftのBedrock 版サーバーをAWSに建ててみた

息子がSwitchで、Minecraftを始めたので、私もiphoneでMinecraftを、始めてみました。

息子とマルチプレーをしたいのですが、Minecraftが提供するRealmsという有料のサーバーを借りる必要があります。 しかも、Switch版でオンラインゲームをするには、Nintendo Switch Onlineというサブスクを購入する必要があり、Minecraft Realmsとともに、二重課金されるわけです。

さいわいなことに、Minecraftには、サーバーアプリケーションが無償で公開されています。 そこで、Minecraft Realmsの分については、AWSに自前のサーバーを立てることで節約することにしました。

とはいえ、AWSもただではありません。 Minecraft Realmsの料金より高くなっては、意味がない。どう料金を抑えるかがポイントです。

ECS上に、安いスポットインスタンスを立て、EBSにデータを保存しておいて、遊んでないときはECSタスクを停止するという構成を考えました。 そして、私が、githubを徘徊し、ついにたどり着いた解が、このCloudformationテンプレートです。

https://github.com/vatertime/minecraft-spot-pricing

しかし、これは、java版のMinecraftでした。java版は、PCでしか遊べないエディションで、SwitchやiOSは、Bedrock版というクロスプラットフォーム向けのエディションでサーバを立てる必要があります。

私は、Bedrock版サーバーのdockerイメージを見つけて、

https://hub.docker.com/r/itzg/minecraft-bedrock-server

このCloudformationテンプレートをBedrock版に書き換えることに成功しました。

それがこれです。

GitHub - ryu1/minecraft-spot-pricing: Cloudformation template to deploy a Minecraft server

ちなみに、Switchから自前のサーバに接続するには、DNSの設定を書き換えるハックが必要です。

www.radical-dreamer.com

Filter pattern in awslogs contains single quotes?

シングルクオートを含む文字列、例えば、'sample'で、CloudWatch Logsを検索したいとき、フィルターパターンとして、"'sample'"を指定すると...

awslogs get ${LOG_GROUP} ALL --start ${START} --end ${END} -f "'sample'"

Oops! エラーになります。

Traceback (most recent call last):
  File "/opt/homebrew/bin/awslogs", line 8, in <module>
    sys.exit(main())
             ^^^^^^
  File "/opt/homebrew/Cellar/awslogs/0.15.0_1/libexec/lib/python3.12/site-packages/awslogs/bin.py", line 210, in main
    getattr(logs, options.func)()
  File "/opt/homebrew/Cellar/awslogs/0.15.0_1/libexec/lib/python3.12/site-packages/awslogs/core.py", line 219, in list_logs
    consumer()
  File "/opt/homebrew/Cellar/awslogs/0.15.0_1/libexec/lib/python3.12/site-packages/awslogs/core.py", line 172, in consumer
    for event in generator():
  File "/opt/homebrew/Cellar/awslogs/0.15.0_1/libexec/lib/python3.12/site-packages/awslogs/core.py", line 159, in generator
    response = self.client.filter_log_events(**kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/awslogs/0.15.0_1/libexec/lib/python3.12/site-packages/botocore/client.py", line 565, in _api_call
    return self._make_api_call(operation_name, kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/awslogs/0.15.0_1/libexec/lib/python3.12/site-packages/botocore/client.py", line 1021, in _make_api_call
    raise error_class(parsed_response, operation_name)
botocore.errorfactory.InvalidParameterException: An error occurred (InvalidParameterException) when calling the FilterLogEvents operation: Invalid character(s) in term '''

\で、エスケープして、"\'sample\'"としても、

Oops! エラーになります。

その場合、%で囲んで正規表現とした上で、シングルクオートを文字コード\x27で指定するとよいです。

awslogs get ${LOG_GROUP} ALL --start ${START} --end ${END} -f "%\x27sample\x27%"

EC2にセッションマネージャー経由で接続する方法 〜CLI編〜

~/.ssh/configの設定は不要です。

ssh \
  -i 秘密鍵のパス \
  -o ProxyCommand="aws ssm start-session \
    --target %h --document-name AWS-StartSSHSession \
    --parameters 'portNumber=%p' \
    --profile AWSプロファイル名" \
   ec2-user@インスタンスID

scpもいけます。

scp \
  -i 秘密鍵のパス \
  -o ProxyCommand="aws ssm start-session \
    --target %h --document-name AWS-StartSSHSession \
    --parameters 'portNumber=%p' \
    --profile AWSプロファイル名" \
   アップロードファイル ec2-user@インスタンスID:アップロードファイル

Reactによる確認ダイアログの実装サンプル

確認ダイアログのReactコンポーネント meets kintone

import React, { FC, useCallback, useState } from "react";
import { Dialog } from "@kintone/kintone-ui-component";
import { Button } from "../Button";

type EventHandler = (e: React.SyntheticEvent<EventTarget>) => void;

// https://kintone-labs.github.io/kintone-ui-component/latest/Reference/Dialog/
interface ConfirmDialogProps {
  isVisible: boolean;
  title: string;
  content: string;
  close: () => void;
  onClickOkButton: EventHandler;
}

const okButtonStyle: React.CSSProperties = {
  color: "white",
  background: "#e74c3c",
};

const createButtons = (
  onClickOkButton: EventHandler,
  onClickCancelButton: EventHandler
) => {
  return (
    <>
      <Button label="キャンセル" onClick={onClickCancelButton} />
      <Button label="OK" onClick={onClickOkButton} style={okButtonStyle} />
    </>
  );
};

export const ConfirmDialog: FC<ConfirmDialogProps> = ({
  isVisible,
  title,
  content,
  close,
  onClickOkButton,
}) => {
  const onClickOkButtonWrapper: EventHandler = (e) => {
    console.log("dialog.onClickOkButton");
    onClickOkButton(e);
    close();
  };

  const onClickCancelButton: EventHandler = () => {
    console.log("dialog.onClickCancelButton");
    close();
  };

  const buttons = createButtons(onClickOkButtonWrapper, onClickCancelButton);

  return (
    <Dialog
      showCloseButton={false}
      header={title}
      content={content}
      footer={buttons}
      isVisible={isVisible}
    />
  );
};

export const useConfirmDialogOpener = (): [boolean, () => void, () => void] => {
  const [currentVisible, setVisible] = useState(false);

  const open = useCallback(() => {
    setVisible(true);
  }, []);

  const close = useCallback(() => {
    setVisible(false);
  }, []);

  return [currentVisible, open, close];
};

使い方

export const App = () => {

  const onClickOkButton = () => {
    // implement deleting
  };

  const [isVisible, open, close] = useConfirmDialogOpener();

  return (
    <div style={{ display: "flex" }}>
      <Button label="削除" onClick={open} type="submit" />
      <ConfirmDialog
        title="削除"
        content="削除します。よろしいですか?"
        isVisible={isVisible}
        close={close}
        onClickOkButton={onClickOkButton}
      />
    </div>
  );
};

playwright on container imageをAWS Lambdaで動かす

dockerコンテナ上のアプリをAWS Lambda上で呼び出すには、awslambdaricというのが必要なようです。 dockerイメージは以下のようになります。

# Define custom function directory
ARG FUNCTION_DIR="/function"

FROM --platform=linux/amd64 python:3.10-slim-bullseye
ENV TZ="Asia/Tokyo"

# Include global arg in this stage of the build
ARG FUNCTION_DIR

# Install aws-lambda-cpp build dependencies
RUN apt-get update -y && \
  apt-get install -y \
  g++ \
  make \
  cmake \
  unzip \
  libcurl4-openssl-dev \
  oathtool

# Copy function code
RUN mkdir -p ${FUNCTION_DIR}
WORKDIR ${FUNCTION_DIR}

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app.py .

# Install the function's dependencies
RUN pip install \
    --target ${FUNCTION_DIR} \
        awslambdaric

ENV PLAYWRIGHT_BROWSERS_PATH=0

RUN python -m playwright install --with-deps chromium

ENTRYPOINT [ "python", "-m", "awslambdaric" ]
CMD [ "app.lambda_handler" ]

なぜ、わざわざ、dockerイメージを使用しているかって? それは、oathtoolコマンドを使用してOTPを生成し、多要素認証を突破したかったからです。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import asyncio
from playwright.async_api import async_playwright
import subprocess

URL = ""
MAILADDRESS = ""
PASSWORD = ""
SECRET = ""

async def main():
    async with async_playwright() as p:
        try:
            browser = await p.chromium.launch(
                args=[
                    '--autoplay-policy=user-gesture-required',
                    '--disable-background-networking',
                    '--disable-background-timer-throttling',
                    '--disable-backgrounding-occluded-windows',
                    '--disable-breakpad',
                    '--disable-client-side-phishing-detection',
                    '--disable-component-update',
                    '--disable-default-apps',
                    '--disable-dev-shm-usage',
                    '--disable-domain-reliability',
                    '--disable-extensions',
                    '--disable-features=AudioServiceOutOfProcess',
                    '--disable-hang-monitor',
                    '--disable-ipc-flooding-protection',
                    '--disable-notifications',
                    '--disable-offer-store-unmasked-wallet-cards',
                    '--disable-popup-blocking',
                    '--disable-print-preview',
                    '--disable-prompt-on-repost',
                    '--disable-renderer-backgrounding',
                    '--disable-setuid-sandbox',
                    '--disable-speech-api',
                    '--disable-sync',
                    '--disk-cache-size=33554432',
                    '--hide-scrollbars',
                    '--ignore-gpu-blacklist',
                    '--metrics-recording-only',
                    '--mute-audio',
                    '--no-default-browser-check',
                    '--no-first-run',
                    '--no-pings',
                    '--no-sandbox',
                    '--no-zygote',
                    '--password-store=basic',
                    '--use-gl=swiftshader',
                    '--use-mock-keychain',
                    '--disable-gpu',
                    '--single-process',
                    '--headless=new'
                ],
                slow_mo=1000)

            context = await browser.new_context()
            page = await context.new_page()
            await page.goto(URL)

            async with page.expect_navigation():
                # ログイン
                await page.type("input[name=loginfmt]", MAILADDRESS)
                await page.click("input[type=submit]")

                await page.type("input[name=username]", MAILADDRESS)
                await page.click("button[type=submit]")


                await page.type("input[name=password]", PASSWORD)
                await page.click("button[type=submit]")

                token = await generate_token(SECRET)

                await page.type("input[id=security-code]", token)
                await page.click("button[type=submit]")
                await page.wait_for_timeout(10000)

                # 以下は省略

        finally:
            await browser.close()


async def generate_token(secret: str) -> str:
    cmd = ['oathtool', '--totp', '--base32', secret]
    out = subprocess.run(cmd, stdout=subprocess.PIPE)
    fa_string = out.stdout.decode()
    # 取得する文字列の最後に改行があるため、6文字で切る処理
    string_2fa = fa_string[:6]
    return string_2fa



def lambda_handler(event, context):
    asyncio.run(main())


if __name__ == "__main__":
    pass

特殊な環境だったため、chromiumが、うまく動かなくて非常にはまりました。 chromiumは、複数プロセスを立ち上げるので、それがどうも相性がわるかったようです。 シングルプロセスで起動するオプションをchromium起動時に指定する必要があります。 また、--disable-gpuも付与しないとクラッシュします。

Anker PowerConf H700

去年、このヘッドセットを購入して、半年程使用しました。

Bluetooth接続できて、かつ、マイクが口元にあり、マイクにもノイズ除去機能があるものを探して、これに辿り着いた次第でした。

ところが、騒がしいところでも、周囲の音を拾わないことを期待したわけでしたが、近くの声や音は拾ってしまうということが、MT相手の反応からわかりました。

いや、求めていたレベルが大きすぎただけで、悪い製品ではないんですがね。

MTは、やはり静かなところでやるに、限りますね。

Enumの名称は単数形か複数系か

learn.microsoft.com

ほとんどの Enum 型には単数名を使用しますが、ビット フィールドのEnum 型には複数名を使用するのがよいそうです。

Javaは、Enumで、ビットフィールドを表現できない(かわりにEnumSetを用いる)ので、単数が当たり前で、 他の言語を使用すると、複数形のEnumが出てきて、長年の疑問でしたが、なるほど、腑に落ちました。

Django(DRF)のレスポンスにcodeを含める

DRFのレスポンスは、通常は、メッセージが表示されるだけ。

{
   "param1": [
     "この項目は必須です。"
   ] 
}

APIの例外クラスを見ると、エラーコード(code)が指定されることがわかります。 このcodeをレスポンスに含めたい場合、どうするか。

class APIException(Exception):
    """
    Base class for REST framework exceptions.
    Subclasses should provide `.status_code` and `.default_detail` properties.
    """
    status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
    default_detail = _('A server error occurred.')
    default_code = 'error'

    def __init__(self, detail=None, code=None):
        if detail is None:
            detail = self.default_detail
        if code is None:
            code = self.default_code

以下のように、EXCEPTION_HANDLERをカスタマイズし、exc.get_full_details()の結果をレスポンスにセットする。

/PATH/TO/xxx/utils.py

from rest_framework.exceptions import APIException
from rest_framework.response import Response
from rest_framework.views import exception_handler


def custom_exception_handler(exc, context):
    response = exception_handler(exc, context)
    if response is not None:
        if isinstance(exc, APIException):
            data = exc.get_full_details()
            response = Response(data, status=exc.status_code, headers=response.headers)
    return response

/PATH/TO/settings/base.py

REST_FRAMEWORK = {
     "EXCEPTION_HANDLER": "xxx.utils.custom_exception_handler",
 }

そうすると、こういうレスポンスが返ります。

{
   "param1": [
     {"message": "この項目は必須です。", "code": "required"}
   ] 
}

ステータスコードだけだと、多様なエラーのハンドリングが難しい場合に、試してみたいです。

キャメルケースにおける略語の表現

HTML(HyperText Markup Language)、XML(Extensible Markup Language)などの略語は、通常、全て大文字で表現しますが、クラス名などのキャメルケースでは、どうするか、いつも悩ましい、私です。

qiita.com

JAVAは、どうもHtmlというふうに、略語も一つの単語扱いするようですね。 潔くていいです。

stackoverflow.com

Pythonは、HTMLというふうに、全て大文字にするようです。

このパターンは、変数にしたときに、接頭に略語がくると、xMLParserとかになるのかっ、変なの。 ってなりますが、Pythonはスネークケースだから関係ないですね。

idも、userIDやuserIdという表現が、あって悩ましいですね。

Oracle JDK8 Install in github action

github actionで、JDKをインストールする場合は、以下のgithub actionが提供されていますが、 難点は、OracleJDKはサポートされていないことです。

github.com

そういった場合、Oracleからwgetしてインストールするgithub actionを書くわけですが、今回、私がインストールしようしたJDKのバージョンは、Oracleからコマンドラインで、ダウンロードできなくなってしまったようです。

wget to download Java 8 · GitHub

↑こちらで議論されていますが、今回インストールしようとしていた1.8.0_191については、どれもうまくいきませんでした。

苦肉の策として、JDKGoogle Driveに配置し、共有リンクからダウンロードすることにしました。

github.com

      - name: Setup java
        run: |
          # Store jdk.tar.gz in google drive and get sharing link.
          # Set <ID> contained in the link (https://drive.google.com/file/d/<ID>/view?usp=sharing) to ID variable.
          ID=""
          FILE="jdk-8u191-linux-x64.tar.gz"
          CONFIRM=$(wget --quiet --save-cookies /tmp/cookies.txt --keep-session-cookies --no-check-certificate "https://drive.google.com/uc?export=download&id=$ID" -O- | sed -En 's/.*confirm=([0-9A-Za-z_]+).*/\1/p')
          wget --load-cookies /tmp/cookies.txt "https://drive.google.com/uc?export=download&confirm=$CONFIRM&id=$ID" -O $FILE
          sudo mkdir -p /usr/lib/jvm
          sudo tar zxf $FILE -C /usr/lib/jvm
          sudo chown -R root:root /usr/lib/jvm/jdk1.8.0_191
          sudo update-alternatives --install /usr/bin/java java /usr/lib/jvm/jdk1.8.0_191/bin/java 1
          sudo update-alternatives --set java /usr/lib/jvm/jdk1.8.0_191/bin/java
          sudo update-alternatives --install /usr/bin/javac javac /usr/lib/jvm/jdk1.8.0_191/bin/javac 1
          sudo update-alternatives --set javac /usr/lib/jvm/jdk1.8.0_191/bin/javac
          sudo update-alternatives --install /usr/bin/jar jar /usr/lib/jvm/jdk1.8.0_191/bin/jar 1
          sudo update-alternatives --set jar /usr/lib/jvm/jdk1.8.0_191/bin/jar
          sudo update-alternatives --install /usr/local/java_home java_home /usr/lib/jvm/jdk1.8.0_191 1
          sudo update-alternatives --set java_home /usr/lib/jvm/jdk1.8.0_191
          echo "JAVA_HOME=/usr/local/java_home" >> $GITHUB_ENV

JDK8は、もうEOLですけどね...

.envとPycharm

JetBrain製のエディターで、.envファイルを読み込ませたいときは、 EnvFileというプラグインを使用するとよいです。

github.com

EAP版やアップグレード直後などに、ために、そのプラグインが使えないケースがあり、少し手間ではあるけど、 以下のように、pipenvを経由して、エディターを起動すると、.envファイルが読み込まれます。

$ pipenv run open ~/Applications/JetBrains\ Toolbox/PyCharm\ Professional.app

突如、WebMoneyのAPIが、SSLPeerUnverifiedExceptionを投げ始めた。

TLS1.2に移行したようです。

すでに知られていることですが、java7は、デフォルトではTLS1.2をサポートしていません。 TLS1.2限定のサーバにリクエストを送信すると、以下のようなExceptionがスローされました。

javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated
        at sun.security.ssl.SSLSessionImpl.getPeerCertificates(SSLSessionImpl.java:421)
        at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:128)
        at org.apache.http.conn.ssl.SSLSocketFactory.connectSocket(SSLSocketFactory.java:397)
        at org.apache.http.impl.conn.DefaultClientConnectionOperator.openConnection(DefaultClientConnectionOperator.java:148)
        at org.apache.http.impl.conn.AbstractPoolEntry.open(AbstractPoolEntry.java:149)
        at org.apache.http.impl.conn.AbstractPooledConnAdapter.open(AbstractPooledConnAdapter.java:121)
        at org.apache.http.impl.client.DefaultRequestDirector.tryConnect(DefaultRequestDirector.java:573)
        at org.apache.http.impl.client.DefaultRequestDirector.execute(DefaultRequestDirector.java:425)
        at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:820)
        at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:754)
        at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:732)

近年のTSL1.2必須化の動きは、有名な話だけど、なぜか、本件は、アナウンスが届かなかった。 何も情報がなかったので、サーバ証明書の問題かなと思ってしまいました。

TLS1.2に、対応するには、いくつか方法があります。

  1. デフォルトで1.2をサポートしているから、java8以上にする。理想を言えばそうしよう。
  2. JVMの引数に、-Djdk.tls.client.protocols=TLSv1.1,TLSv1.2,,TLSv1.2-Dhttps.protocols=TLSv1.1,TLSv1.2,TLSv1.3をつけて実行しよう。
  3. これで駄目なケースもあるようだ。プログラムの修正が必要。

私の場合、3のパターンで。 Apache CommonsのHttpClientを使用しているので、以下のような修正をしました。

            SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
            sslContext.init(null, null, new SecureRandom());
            SSLSocketFactory sf = new SSLSocketFactory(sslContext);
            Scheme httpsScheme = new Scheme("https",  443, sf);
            SchemeRegistry schemeRegistry = new SchemeRegistry();
            schemeRegistry.register(httpsScheme);
            ClientConnectionManager cm = new SingleClientConnManager(schemeRegistry);
            DefaultHttpClient client = new DefaultHttpClient(cm);

HttpClientのバージョンよっては、こちらの修正方法になります。 というかググるとこればかり出てきます。

SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
  SSLContexts.createDefault(),
  new String[] { "TLSv1.2", "TLSv1.3" },
  null,
  SSLConnectionSocketFactory.getDefaultHostnameVerifier());

CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(sslsf).build();

よく見かけるこのやり方は、おそらく全体的に適用されるので、気をつける必要があると思う。

            SSLContext ctx = SSLContext.getInstance("TLSv1.2");
            ctx.init(null, null, null);
            SSLContext.setDefault(ctx);