textlintをWeb Workerとして動かしてアプリに組み込む

Web Workers APIを使うのは初めてなので、この使い方で問題ないのかはよくわかっていない。これから学んでいきます。

動機

会社でtextlintを便利に使っており、ユーザーさんからのお問い合わせ窓口を自社開発しているので、返信内容を送信前にlintにかけたいというのも自然な流れ。などと思っていたら、azuさんがこういうのを用意してくれていた。

内部での利用だからtextlintをサーバーで動かして結果を受け取るのでもいいけど、これのためにWebサーバーにNode.js入れて動かすのもなんかやりすぎ。かといってJobサーバーで動かす構成にするのはもっとやりすぎ。 ブラウザ拡張も用意してくれているけど、構造はシンプルなのでアプリに組み込めればその方が使う側の差異が出なくてサポートがラク。というわけで組み込みを試してみようとなった。

要望

  • 送信前のメッセージをtextlintにかけられる
  • テキストエリアへのオーバーレイでのlint違反表示は不要
  • 特定のエリアに違反内容が表示されればいい

環境

  • typescript: 3.9.10
  • react: 17.0.2
  • @textlint/script-compiler: 0.12.1

tl;dr

  • ブラウザ拡張で使うための @textlint/script-compiler を用意してくれているので、これでworker用のコードを生成
  • 生成したWorkerのコードをロードし、あとは普通にpostMessageで送信し、onmessageで結果の受けとりを書けばいい

残る課題

コンパイルする仕組み上、設定をリソースファイルに切り出しているルールなどはコンパイルできない。prhは https://github.com/textlint/editor/tree/master/packages/%40textlint/config-inliner で対応してくれているけど、それ以外は今のままでは取り込めない。

https://github.com/lostandfound/textlint-rule-ja-hiragana-hojodoushi とかも使いたいので、config-inlinerで対応できるように改造したい。

やったこと

見返してみると別に何もしていない。worker用のコードは生成してくれたものをそのまま使っているだけだし。

textlintのスクリプトコンパイラーと使いたいルールをインストール

yarn add -D @textlint/script-compiler textlint-rule-no-dropping-the-ra

.textlintrcを作成

{
  "rules": {
    "no-dropping-the-ra": true
  }
}

textlint-worker.jsを生成

textlint-script-compiler --output-dir ./public --metadataName 'foo_bar' --metadataNamespace 'http://localhost:3000/' --metadataHomepage 'http://localhost:3000/'

そういえば、metadataNamespace, metadataHomepageはlocalhostのままだけど特に問題ないな。このままでいいのだろうか。

textlintルールを追加するたびに生成し直すので、package.jsonのscriptsにも入れておく。

  "scripts": {
    "compile-textlint-worker": "textlint-script-compiler --output-dir ./public --metadataName 'foo_bar' --metadataNamespace 'http://localhost:3000/' --metadataHomepage 'http://localhost:3000/'"
  }

これでpublic配下に出力されるので、あとは読み込み側。こんなモジュールを書いた。 意識するコマンドはただ使うだけなら lint, lint:result だけ。

const textlintWorker = new Worker("/textlint-worker.js");

export function postToTextlint(body: string) {
  textlintWorker.postMessage({
    command: "lint",
    text: body,
    ext: ".md",
  });
}

interface LintResult {
  type: string;
  ruleId: string;
  message: string;
  column: number;
  index: number;
  line: number;
}

export function registerReceiver(
  setLintResults: React.Dispatch<React.SetStateAction<string[]>>
) {
  textlintWorker.onmessage = function (event) {
    switch (event.data.command) {
      case "init":
        break;
      case "lint:result":
        const messages: LintResult[] = event.data?.result?.messages;
        const messageStrings = messages.map(
          (m) => `[${m.line} 行目:${m.column} 文字目] ${m.message}`
        );
        setLintResults(messageStrings);
        break;
      default:
        console.log("unknown command, ignore");
    }
  };
}

Reactコンポーネントの方で読み込み。こんな感じ。これでlintの結果がstateに入ってくれるようになった。

  const [lintResults, setLintResults] = React.useState([] as string[]);
  React.useEffect(() => {
    registerReceiver(setLintResults);
    postToTextlint(body);
  }, [body, setLintResults]);

