Skip to content

最新Crystalらしい書き方へのリファクタリング #84

@limit7412

Description

@limit7412

背景

このプロジェクトは GitHub 通知を Slack に転送する AWS Lambda(カスタムランタイム)。shard.yml は crystal: 1.11.2 指定だが、実ビルドは crystallang/crystal:latest(現在 1.20.2)の Docker で行われており、コードは古いイディオムのまま。最新の Crystal らしいスタイルに合わせて全面的にリファクタリングする。外部から見た挙動は変えない。

調査で見つかった問題:

  • src/runtime/lambda.crprint_logputs `echo '#{...}'` とサブシェル経由でログ出力しており、APIレスポンスボディがそのままシェルに渡る(コマンドインジェクション/クォート破壊リスク)
  • spec/spec_helper.cr が存在しない ../src/github_notifications_slack を require しておりスペックはコンパイル不能。スペック本体も false.should eq(true) のプレースホルダ
  • crystal tool format --checklambda.cr / slack/models.cr に差分あり
  • shard.lock に未使用の clim が残存、.travis.yml は死んだ CI の残骸
  • src/github/usecase.cr で attachment 1件ごとに NotificationRepository.new(= HTTP::Client 生成)している N+1

Phase 1: イディオム・スタイル最新化

shard.yml

  • crystal: ">= 1.20.0" に更新(ビルドは latest Docker なので整合)

src/runtime/lambda.cr

  • print_logputs `echo '...'` を stdlib Log に置換(Log.for("lambda") 等)。改行除去(CloudWatch で1エントリ化するため)と 50,000 文字チャンク分割の意図は each_char.each_slice ベースで維持し、シェル呼び出しだけ排除
  • def handler(name : String) → 明示ブロックパラメータ def handler(name : String, &)
  • formatter 適用

src/github/repository.cr

  • ステータスコード数値比較を HTTP::Status の述語メソッドへ(server_error? / unauthorized? / client_error?)。401スキップの意図コメントは維持
  • Array(Notifications).new[] of Notification
  • typo 修正: retrunreturnfaildfailed
  • Serverless::Lambda.print_log 呼び出しを Log

src/github/models.cr

  • API レスポンスは不変なので propertygetter
  • @[JSON::Field(emit_null: false)] はデフォルト挙動なので削除
  • update? / mention?[...].includes?(x) → 定数配列 + x.in?(CONST)
  • comment_urllatest_comment_url.presence || url
  • Subject::Type の enum 化は やらない(GitHub が未知 type を返すと from_json が落ちるため、文字列定数を維持)

src/github/usecase.cr

  • !notify.repository.full_name.nil? ? notify.repository.full_name : "github"notify.repository.full_name || "github"

src/notify/usecase.cr

  • if notices.size != 0unless notices.empty?

src/error/usecase.cr

  • err.backtraceerr.backtrace?.try(&.join('\n'))(backtrace 未設定時の例外を防止)

src/slack/models.cr

  • formatter 適用(引数リスト末尾カンマ)。propertygetter(シリアライズ専用)

Phase 2: 設計改善

  • リネーム: Github::NotificationsGithub::Notification(1件の通知を表すため単数形)。find_notifications_unread の返り値型も追随
  • N+1解消 + DI: Github::Usecase#to_slack_attachment 内の NotificationRepository.new をやめ、コンストラクタ注入 Github::Usecase.new(repo : NotificationRepository) に。HTTP::Client が全通知で1つになる
  • ENV アクセスの集約: ENV["GITHUB_TOKEN"] / WEBHOOK_URL / SLACK_ID などの参照を src/main.cr(composition root)に集約し、ENV.fetch で必須変数の欠如を起動時に検出。Notify::Usecase / Error::Usecase はコンストラクタで依存を受け取る
  • Slack::PostRepository: send_attachmentsend_attachments([attachment]) への委譲に統一。@url を保持せず @uri のみに

Phase 3: ツーリング整備

  • ameba 導入: development_dependenciesameba(crystal-ameba/ameba, ~> 1.6)を追加 → shards install(副作用で stale な clim も shard.lock から消える)→ bin/ameba の指摘に対応。必要なら .ameba.yml で調整
  • spec 修正: spec_helper.cr の require を実在ファイルに修正し、プレースホルダ spec を削除。純粋ロジックのユニットスペックを追加(Subject#color / #update? / #comment_urlNotification#mention?、JSON パース)。HTTP 層のモックは今回は導入しない
  • CI 追加: .github/workflows/ci.yml を新規作成。crystal-lang/install-crystal アクションで latest を入れ、crystal tool format --check / bin/ameba / crystal spec を PR と master push で実行
  • 掃除: .travis.yml 削除

検証

  1. crystal tool format --check src spec が差分なし
  2. crystal build src/main.cr がコンパイル成功(static リンクは Linux ビルド時のみなので通常 build で確認)
  3. crystal spec が green
  4. bin/ameba が指摘なし
  5. ロジック差分を最終レビューし挙動不変を確認(特に 401 スキップ・5xx スキップ・既読化の条件分岐)。実環境確認は serverless-dev デプロイに委ねる

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions