LEFログ

:smile

送信前の下書きに使えるエディタ One-Time Editor をリリースしました

One-Time Editorというアプリを作りました。

One-Time Editorの見た目

上のはダークモード。ライトモードだとこんな感じ。

ライトモードの見た目

どんなアプリかというと、チャット送信前の下書きに使えるテキストエディタです。

このアプリを使うことで、Enterで誤って送信を誤爆する心配から逃れられます。

どんな使い勝手かについては動画を見ると速いと思うので、撮影したものを下に添付します。

youtu.be

https://youtu.be/qwj9fr77vQg

インストール方法

macOSの場合はhomebrewを使います。ターミナルで次のコマンドを打つだけですぐに使い始められます。

brew tap lef237/tap
brew install --cask one-time-editor

Electron製のアプリなので、一応クロスプラットフォームにも対応しています。

WindowやLinuxの方は、ビルド済みのアプリをここからインストールできます。

github.com

https://github.com/lef237/one-time-editor/releases

Windowsの方はexe、Linuxの方はAppImageを選択すればたぶんインストールできるはず。*1

特徴

一番のポイントは、ショートカットキーでウィンドウをトグル(表示/非表示)できることです。

しかも、トグルと同時に入力した内容をコピーしてくれるので、一瞬でペーストできます。

  1. ショートカットキーでOne-Time Editorを開く
  2. 送信したい内容を入力する
  3. ショートカットキーで閉じる
    • 自動で内容もコピーされます
    • macOSの場合は、自動で直前に操作していたウィンドウにフォーカスが戻ります
  4. ペーストして、後は送信するだけ

ライトモードとダークモードの切り替えにも対応しています。デザインもお洒落だと思います。

もちろん、ショートカットキーもカスタマイズできます! デフォルトはCtrl+jですが、好きなショートカットに割り当ててください。

なぜ作ったのか

チャットなどの送信で、EnterかShift+Enterか、はたまたCtrl+Enterなのかに悩んだ経験がある方は多いと思います。

そんでもって、文章の作成途中で誤爆してしまったり……

何かしらのエディタを使って下書きを書くのも悪くはないのですが、毎回ファイルを作成するかを聞かれたり、ウィンドウをどう管理するかがやや面倒だったりします。

このアプリの場合は書き捨ての用途で使えるし、1つのショートカットで表示/非表示&内容のコピーをしてくれるので、かなりスピーディかなと。

開発で苦労したところ

いくつかありました。

が、一番難しかったのは、コード署名と公証をどうするか問題でしょう。

Electronでアプリを作ると、手元で動かす分には問題ないのですが、ネットに上げてダウンロードするとアプリが壊れてしまいます。

Dockerなどのダウンロード時に見たことある方もいらっしゃるのではないでしょうか?

このアプリは壊れているため開けません、の表示

なので、xattrコマンドを使う必要があります。

qiita.com

https://qiita.com/quattro_4/items/f5b56c1897c0cc235c0f

つまりインストールしたあとに、このコマンドxattr -rc "/Applications/hoge.app"を打つ必要があります。これはやや面倒です。

シンプルにできないか検討したところ、postflightを使えば、homebrew-tap経由なら自動化できることに気づきました。

Electronアプリを配布したい方は参考にしてみてください!

GitHub

GitHubも公開しています。

追加の機能の要望があればIssueなどお待ちしています。🐘

github.com

https://github.com/lef237/one-time-editor

*1:もしWindows等で動かなかったらGitHubにコメントください!

Rails の find はどう動く? Active Recordの内部実装を見てみよう!

この記事はフィヨルドブートキャンプ Advent Calendar 2025の22日目の記事です。

昨日はodentakashiさんのgem の PR から学んだ ActiveRecord と lease_connection の話でした!

自分もおでんくんに引き続き、ActiveRecordの話題、いっちゃいます🍢

はじめに

皆さんはRuby on Rails使っていますか? Ruby on RailsにはActive RecordというORMがあります。

RailsのActive Recordを使う上で欠かせないのが、『Ruby on Railsガイド(以下Railsガイド)』です。この公式ドキュメントを読むことで、全体像を理解しながら体系的に学ぶことができます。

railsguides.jp

Ruby on Rails ガイド:体系的に Rails を学ぼう

Railsガイドには載っていないような細かい挙動を調べたいときもあります。そんなときは、『Ruby on Rails API』を調べるのが鉄板です。

api.rubyonrails.org

Ruby on Rails API

この2つのドキュメントを参考にするだけで、基本的には困らないのですが……もっと先へ進んでみたくないですか?

そうです。Ruby on Railsそのもののソースコードです。今回は find を取り上げて、このメソッドを読み解いていきましょう。

find ってそもそもなに?

まずはおさらいから。

ActiveRecord の find は 主キー検索 を行うためのメソッドです。

User.find(1)        # id=1 の User を取得
User.find([1, 2, 3]) # 複数レコードも取得可能

シンプルではありますが、内部ではキャッシュの利用、例外の扱い、SQL の組み立てなど様々な処理が行われます。

どこで定義されている?

では、findメソッドはどこで定義されているでしょうか?

GitHubにある rails/rails で調べてみましょう。

github.com

find メソッドは ActiveRecord の FinderMethods モジュールで定義されています。

具体的には次のファイルです。

それでもって、中のコードを読んでいくと、findが定義されています。

def find(*args)
  return super if block_given?
  find_with_ids(*args)
end

おっ、findは find_with_ids を呼んでいますね。今度はこのメソッドを見てみましょう。

  def find_with_ids(*ids)

    # 中略

    case ids.size
    when 0
      error_message = "Couldn't find #{model_name} without an ID"
      raise RecordNotFound.new(error_message, model_name, primary_key)
    when 1
      result = find_one(ids.first)
      expects_array ? [ result ] : result
    else
      find_some(ids)
    end
  end

このメソッドの最後の方を見ると、idsのサイズが0のときはエラー、1のときはfind_one、それ以外のときはfind_someを呼んでいます。

つまり、findfind_oneかfind_someを呼んでいる!ということです。*1

両方とも追いかけるとややこしいので、今回はidを一つしか渡さないと仮定して、find_oneを潜っていきましょう。

find_oneはこのように定義されています。

  def find_one(id)

    # 中略

    relation = if model.composite_primary_key?
      where(primary_key.zip(id).to_h)
    else
      where(primary_key => id)
    end

    record = relation.take

    raise_record_not_found_exception!(id, 0, 1) unless record

    record
  end

複合キーではない場合と仮定して、ものすごくざっくりと解説してみますが、つまり、whereメソッドで条件に合うものを検索して、そのなかから一つだけを take する。これがfind_oneがやっていることです。

whereはどうなってる?

つまりfindは内部的にwhereを呼んでいることが、Railsの実装を見ていくとわかります。

次にwhereを紹介していきます。*2

whereメソッドはこのファイルで定義されています。

具体的なコードだとここになります。

def where(*args)
  if args.empty?
    WhereChain.new(spawn)
  elsif args.length == 1 && args.first.blank?
    self
  else
    spawn.where!(*args)
  end
end

引数はempty?でもblank?でもないはずなので、とりあえずwhere!を見てみましょう。

def where!(opts, *rest) # :nodoc:
  self.where_clause += build_where_clause(opts, rest)
  self
end

どうやら、build_where_clauseっていうメソッドの結果を、どんどんオブジェクトに追加していっているらしいということがわかります。

build_where_clauseへ移動します。

  def build_where_clause(opts, rest = []) # :nodoc:
    opts = sanitize_forbidden_attributes(opts)

    if opts.is_a?(Array)
      opts, *rest = opts
    end

    case opts
    when String
      if rest.empty?
        parts = [Arel.sql(opts)]
      elsif rest.first.is_a?(Hash) && /:\w+/.match?(opts)
        parts = [build_named_bound_sql_literal(opts, rest.first)]
      elsif opts.include?("?")
        parts = [build_bound_sql_literal(opts, rest)]
      else
        parts = [Arel.sql(model.sanitize_sql([opts, *rest]))]
      end
    when Hash
      opts = opts.transform_keys do |key|
        if key.is_a?(Array)
          key.map { |k| model.attribute_aliases[k.to_s] || k.to_s }
        else
          key = key.to_s
          model.attribute_aliases[key] || key
        end
      end
      references = PredicateBuilder.references(opts)
      self.references_values |= references unless references.empty?

      parts = predicate_builder.build_from_hash(opts) do |table_name|
        lookup_table_klass_from_join_dependencies(table_name)
      end
    when Arel::Nodes::Node
      parts = [opts]
    else
      raise ArgumentError, "Unsupported argument type: #{opts} (#{opts.class})"
    end

    Relation::WhereClause.new(parts)
  end
  alias :build_having_clause :build_where_clause

一気に難しくなってきました。

この引数に、例えば文字列を渡すとします(User.where("age > 20")みたいなやつ)。すると、when Stringの条件に入ります。

    when String
      if rest.empty?
        parts = [Arel.sql(opts)]
      elsif rest.first.is_a?(Hash) && /:\w+/.match?(opts)
        parts = [build_named_bound_sql_literal(opts, rest.first)]
      elsif opts.include?("?")
        parts = [build_bound_sql_literal(opts, rest)]
      else
        parts = [Arel.sql(model.sanitize_sql([opts, *rest]))]
      end

するとparts = [Arel.sql(opts)]が呼ばれるはず。そして、[Arel.sql("age > 20")]がpartsの中に入ります。*3

最後にRelation::WhereClause.new(parts)が呼び出され、WhereClause オブジェクトが生成されます。

この WhereClause オブジェクトが、Relation オブジェクトの where_clause フィールドに+=でどんどん貯まっていき、最終的にはSQLに変換されて、実行されます。*4

SQLに変換される仕組みや、実行される仕組みまで解説すると、かなり長くなってしまうので、このあたりで止めたいと思います。

おわりに

いかがでしたでしょうか? 後半はちょっとむずかしくなってしまいましたが、前半は分かりやすかったと思います。

RailsRubyのコードでできています。なので、Rubyに慣れて親しんでいれば、Railsのコードは理解できるようになっています。

もし、公式ドキュメントなどを読んでも分からない挙動に遭遇した場合は、Railsそのもののソースコードを読んでみると、原因を特定できるかもしれません。*5

コードを読み解くことで、勉強になる面は多いですし、貢献できる可能性もあります。みなさんもぜひ、トライしてみてください!


明日のAdvent Calendarは ゆーかさんと mh-mobile さんです。🎄

*1:ブロック無しで ID を渡した一般的なケースにおいて

*2:コードが難しいので、自信がない箇所もあります。間違いがあったらごめんなさい。🐧

*3:Arelについては、この記事が詳しく解説していてオススメです。→https://www.timedia.co.jp/tech/activerecord-arel-5-1/

*4:自分の理解が間違っていなければ、WhereClause が積み重なった Relation が、最終的に Arel を通して SQL に変換されて、take や to_a などで DB に対して実行されるはずです。

*5:実際の業務でも、理解を深めるために元のソースコードを読む場面はたびたびありました。

RustでCSVをJSONに変換するツールをリリースしました

概要

最近、Rustを使って、CSVJSONに変換するツールをリリースしました。

ctj - crates.io: Rust Package Registry

cargo install ctjですぐインストールして使うことができます。

2025-07-17現在のバージョンは0.1.8。ちょこちょこ機能を追加しています

このツールを作った理由について、簡単に書いてみたいと思います。

作った理由

一言でいうと、ヘッダ行がないCSVファイルにも対応をしたかったのが一番大きな理由です。他のツールを使うと情報が欠落することがありました。

また、CSVの真偽値や数値について、ツール側で文字列に変換されるのが困っていました。

具体的には…?

,b,
,,FALSE
,55.5,

このようなファイルがあったとき、別のツールを使うと次のような結果になります。

[
  {
    "": "FALSE",
    "b": ""
  },
  {
    "": "",
    "b": "55.5"
  }
]

大文字のFALSEがそのまま文字列として扱われています。また、55.5という数値も文字列になってしまっています。