あとはよしなにstateの内容を表示するとこんな感じになった。 これはng-wordsも入れている。便利。

f:id:shrkw:20210702164811p:plain
textlintの適用結果

ng-word ruleが行数とかが出ていないのはPRを出してある。マージしてくれるだろうか。

github.com

遺族年金について調べた

生命保険とかについて考えるために、遺族年金についてまるで理解していないので調べた。以下に調べて理解したことを書くけど、細かい条件があるので実際には必ず日本年金機構に相談すること。

このまとめの表が一番わかりやすかった。

遺族基礎年金(国民年金 遺族厚生年金(厚生年金保険)
支給対象となる遺族の範囲 子のある配偶者 子 子のある妻または子のある55歳以上の夫 子 子のない妻 子のない55歳以上の夫 55歳以上の父母 孫 55歳以上の祖父母
年金額 定額 老齢厚生年金の報酬比例部分の4分の3
独自の制度・加算 寡婦年金、死亡一時金 中高齢寡婦加算

遺族基礎年金・遺族厚生年金の違いと支給要件|専業主婦・主夫の年金【保険市場】

遺族基礎年金

これがわかりやすい。 遺族基礎年金(受給要件・支給開始時期・計算方法)|日本年金機構

子が18歳になるまでの支援という意味合いが強いと理解した。

ざっくりまとめると以下。

  • 支給要件(論理和
    • 国民年金加入中の被保険者、または、老齢基礎年金の受給資格期間が25年以上ある者が死亡したとき
  • 対象者(以下のいずれか)
    • 死亡した者によって生計を維持されていた、
      • (1)子のある配偶者
      • (2)子
      • 子とは次の者に限ります
        • 18歳到達年度の末日(3月31日)を経過していない子
        • 20歳未満で障害年金の障害等級1級または2級の子
  • 支給期間
    • 末の子が18歳になるまで
  • 年金額
    • 実際には変動率が加味される
    • 780,900円+子の加算
    • 子の加算 第1子・第2子 各 224,700円
    • 第3子以降 各 74,900円

遺族厚生年金

これがわかりやすい。 遺族厚生年金(受給要件・支給開始時期・計算方法)|日本年金機構

厚生年金を払ったことへのリターンの意味合いが強いと理解した。

  • 支給要件(論理和
    • 被保険者が死亡したとき、または被保険者期間中の傷病がもとで初診の日から5年以内に死亡したとき
    • 老齢厚生年金の受給資格期間が25年以上ある者が死亡したとき
    • 1級・2級の障害厚生(共済)年金を受けられる者が死亡したとき
  • 対象者(以下のいずれか)
    • 死亡した者によって生計を維持されていた、
      • 子、孫(18歳到達年度の年度末を経過していない者または20歳未満で障害年金の障害等級1・2級の者)
      • 55歳以上の夫、父母、祖父母(支給開始は60歳から。ただし、夫は遺族基礎年金を受給中の場合に限り、遺族厚生年金も合わせて受給できる。)
    • 子のない30歳未満の妻は、5年間の有期給付
  • 支給期間
    • 生涯
    • だけど、妻が過去に厚生年金に加入していた場合、その分の遺族厚生年金が減額される
  • 年金額
    • 老齢厚生年金の報酬比例部分の4分の3

中高齢寡婦加算

  • 対象者
    • 夫が亡くなったとき、40歳以上65歳未満で、生計を同じくしている子がいない妻
    • 遺族厚生年金と遺族基礎年金を受けていた子のある妻が、子が18歳到達年度の末日に達した(障害の状態にある場合は20歳に達した)等のため、遺族基礎年金を受給できなくなったとき
  • 年金額
    • 40歳から65歳になるまでの間、585,700円(年額)が加算

寡婦年金

  • 第1号被保険者として保険料を納めた期間および国民年金の保険料免除期間が10年以上
  • 10年以上結婚していた妻に60歳から65歳になるまで支払われる
  • 年金額
    • 夫の第1号被保険者期間だけで計算した老齢基礎年金額の4分の3の額
  • 繰り上げ支給の老齢基礎年金とは併用できない

サンプルケース

これのシミュレーションがわかりやすかった。

遺族年金とは?いつからいつまでが受給期間?種類や支給金額、手続きの流れをわかりやすく解説 | ナビナビ保険

遺族基礎年金+遺族厚生年金のシミュレーション(年額)

  • 夫死亡から長男18歳まで(4年間):遺族基礎年金1,229,100円 + 遺族厚生年金591,948円 = 1,821,048円
  • 長男18歳から次男18歳まで(5年間):遺族基礎年金1,004,600円 + 遺族厚生年金591,948円 = 1,596,548円
  • 次男18歳から妻65歳まで(17年間):遺族基礎年金0円 + 遺族厚生年金591,948円 + 中高齢寡婦加算585,100円 = 1,177,048円
  • 妻65歳以降※:遺族厚生年金591,948円 + 老齢基礎年金780,100円 = 1,372,048円

ActiveRecordで子のアソシエーションが必須の場合のFactoryBotの使い方いろいろ

社内ドキュメントに書いたものの転載。

前提

猫と猫の脚というモデルがあり、猫は脚が一本以上は必要というバリデーションがされているとき、

class Cat < ApplicationRecord
  has_many :cat_legs, dependent: :destroy
  validates :cat_legs, presence: true
end

class CatLeg < ApplicationRecord
  belongs_to :cat
end

素直にFactoryを書くとこうだが、

FactoryBot.define do
  factory :cat do
    name { 'たま' }
    age { 5 }
  end
  factory :cat_leg do
    nail { true }
    association :cat # 書いても意味がない。先にcatを作ろうとするため、下記の通り、RecordInvalidになる
  end
end

これらは create では使えない。

pry(main)> FactoryBot.create(:cat)
ActiveRecord::RecordInvalid: Validation failed: Cat legs can't be blank
from /usr/local/bundle/gems/activerecord-6.0.3.3/lib/active_record/validations.rb:80:in `raise_validation_error'
pry(main)> FactoryBot.create(:cat_leg)
ActiveRecord::RecordInvalid: Validation failed: Cat legs can't be blank
from /usr/local/bundle/gems/activerecord-6.0.3.3/lib/active_record/validations.rb:80:in `raise_validation_error'

CatとCatLegは同時に保存されないとバリデーションを通過できない。

ファクトリー関係なく保存する方法

肝は、CatLegの方はsaveせずにbuildだけにしておいて、saveはCatの方で一回でやるということ。 これで、単一のトランザクションでINSERTされる。

irb(main):006:0> c = Cat.new
=> #<Cat id: nil, created_at: nil, updated_at: nil>
irb(main):007:0> c.cat_legs.build
=> #<CatLeg id: nil, cat_id: nil, created_at: nil, updated_at: nil>
irb(main):008:0> c.cat_legs.build
=> #<CatLeg id: nil, cat_id: nil, created_at: nil, updated_at: nil>
irb(main):009:0> c.save
   (0.1ms)  begin transaction
  Cat Create (28.5ms)  INSERT INTO "cats" ("created_at", "updated_at") VALUES (?, ?)  [["created_at", "2021-06-16 08:13:51.866786"], ["updated_at", "2021-06-16 08:13:51.866786"]]
  CatLeg Create (9.5ms)  INSERT INTO "cat_legs" ("cat_id", "created_at", "updated_at") VALUES (?, ?, ?)  [["cat_id", 12], ["created_at", "2021-06-16 08:13:51.905388"], ["updated_at", "2021-06-16 08:13:51.905388"]]
  CatLeg Create (0.3ms)  INSERT INTO "cat_legs" ("cat_id", "created_at", "updated_at") VALUES (?, ?, ?)  [["cat_id", 12], ["created_at", "2021-06-16 08:13:51.919341"], ["updated_at", "2021-06-16 08:13:51.919341"]]
   (40.5ms)  commit transaction

1行でやりたいならこういうのでもいい。

irb(main):012:0> c = Cat.new(cat_legs: [CatLeg.new, CatLeg.new])
=> #<Cat id: nil, created_at: nil, updated_at: nil>
irb(main):013:0> c.save!
   (0.1ms)  begin transaction
  Cat Create (25.2ms)  INSERT INTO "cats" ("created_at", "updated_at") VALUES (?, ?)  [["created_at", "2021-06-16 08:26:17.387995"], ["updated_at", "2021-06-16 08:26:17.387995"]]
  CatLeg Create (8.7ms)  INSERT INTO "cat_legs" ("cat_id", "created_at", "updated_at") VALUES (?, ?, ?)  [["cat_id", 14], ["created_at", "2021-06-16 08:26:17.427883"], ["updated_at", "2021-06-16 08:26:17.427883"]]
  CatLeg Create (0.6ms)  INSERT INTO "cat_legs" ("cat_id", "created_at", "updated_at") VALUES (?, ?, ?)  [["cat_id", 14], ["created_at", "2021-06-16 08:26:17.440913"], ["updated_at", "2021-06-16 08:26:17.440913"]]
   (57.2ms)  commit transaction
=> true

Catモデルに accepts_nested_attributes_for :cat_legs があれば、もちろん、cat_legs_attributes で一発で渡せる。

irb(main):001:0> Cat.create!(cat_legs_attributes: [{}])
   (1.2ms)  SELECT sqlite_version(*)
   (0.1ms)  begin transaction
  Cat Create (25.7ms)  INSERT INTO "cats" ("created_at", "updated_at") VALUES (?, ?)  [["created_at", "2021-06-16 08:02:22.525263"], ["updated_at", "2021-06-16 08:02:22.525263"]]
  CatLeg Create (10.4ms)  INSERT INTO "cat_legs" ("cat_id", "created_at", "updated_at") VALUES (?, ?, ?)  [["cat_id", 10], ["created_at", "2021-06-16 08:02:22.561846"], ["updated_at", "2021-06-16 08:02:22.561846"]]
   (35.8ms)  commit transaction
=> #<Cat id: 10, created_at: "2021-06-16 08:02:22", updated_at: "2021-06-16 08:02:22">

解法

ストラテジーごとにやり方はいくつかある。上から推奨順に書いている。

注意

以下のやり方は create_list する場合には意図した通りになりません。

github.com

cat_legは同じインスタンスが使いまわされてしまうので、最後のcatにしかcalt_legsが入っていないことになってしまいます。複数作りたい場合は、諦めて素直にcreateを複数実行しましょう。

    cats = [
      FactoryBot.create(:cat, tags: [FactoryBot.build(:cat_leg)]),
      FactoryBot.create(:cat, tags: [FactoryBot.build(:cat_leg)])
    ]

createのパラメータとして子のアソシエーションも渡す

いろいろ検討したけど、いつものやり方で普通にできた。

FactoryBot.create(:cat, cat_legs: FactoryBot.build_list(:cat_leg, 4))

これはaccepts_nested_attributes_forの有無に拘らず、単一のトランザクションで処理される。

irb(main):003:0> FactoryBot.create(:cat, cat_legs: FactoryBot.build_list(:cat_leg, 4))
   (0.1ms)  begin transaction
  Cat Create (34.5ms)  INSERT INTO "cats" ("created_at", "updated_at") VALUES (?, ?)  [["created_at", "2021-06-16 07:50:50.484299"], ["updated_at", "2021-06-16 07:50:50.484299"]]
  CatLeg Create (14.6ms)  INSERT INTO "cat_legs" ("cat_id", "created_at", "updated_at") VALUES (?, ?, ?)  [["cat_id", 7], ["created_at", "2021-06-16 07:50:50.527524"], ["updated_at", "2021-06-16 07:50:50.527524"]]
  CatLeg Create (0.1ms)  INSERT INTO "cat_legs" ("cat_id", "created_at", "updated_at") VALUES (?, ?, ?)  [["cat_id", 7], ["created_at", "2021-06-16 07:50:50.548179"], ["updated_at", "2021-06-16 07:50:50.548179"]]
  CatLeg Create (0.3ms)  INSERT INTO "cat_legs" ("cat_id", "created_at", "updated_at") VALUES (?, ?, ?)  [["cat_id", 7], ["created_at", "2021-06-16 07:50:50.551906"], ["updated_at", "2021-06-16 07:50:50.551906"]]
  CatLeg Create (0.2ms)  INSERT INTO "cat_legs" ("cat_id", "created_at", "updated_at") VALUES (?, ?, ?)  [["cat_id", 7], ["created_at", "2021-06-16 07:50:50.555688"], ["updated_at", "2021-06-16 07:50:50.555688"]]
   (37.0ms)  commit transaction
=> #<Cat id: 7, created_at: "2021-06-16 07:50:50", updated_at: "2021-06-16 07:50:50">

build して明示的にsaveする

cat = FactoryBot.build(:cat, cat_legs: FactoryBot.build_list(:cat_leg, 4))
cat.save!

以下のように2行に分けてやるとcatの方で関連を認識できないのでバリデーションを通過できないので注意。

cat = FactoryBot.build(:cat)
FactoryBot.build_list(:cat_leg, 4, cat: cat)
cat.save!

よく使う場合はfactoryと同じファイルでこんなメソッドを定義してもいい。

def cat_with_legs_by_build(legs_count: 4)
  cat = FactoryBot.build(:cat, cat_legs: FactoryBot.build_list(:cat_leg, legs_count))
  cat.save!
  cat
end

(accepts_nested_attributes_forの場合) attributes_for を使う

accepts_nested_attributes_for なら子の属性も同時に渡せるのでこうもやれる。やってることは上記の build のやり方と同じなので、 build の方がコードがシンプルになりやすいのでbuildの方がいいと思う。

cat = Cat.new(FactoryBot.attributes_for(:cat).merge(cat_legs_attributes: FactoryBot.attributes_for_list(:cat_leg, 4)))
cat.save!

(非推奨) :skip_validate を使う

create(:cat, :skip_validate) で保存はできるが、あとでvalidかどうかの確認をしないといけないので、上記のやり方でカバーできるのであれば使わない方がいい。

ダメだったやり方

has_many association のやり方で

def cat_with_legs(legs_count: 5)
  FactoryBot.create(:cat) do |user|
    FactoryBot.create_list(:cat_leg, legs_count, cat: cat)
  end
end

こんな感じでいけるかと思ったけど、やはり親のsaveの方が先に実行されてしまうようでバリデーションを通過できなかった。

S3の暗号化方式についておさらい 2021.06

社内向けに書いた記事を転載。

SSE (サーバーサイド暗号化)

SSE-S3

S3が管理するキーでの、透過的暗号化。

  • Pros
    • 設定すればいいだけなのでラク
    • 共通の鍵なので追加コストもなし
  • Cons
    • 透過的復号がされてしまうので、S3のコンソールでダウンロードしても復号されてしまう。クレデンシャル漏洩には無意味。

SSE-S3になっているかは、オブジェクトの「サーバー側の暗号化設定」の箇所で確認できる。

SSE-KMS

AWS-KMSの鍵での、透過的暗号化。

  • Pros
    • 設定すればいいだけなのでラク
  • Cons
    • 透過的復号がされてしまうので、S3のコンソールでダウンロードしても復号されてしまう。クレデンシャル漏洩には無意味。
    • KMSから鍵を取ってくるので回数が増えると多少、コストに響く

確認方法はSSE-S3と同様。

f:id:shrkw:20210615143250p:plain

SSE-C (カスタマーキーによるサーバーサイド暗号化)

ユーザーが管理する鍵をサーバーにオブジェクトと一緒に毎回アップロードしてサーバーサイドで暗号化する方式。

CSEに比べるとあまり変わらないが、暗号化、復号にユーザー側サーバーのリソースを使わないのが利点か。

以下の項目をパラメーターとしてS3にputする。

  • sse_customer_algorithm
    • AES256のみ
  • sse_customer_key
  • sse_customer_key_md5 (optional)
    • なくても使えるけど、つけた方がいい

残念ながらPresigned URLは使えない。発行はできるが、エンドユーザーがアクセスするときにも固有のヘッダーが必要になるため、直接アップロード、ダウンロードなどには使えない。

SSE-C 以外のオブジェクトでは、署名付き URL を生成し、それをブラウザに直接貼り付けることで、たとえばデータにアクセスできます。

ただし、これは SSE-C オブジェクトには当てはまりません。署名付き URL に加えて SSE-C オブジェクトに固有の HTTP ヘッダーも含める必要があります。したがって、SSE-C オブジェクトの署名付き URL はプログラムでのみ使用できます。

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/specifying-s3-c-encryption.html

AWSのサポートにも問い合わせたが、以下を満たす最高のソリューションは存在しないとのこと。

  • 透過的復号をしない
  • 署名付きURLが特別なヘッダーなしで使える

透過的復号に関しては、IAMの権限でGet-Objectを制限することで近い状態にはできるのでは、というアドバイスがあった。

Pros/Cons

  • Pros
    • 透過的復号がされないので、クレデンシャル漏洩に効果がある
  • Cons
    • Presigned URLが実質使えない
    • コンソールから気軽にダウンロードできない
    • させるべきでないならこれもProsになる
  • ローカルで鍵の管理が必要
    • RailsのCredentialsに入れてもいいしKMSから鍵を取得する方式にしてもいいので、それほどのConsでもない

CLIでの例

$ aws s3api put-object --bucket $BUCKET   --key sse-upload   --body sse-c.txt   --sse-customer-algorithm AES256   --sse-customer-key 
{
    "ETag": "\"3a2def088089b2d7d7aee1xxxxxxxxx\"",
    "SSECustomerAlgorithm": "AES256",
    "SSECustomerKeyMD5": "NX6C25NPxF9KJbS4Pxxxxx=="
}
$ aws s3api head-object --bucket $BUCKET   --key sse-upload    --sse-customer-algorithm AES256   --sse-customer-key $KEY
{
    "AcceptRanges": "bytes",
    "LastModified": "2021-05-24T02:03:42+00:00",
    "ContentLength": 11,
    "ETag": "\"3a2def088089b2d7d7aee19ca9bxxxxx\"",
    "ContentType": "binary/octet-stream",
    "Metadata": {},
    "SSECustomerAlgorithm": "AES256",
    "SSECustomerKeyMD5": "NX6C25NPxF9KJbS4Pxxxxx=="
}

S3のコンソールからはサーバー側の暗号化設定、メタデータなどは空欄になっている。

f:id:shrkw:20210615143335p:plain

コンソールからダウンロードしようとすると以下のように拒否される。

<Error>
<Code>InvalidRequest</Code>
<Message>The object was stored using a form of Server Side Encryption. The correct parameters must be provided to retrieve the object.</Message>
<RequestId>AQVE86HB0HCNQW7Z</RequestId>
<HostId>xxxxxxxxxxxxxxx7bXv0ZCp9Ukn4OX710r7UZR/Kl3Dql9cdNr6Tv+rH8EFnHXeqqvejVBK1BthWGo=</HostId>
</Error>

refs.

CSE (クライアントサイド暗号化)

SDKModule: Aws::S3::Encryptionを使うとCSEになる。CSEにも以下の2種類がある。

  • AWS-KMSの鍵を使う
    • 上記SDKのパラメータは kms_key_id, kms_client
  • ローカルの鍵を使う
    • 上記SDKのパラメータは encryption_key
  • 本当はもう一つKeyProviderを渡す形式もあるけど割愛

Pros/Cons

  • Pros
    • 透過的復号がされないので、クレデンシャル漏洩に効果がある
  • Cons
    • 自前のサーバーで一旦、復号しないといけないので、エンドユーザーに渡すようなコンテンツの場合、帯域、リソース、メモリを消費する
    • Presigned URLが使えないので、エンドユーザーからの直接アップロード、ダウンロードが使えない
    • ローカルの鍵の場合、鍵の管理が必要
    • credentials, KMSなど利用するといい

CSEで暗号化されたかどうかはS3上のオブジェクトのメタデータで確認できる。 x-amz-meta-x-amz-keyがあればCSE。暗号化に使われたデータキーがここに収納されている。

CSEJavaと.NETの実装例 クライアント側の暗号化を使用したデータの保護 - Amazon Simple Storage Service

ActionMailerのdeliver_laterでメール送信する場合のユニットテストの書き方

社内ドキュメントに書いたやつを転載。

課題

deliver_laterを使う場合、Railsガイドのメイラーの機能テストの項にあるような、 ActionMailer::Base.deliveries.last での検証はできません。 これは、deliver_laterは非同期でのメール送信となるため、即時のメール送信結果の作成が行われないからです。

これに対しては以下の二つのアプローチがあります。

ここでは便利なのでrspecを利用した例を書いていますが、TestHelperを使えばminitestでも同様のテストは可能です。

対応1: ActiveJobのキューを検証する

基本的にこの対応がいいと思います。 コントローラーやモデルでは、キューにいれるまでが責務で、渡された情報をどのようにメールに組み立てるかはActionMailerのテストで賄うべきだからです。

RSpecではマッチャーが用意されており、 have_enqueued_mail を使うことで簡易に検証できます。

RSpec.describe NotificationsMailer do
  it "matches with enqueued mailer" do
    expect {
      NotificationsMailer.signup.deliver_later
    }.to have_enqueued_mail(NotificationsMailer, :signup)
  end
end

https://relishapp.com/rspec/rspec-rails/v/5-0/docs/matchers/have-enqueued-mail-matcher

もし、Mailerを呼び出す側でエンキューするときの引数を組み立てるロジックがある場合でも、 .with で引数を確認するくらいが適切です。 ActionMailer::Base.deliveries.last の中身の確認はやりすぎ。

rspecでない場合は、ActiveJob::TestHelper を使って、 ActionMailer::MailDeliveryJob のジョブが入っているかなどを確認するのが良いでしょう。

対応2: 局所的に即時実行にして送信結果を作成する

対応1がおすすめですが、どうしてもメール内容を確認したいという場合はこちらも使えます。

perform_enqueued_jobs というAPIが用意されているのでそれを使います。 https://edgeapi.rubyonrails.org/classes/ActiveJob/TestHelper.html#method-i-perform_enqueued_jobs

ActiveJob::TestHelper

RSpec.describe NotificationsMailer do
  it "matches mail result" do
    perform_enqueued_jobs(only: ActionMailer::MailDeliveryJob) do
      expect(NotificationsMailer.signup.deliver_later).to change(ActionMailer::Base.deliveries, :count).by(1)
      mail = ActionMailer::Base.deliveries.last
      expect( mail.subject).to eq 'mail subject'
    end
  end
end

局所的に設定変更するやり方の記事もありますが、これだと全部のJobが動いてしまうので悪手です。有効にするJobを選べる、 perform_enqueued_jobs を使う方が賢い。

長野県上田市に引っ越してもう半年も経っていた

去年の秋に東京都練馬区から長野県上田市に引っ越して、もう半年が経っていた。

なぜ東京を離れたのか

  • 新型コロナで在宅勤務が平常になった
  • 新型コロナ感染リスクのストレスが嫌になった
  • 子供が小さいうちは東京のミニシアターや美術館、博物館などの文化資本にもそんなに触れられない
    • たまにいくくらいなら地方からでもいける
    • 自然史系の博物館は地方も充実している
  • 東京にしかないレストランも子供が小さいうちは行く機会が少ない
  • プログラミングの勉強会とかも子供が小さいうちは基本的にいかないことにしている
  • 関東圏の夏は暑くて湿気が強いのがすごく嫌い

というのもあり、東京離れるのを会社に相談したら快諾してくれたので、もはや東京にいる意味がなくなり、引っ越すことにした。

なぜ上田なのか

雨が少ないところがいい

長野は日照時間が日本でトップクラスのところが多いので、長野県が候補になった。なので、逆に栃木県那須塩原市とかは雨が多いのがネックで候補から外れた。

夏の暑さがマイルドなところがいい

群馬県高崎市茨城県筑波市とかはこれで候補から外れた。そこそこ標高があるところがいい。

東京駅まで行きやすいところがいい

松本市は東京へのアクセスのしにくさで候補から外れた。後日で、松本へ遊びに行ったけど、道路事情の辛さもあって松本選ばなくてよかったとなった。

雪は少ないところがいい

長野市まで行くと日本海の気候の影響が強くなるので降雪量が多いけど、上田市は比較的降雪量が少ない。

コンパクトな街がいい

佐久市もいいんだけど、上田の方が市街地がほどよくコンパクト。

という感じで上田市になった。

引っ越してどうか

今のところ、住みやすくて大満足。 郊外にあるようなチェーン店は大概あるし、美味しいレストランやパン屋、お菓子屋も探せば結構あって嬉しい。パニエレストランとか、東京いた時にもなかなかいけないような店でそれだけでも移住してよかったと思えている。 あと、そばがやはり普通にすごく美味しい。草笛が盛りがケチケチしてないのでお気に入りです。近所だし。

すごく驚いたのは、車が歩行者優先で一時停止すること。 「横断歩道で車が止まってくれる率」長野県はなぜダントツなのか 県警に聞いてみた | 乗りものニュース

神奈川や東京で、横断歩道を無視してビュンビュン走る車に怯えながら暮らしてた身からすると、それだけでかなり好感度高い。 けど、車を運転する身としては、ウィンカーを出すタイミングが遅すぎるのでそれはどうにかしてほしい。

野菜や果実が豊富で、直売所行くと、美味しいりんごがたくさん買えるし、地物の大豆や落花生も安価で買えてすごく助かる。

松本の方が栄えているんだけど、幼児を育てる身としては盛り場はなくていいので、今のニーズには松本は合わない。子供が中学生、高校生になったら松本に行きたがりそう。その時はバスで行ってもらおう。

人生を思い返すと、わりと引っ越しが多い。引っ越しは荷物減らす圧になるので、そういうのも気に入っている。これからもまた引っ越しするのかな。どうだろ、子供の学校のことがあるのでしばらくは定住すると思う。

GitHub Actionsでバージョンをバンプしつつタグを打ち、リリースノートにPRベースのchangelogを記載したい

tl;dr

https://github.com/shrkw/sandbox/blob/master/.github/workflows/create_release_tag.yml

このワークフローを実行すると、こんなリリースタグが生成されて便利。

f:id:shrkw:20210310122951p:plain

Release Release v0.4.0 · shrkw/sandbox · GitHub

なぜやりたいのか

  • 手動でリリースタグを作成しているとバージョンナンバーをたまに間違えたりして面倒
  • ローカルでタグを打ってプッシュするやり方もできるけど、ローカルにリモートの最新を取ってくるのも面倒なので、GitHubのWeb UI上で完結させたい
  • 今はActionsの手動実行でやるフローにしているけど、mainやreleaseへのコミットで起動させるようにしてもいいと思う
  • リリースノートにはコミットではなくプルリクエストを列挙したかった

どうやるか

やるべき要素は以下。

できたワークフローは以下。

sandbox/create_release_tag.yml

on:
  workflow_dispatch:
    inputs:
      bumping_part:
        description: 'major, minor or patch'
        default: 'minor'
        required: true

workflow_dispatchがあると Run workflow のメニューが出てくる。

f:id:shrkw:20210310122932p:plain

semverのどのパートを更新するかを選べるようにしていて、github-tag-actiondefault_bump でパートを指定できるので渡している。

yarn run の結果を全部出してるから余計な箇所もあるので、きれいにしたい場合はsedとかでがんばって。

リリースノート記載内容をどう集めるか

リリースノート、チェンジログを自動で生成したいのでlerna-changelogを使った - Bouldering & Com. に書いた、 @shrkw/lerna-changelog を使う。

      - uses: actions/checkout@v2
        with:
          fetch-depth: 0 # 履歴を全て見たい
      - name: Install dependencies
        run: yarn install
      - name: run lerna-changelog
        id: run_changelog
        run: |
          yarn run lerna-changelog | tee /tmp/changelog.txt
          changelog=$(cat /tmp/changelog.txt)
          echo "::set-output name=changelog::${changelog//$'\n'/'%0A'}";

これだと毎回インストールが実行されるのでお好みでキャッシュを利用すると良いです。