真偽値と数値はそのままJSONで扱えるので、できればその型情報を識別し、同じように展開したいです。

それでは自分が新たに作成したツールを紹介しましょう。このctjを使うと、次のような結果になります。

[
  {
    "": false,
    "b": ""
  },
  {
    "": "",
    "b": 55.5
  }
]

FALSEfalseに、55.5は数値(float)として扱われます(小数点が無ければintegerとして処理されます)。

また、今回のCSVファイルはヘッダ行がありません。このような状況は実際にあり得ると思います。ヘッダ行がないと、情報の欠落が生じるリスクが高まります。

そのユースケースに対応するため、オプションを用意しました--no-headerをつけると、ヘッダ行がないことを ctj に伝えられるため、このように出力できます。

[
  {
    "column_0": "",
    "column_1": "b",
    "column_2": ""
  },
  {
    "column_0": "",
    "column_1": "",
    "column_2": false
  },
  {
    "column_0": "",
    "column_1": 55.5,
    "column_2": ""
  }
]

何列目にどの情報が入力されていたのか、すぐに把握できます。

無事に変換時の情報の欠落を防ぐという目的を達成することができました!

おわりに

あえて機能はシンプルにしつつ、実務に活かせるように設計してみました。すでにあるCSVファイルだけでなく、パイプで渡した文字列を処理することも可能です。

CSVJSONに変換するツールを探している方は、ぜひ使ってみてください。コントリビュートも歓迎です!

github.com

開発生産性Conference 2025に行きました!

グッドハートの法則を説明するKent Beck

開発生産性Conference2025に行きました!

何よりも嬉しかったのが、やはりKent Beckさん御本人にお会いできたことです。自分がプログラミングの学習をし始めてから、アジャイルソフトウェア開発宣言をはじめとして、色んなところで名前を聞いていた伝説的な人物の講演を、肉眼で見ることができたのは僥倖でした。

発表はほとんどスライドを使わず、身振り手振りを用いながら行われており、壇上の左から右へと歩きながら喋っていくスタイルでした。

内容については、jgeemさんやginkounoさんがまとめてくださった、こちらの資料に詳しく書いてあります。

scrapbox.io

note.com

「プレッシャーではなく、気づきを促す」。自分も将来教育する立場に回ったときは、このことを強く意識したいと思いました。

講演のあとにはサイン会がありました。拙い英語でしたが、直接Kent Beckさんに感謝を伝えることができて嬉しかったです。timer.teamのカードも直接お渡しできました。

(サイン会のときにはできなかったのですが、翌日たまたまKent Beckさんが会場の廊下で佇んでいて、握手してもらいました!🤝)

また、お一人お一人を挙げると長くなってしまうので省略しますが、他のイベントや仕事でもお世話になっているRubyistの方々にお会いすることもできました。RubyAgileの結びつきについて再確認する機会になりました。

今回、このような貴重な機会に参加することができて、本当に良かったです。イベントを主催してくださった皆様、お話ししてくださった皆様、ありがとうございました。

講演中のKent Beck

サイン会。timer.teamのプレゼントカードを受け取ってもらえて感無量です

Railsで別ファイルに切り出さずに、Viewのプライベートなpartialを実装する

結論

captureメソッドとprocを使おう!

経緯

部分テンプレート(partial)はとても便利な機能ですが、使いすぎるとコードが少し散らかってしまいます。

たった1つのViewファイルで使うためだけの場合でも、_foo.html.erb_bar.html.erbといったファイルが増えてしまいます。それにもかかわらず、これらのファイルはどのViewからも呼び出せてしまいます(スコープが広いです)。

また、複数のファイルを行ったり来たりする手間もかかります。

例えばReactの場合では、単一ファイルの中でコンポーネントを分割することができます。このような機能をRailsのView(erb, haml, slim)でも実装する方法があるので共有します。

実装方法

結論に書いたように、captureメソッドとprocを使います。

captureメソッドについてはRailsガイドのこちらに説明があります。

railsguides.jp

captureメソッドを使うと、以下のようにテンプレートの一部を抽出して変数にキャプチャできます。

このcaputreメソッドを、procと組み合わせます。

docs.ruby-lang.org

2つを組み合わせて、次のようなコードにします。

<%# --- 定義:UIの断片を Proc オブジェクトに束ねる --- %>
<% row = proc do |user| %>
  <tr>
    <td><%= user.name %></td>
    <td><%= user.email %></td>
  </tr>
<% end %>

<%# --- 呼び出し: capture ヘルパーで Proc を評価・描画する --- %>
<table>
  <% @users.each do |user| %>
    <%= capture(user, &row) %>
  <% end %>
</table>

この方法によって、UIの断片を別ファイルに切り出すことなく、プライベートなpartialを実現できます。

もちろん、次のように複数の引数を渡すことも可能です。

<% row = proc do |user, book| %>
  <tr>
    <td><%= user.name  %></td>
    <td><%= book.title %></td>
  </tr>
<% end %>

<%= capture(current_user, featured_book, &row) %>

具体例

例えばこのようなerbを書きます。

<h1>Products</h1>

<%# --- 定義:テンプレート断片を Proc に束ねる --- %>
<% render_product = proc do |product| %>
  <div class="product">
    <h2><%= product.name %></h2>
    <p><%= product.description %></p>
    <span>¥<%= product.price %></span>
  </div>
<% end %>

<%# --- 1回目の呼び出し:通常の一覧表示 --- %>
<div id="products">
  <% @products.each do |product| %>
    <%= capture(product, &render_product) %>
  <% end %>
</div>

<%# --- 2回目の呼び出し:おすすめ商品セクション --- %>
<h2>Recommended Products</h2>
<div id="recommended">
  <% @products.select(&:recommended?).each do |product| %>
    <%= capture(product, &render_product) %>
  <% end %>
</div>

データを用意してCSSをつけると、次の画像のように表示されます。

プライベートなpartialが動いている画面

おわりに

この方法が一般的かは分かりませんが、一つのファイル内でプライベートなpartialを実装したいときに便利です。外部のGemや、複雑な実装をおこなうことなく、RubyRailsの基本的な機能のみで実現できます。

(もしかしたらもっと良い方法があるかもしれないので、お気軽にコメントください!)

えにしテック内のSlackでtmaedaさんとdarashiさんに頂いたコメントが、この記事のアイデアの元でした。この場を借りて感謝いたします🙏

「私が知る最高のプログラマーの習慣」という記事が良かった

endler.dev

The Best Programmers I Know | Matthias Endler

こちらの記事、Hacker Newsでたまたま見つけたのですが、とても良かったので紹介します。

素晴らしいプログラマーの習慣として、次の項目が挙げられていました。

  • 公式リファレンスを読もう
  • 使うツール(道具)に熟知しよう
  • エラーメッセージを読もう
  • 課題を分解しよう
  • コードに触れることに恐れない
  • チームのみんなを助けよう
  • 良い文章を書こう
  • 学び続けよう
  • 地位を気にしない
  • 信頼を築く
  • 忍耐力を持つ
  • コンピュータを責めない
  • 「分からない」と言うことを恐れない
  • 推測しない(調査する)
  • シンプルに保つ

見出しを元にざっとまとめてみました。どの項目も的確で、指針になる内容だと思いました。

個人的に面白かったところをピックアップしてみます。

使うツールに熟知しよう」という項目は、えにしテック社内でもよく言われていることです。ツールを使いこなすために、そのツールが「なぜ」作られたのかという根本的なところを意識して、掘り下げながら習得していく。表面上の使い方を知るだけではなく、背景や仕組みを理解することで、熟達の道につながります。

コードに触れることに恐れない」。実際に自分の手でコードを書いたり読んだりしないと、分からない面は多いです。例えばむかしCSSに苦手意識がありましたが、自分の手で色々実験した結果、少しずつ体系的に理解が深まってきた気がします。自分が苦手かもという思う領域も敬遠せずに、コードに触れて色々実験するのは大切です。

良い文章を書こう」。これは37signalsの本にも書かれていましたが、テキストで情報を伝達することはとても大切です。他者とのコミュニケーションを潤滑にする役割だけでなく、最近はLLMとのインターフェイスという点でも重要になってくると思います。

地位を気にしない」。お互いの地位を気にしなくて済むチームは、心理的安全性も高いです。また自由な発想も出てきますし、意見が出しやすい環境になります。Googleの研究でも心理的安全性の効果が示されていました。

www.businessinsider.jp

グーグルが行なった「最高のチームをつくる」調査の意外な結果。メンバーは重要ではなかった | Business Insider Japan

信頼を築く」。Reputationを敢えて「信頼」と訳してみました(評判・名声・信用などの多義的な言葉です)。アジャイル開発で大切にされている習慣として、「信頼貯金」という概念があります。角谷さんの記事に詳しく書かれていますが、チームのこれまでの習慣を変えたい、新しいプラクティスを導入したい、などと言った漸進的な組織づくりにはお互いの信頼が必要です。

gihyo.jp

最終回 信頼貯金を増やす | gihyo.jp

推測をしない(計測や調査をする)」。これはロバート・C・パイクの五箇条が元ネタですが、色んな場面で引用されています。思いつきで行動をするのではなく、まずは数値を計測して客観的なデータを出してから行動に移る。定量的な指標(数字)で考える大切さについては、まつもとゆきひろさんのこちらの記事が素晴らしいのでおすすめです。

logmi.jp

エンジニアは推測するな、計測せよ まつもとゆきひろ氏が説く、非機能要件で数字を重視すべき理由 | ログミーBusiness

シンプルに保つ」。複数の解法があったとき、迷ったときはできるだけシンプルな方法で解決を試みる。一度複雑化してしまったコードベースを、スッキリとリファクタリングするのは大変です。なのでシンプルなコードを保って、技術的負債を溜めないことが、長期的な開発を可能とし、ユーザーへの価値提供に繋がります。

例えばRuby on Railsというフレームワークでは、Rails Wayという考え方があります。これは、フレームワークが推奨するアーキテクチャのデフォルトに可能な限り従いつつ、ソフトウェアを育てていく方法論です。シンプルさを保ちながら、いかにスケールさせていくかがテーマであり、探求しがいのある設計論となっています。

youtu.be

基調講演 | Kaigi on Rails 2024

最近えにしテック社内で読んでいる Sandi Metzさんの『99 Bottles of OOP』は、コスト効率が高く、保守しやすく、満足のいくコードを書くことについて掘り下げられていてとても面白いです。JavaScript, PHP, Python, Rubyなど様々な言語版を一度に読めるので、気になった方はぜひ読んでみることをオススメします。

sandimetz.com

99 Bottles — Sandi Metz

と、見出しから連想したことをつらつらと書いてみました。

最高のプログラマーになるためには、こうした基本をきっちり実践していくことが大事だと改めて意識させられた記事でした。🌱

(もちろん、頭ではわかっていても、恐怖を克服するのは難しいですが)

ちなみに、もし自分が上の項目に付け加えるならば……

――アウトプットを続けよう

そして

――最後まで作り切ろう

自戒を込めて。

【VSCode】erbのコードをctrl + /で簡単にコメントアウトする方法【Ruby on Rails】

結論

この拡張機能を使おう!

marketplace.visualstudio.com

https://marketplace.visualstudio.com/items?itemName=setobiralo.erb-commenter

経緯

VSCodeでerbのファイル内でRubyの部分を、ctrl + /(もしくはcommand + /)でうまくコメントアウトすることができませんでした

具体的にはこんな感じで変なふうになります。

うまくコメントアウトできていない例

あーっ、これはいけません。

というわけで先程の拡張機能を導入しましょう。

もういちどctrl + /コメントアウトします。

するとどうでしょう。うまく全部コメントアウトできます!

コメントアウトできた例

ちなみに、複数行でも大丈夫です。HTMLのところとRubyのところをうまい感じに切り替えてくれます。

これが……

こうじゃ!

というわけで便利なのでerbを使う方はぜひ試してみましょう!

marketplace.visualstudio.com