esm アジャイル事業部 開発者ブログ

永和システムマネジメント アジャイル事業部の開発者ブログです。

RubyKaigi Takeout 2021 に Platinum スポンサーとして協賛します

こんにちは、 @yucao24hours です。

私たち永和システムマネジメントは、2021 年 9 月 9 日・10日・11 日の 3 日間に渡って開催される RubyKaigi Takeout 2021 に Platinum スポンサーとして協賛することとなりましたのでお知らせします。

rubykaigi.org

新型コロナウィルスの猛威が依然として収まらぬ中、多くの困難を乗り越えてこのような素晴らしいカンファレンスの場を今年も作っていただけたことを大変ありがたく思っています。

状況の改善を願い "RubyKaigi" として一同に集まってのカンファレンス開催を企画してくださっていた関係者のみなさまの胸中は察するに余りあります。そんな大変な中でありながらも、素晴らしいスピーカーの方々をお迎えいただき開催されるカンファレンスを、参加者として今からとても楽しみにしています。


弊社は前回までの RubyKaigi でも、さまざまな形のスポンサーとして開催のお力添えをさせていただいてきました。

今回の RubyKaigi Takeout も、関わるみなさまにとってより充実した場となり、ひいては Ruby をとりまくコミュニティ全体が活気に溢れた場であり続けられるようにという想いを込めて、微力ながらサポートさせていただきます。

なお、Platinum スポンサー特典として、幕間後に CM を流させていただけることとなりました。現在、オリジナル CM を鋭意準備中です!ぜひお楽しみに。

ぼくがかんがえたさいきょうの Input object

こんにちは。最近筋トレにはまっている wat-aro です。

blog.agile.esm.co.jp で Input object が紹介されていますが、実際に使っていくとネストしたパラメータの扱いに困ったためその解決方法を紹介します。
この記事のコードはすべて https://github.com/wat-aro/input_object にあります。

ここでは題材として GitHub REST API の

POST /repos/{owner}/{repo}/pulls/{pull_number}/reviews

を扱います。
API の定義は以下です。
https://docs.github.com/ja/rest/reference/pulls#create-a-review-for-a-pull-request

type RequestBody = {
  commit_id?: string;
  body?: string;
  event?: string;
  comments?: Array<{
    path: string;
    position?: number;
    body: string;
    line?: number;
    side?: string;
    start_line?: number;
    start_side?: string
  }>
}

ネストしたパラメータ

題材にしたAPI定義の comments のようにリクエストパラメータがネストしている場合がよくあります。
Input object で API のリクエストパラメータを受け取るようにしていると、このネストしたパラメータの扱いに困ります。
先の記事に紹介されたように、 AtiveModel::Attributes API を使うと ActiveRecord と同じようにキャストした値が扱えるのですが、 ActiveModel::Type にはネストしたパラメータを扱えるようなものがありません。

class ReviewInput
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :commit_id, :string
  attribute :body, :string
  attribute :event, :string, default: 'COMMENT'
  # attribute :comments, ここでネストしたパラメータを扱いたい
end

これを解決するためにハッシュや配列から別オブジェクトにキャストできるクラスを作成します。
まずは comment 用の Input object を用意します。

class Review::CommentInput
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :path, :string
  attribute :position, :integer
  attribute :body, :string
  attribute :line, :integer
  attribute :side, :string
  attribute :start_line, :integer
  attribute :start_side, :string
end

この Review::CommentInputReviewInputattribute に指定できるようにします。
Input クラスを引数にとって initialize できる ActiveModel::Type を作成します。

class InputType < ActiveModel::Type::Value
  def initialize(input_class, array: false)
    @input_class = input_class
    @array = array
  end

  def type
    :"#{@input_class.to_s.tableize.singularize}"
  end

  private

  def cast_value(value)
    if array?
      if value.is_a?(Array)
        value.map {|v| to_input(v) }
      else
        []
      end
    else
      to_input(value)
    end
  end

  def array?
    @array
  end

  def to_input(value)
    @input_class.new(value)
  rescue ArgumentError
    nil
  end
end

InputType を使うと Review::CommentInputattribute で指定できるようになります。

  attribute :comments, InputType.new(Review::CommentInput, array: true), default: []

これでネストしたパラメータや配列になっているパラメータを Input object として扱えるようになりました。

ネストしたパラメータのバリデーション

ネストしたパラメータを扱えるようになりましたが、今のままだとバリデーションが少し不便です。
ReviewInput#valid? を呼び出してもネストした comments のバリデーションが実行されません。
バリデーションメソッドを使って comments のバリデーションを実行したとしても、そのままでは ReviewInput#errors に追加されませんし、どのフィールドでエラーがでているかを翻訳できる形で表示するには一工夫必要です。

ここではカスタムバリデータを作成してこの問題を解決します。
このカスタムバリデータは以下のように書いて呼び出します。

  validates :comments, input: true

これは、comments に対して valid? を呼び出し、errors に翻訳できる形でエラーを追加します。

class InputValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, input)
    if input.respond_to?(:each)
      input.each do |i|
        validate_input(record, attribute, i)
      end
    else
      validate_input(record, attribute, input)
    end
  end

  private

  def validate_input(record, attribute, input)
    if input.respond_to?(:valid?)
      return if input.valid?

      input.errors.messages.each do |key, values|
        values.each do |value|
          record.errors.add("#{record.class.to_s.tableize.singularize}/#{attribute}.#{key}", value)
        end
      end
    else
      record.errors.add(attribute, :invalid)
    end
  end
end

翻訳ファイルについては

---
ja:
  activemodel:
    attributes:
      'review_input/comments':
        body: コメントの本文
        path: コメントのパス

のようにしておくとエラーがあった場合に

irb(#<Repositories::PullRequests::ReviewsController:0x00005634fe0e8a38>):003:0> e.record.errors.messages
=> {:"review_input/comments.body"=>["を入力してください"]}
irb(#<Repositories::PullRequests::ReviewsController:0x00005634fe0e8a38>):004:0> e.record.errors.full_messages
=> ["コメントの本文を入力してください"]

のようにエラーメッセージを出せるようになります。

さいごに

Input object を実際に扱う上で困るネストしたパラメータに対応する方法を紹介しました。
この記事がいいと思った方はチャンネル登録とグッドボタンをよろしくおねがいします。

agile.esm.co.jp

Rails / OSS パッチ会オンライン 2021年7月のお知らせ

2021年7月の Rails / OSS パッチ会を 7月29日(木)にオンライン開催します。

この会をひとことでいうと、日頃のお仕事で使っている Rails をはじめとする OSS への upstream にパッチを送る会です。

会には Ruby と Rails のコミッターである顧問の a_matsuda もいますので、例えば Rails に送るパッチのネタがあるけれど、パッチを送るに適しているかの判断やパッチを送る流れが悩ましいときなど a_matsuda に相談して足がかりにするなどできます。

開催時間は 17:00-19:00 となりますがご都合のあう方はぜひご参加下さい。Zoom あたりのテレビ会議システムを使います。

当日の招待 URL は Idobata の esminc/rails ルームで共有する予定です。

idobata.io

特に募集ページなど設けませんが、上記理由から Idobata のアカウントが必要になると思います。

翌 7月30日(金) 締め切りの Kaigi on Rails 2021 の CFP や、最近の Ruby / Rails まわりの動向に関する話題などあるかもしれません。

その他の開催方針については以下の Gist に記していますので、ご参照ください。

Reboot Rails/OSS meetup online · GitHub

Mac から Windows マシンに移行して 9 ヶ月がたちました

こんにちは、 @yucao24hours です。ことし 3 月ごろから本格的にボディメイクにハマっていて、3 ヶ月間で筋肉量をほぼ変えず体脂肪率を 24% から 19% まで減らせました!今は筋肉量を増やすべく、もりもり食べてがんがんトレーニングしているところです。


さて、タイトルの件。

2013 年の入社以来ずっと Mac を使っていた私が Windows マシンに変えて 9 ヶ月が経過しましたので、ここに至るまでに私が実際に感じてきたことなどをまとめてみようと思います。(以下、特筆しない限り "Mac" は "macOS のマシン" を指します。)

過去の私がそうだったように、「今は Mac を使っているし、どれくらい影響があるかわからないので、なかなかふんぎりがつかないな...」と思われている方もいらっしゃるのではと思います。そのような方々の判断材料のひとつとしてお役に立てれば嬉しいです。

※ 対象読者としては、現在 Mac を使っていてお悩みのかたを想定しています。現在 Linux を使っている方は、Windows を消して Arch Linux なぞを入れれば問題ないでしょう。(ジョークだよ!)1

前提1: PC を移行する時点での私の開発環境

私がふだんのお仕事でどんなツールを使って何をしているか、ここ最近で参加した数プロジェクトの実績から代表的なものをまとめておきます。

  • コードを書くのは Vim
  • ターミナルの管理は Tmux
  • シェルは Zsh
  • 開発環境は Docker (on WSL2)
  • メインのブラウザは Chrome
  • 資料の確認のために Microsoft Excel, PowerPoint
  • チーム内、社内での連絡には Slack, Idobata
  • プロジェクト管理は Pivotal Tracker, Trello, Notion
  • ドキュメント管理は DocBase, esa, Notion, Google Docs
  • ソースコード管理は GitHub(Git の操作は CLI で)

前提2: 購入したマシン

参考までに、購入したマシンとスペックについても記載しておきます。

当時、社内の複数の先輩が既に業務で使っていたこと、私自身も某プロジェクトの検証機として使った経験があったことから、Dell XPS 13 を選びました。

  • マシン: Dell XPS 13 (9300)
  • OS: Windows 10 Pro (64ビット)
  • メモリ: 32GB
  • CPU: 第10世代 インテル® Core™ i7

で、どうだった?ファーストインプレッション

改めてこのことについて書いてみようと思ってここ一年弱の業務体験を思い返してみると、「Mac のころからあまり変わらない」というのが最初の印象です。

前述した、頻繁に利用するツールのリストをご覧いただいてもわかるとおり圧倒的にブラウザベースのツールが多いですし、ネイティブアプリがしっかり用意されているサービスばかりだというのが大きな要因かもしれません。

また、開発で使っているツールについては、WSL2 の Ubuntu 上で使っているというのもあって "このマシンは Windows である" ということを殊更意識するような場面はあまりなかったように思います。(私が VSCode 使いとかだったら、もしかしたらもう少しなにか違ったのかな...いや、そうでもないか...?)

なお、WSL2 のセットアップについては当ブログの以下の記事でも解説しています!が、最新の情報ではないので今からトライする方は必ず公式情報もあたってみてください。 blog.agile.esm.co.jp

WSL2 に関しての留意事項

私が作業している中においては、以下の WSL2 特有のふるまいに遭遇し影響を受けました。WSL2 を使っているシステム開発者はほぼみな関係してくることだと思うので、予め知っておいて損はないんじゃないかなぁと思います。

systemd がデフォルトでは動いていない

timedatectl を使おうとしたら動かなかったため「はて?」と思い、調べたところで知りました。

こちらの現象に対する対応は https://github.com/arkane-systems/genie というライブラリを導入するのが定石になっているようです。

VPN に接続するとネットワークが切断され、インターネットにアクセスできなくなる

私が関わる業務ではセキュリティ上の理由から VPN に接続した状態でないとアクセスできないように制限されている操作などがあるため、よく VPN に接続するのですが、そのたびになぜか git pullcap deploy ができなくなるという現象が起きていて大変困っていました。上記に書いたとおり、VPN に接続するときというのはたいてい「失敗するとちょっとこわい」操作なので、原因がわかっていない頃に接続できなかったときは焦ったものです。。

こちらは Microsoft のページで対応方法が解説されています。他にも現実的に運用しやすいワークアラウンドを考案している方もいるようなので、参考にしてみるとよいかもしれません。

Windows にしてよかったこと

さて、それではそろそろ「Windows にしてよかった!」と思ったことについてもふれておきましょう... それは、他でもなく「(WSL2 上で)Docker を使った開発が快適にできるようになったこと」です。実際、これを一番の目当てにしてマシンを変えたので、期待通りのメリットを享受できた点にはとても満足しています。

ここ数年で参画したプロジェクトの中で、開発環境に Docker を使っていないというプロジェクトはひとつもありませんでした(本番環境でもコンテナを使う、というプロジェクトも珍しくなくなりましたね)。

それくらい Docker 上で開発することが普通になった今、Mac の Docker for Mac の処理のスローぶりは私にとって致命的でした。

私は TDD が大好きなので、コードを書くときにはまずテストを書いて作業にエンジンをかけ、そこからドライヴしていくのがお決まりのスタイルです。ところが、Mac の Docker においてはそれを満足にできず、長いこと「なんとかしたいなぁ... 」と感じていました。

しかし、Windows (WSL2)に変えてからというもの、そういったストレスは一切なくなったのです!これは本当に感動的で、当時の日報にも

Docker 上でテストを流したり、ブラウザ操作をするたびに、お笑い芸人並のオーバーリアクションで「いや処理はやっ!!!!」と声が出るくらいである。

と書いていました。

これだけでも、フォントが Mac に比べて好みじゃないなどといった微妙なネガティブポイントを補って余りある利点だと感じています。WSL2 さまさまです。

気に留めておいたほうがいいこと

最後に、乗り換えるにあたって認識しておくと後悔が少ないであろうことを挙げてみます。(当たり前のことだと思われるかもしれませんが、そう思えた方はこちらを気にせず迷わず WSL に乗り換えてしまいましょう!)

(日本語の)情報が少ない

まず、WSL2 については利用者は徐々に増えているものの、Mac に比べたら圧倒的にマイナー側なので公に出ている情報は少ないです。

何かあったときには発生事象から自力で原因を突き止めたり、WSL の公式 repo の issues 等を見て状況を把握したりできる必要はあると思います...当ブログのような技術専門リソースにご興味のあるような方ならそんなの当たり前だと思われるでしょうが。

あ、あるいは弊社のように、なにかあったときにすぐに聞ける人たちが近くにいてくれるような環境だとよりよいですね!(宣伝)

気にすることが多くなる

学習をはじめて間もない方にとっては、WSL2 というものに加えて、ホスト OS / WSL2 上の OS / Docker 等とさまざまな要素が絡み合っている状態はトラブルシューティングに苦慮するところかもしれません。

なにかあったときの問題の切り分け作業が多岐に渡る可能性が高くなるため、これらの要素に不慣れな方は解決までの道のりがちょっとハードかもしれないということは認識しておくとよいでしょう。もしも Linux( や Docker )にまったくさわったことがないという方なら、それらについてを学びたいというのが作業の主眼でない限りはまずは Mac をお使いになったほうが、やりたいことにフォーカスし続けられてよいのではと思います。


以上、気がついたことをまとめてみました。

他にもご意見などがあればぜひ聞かせてもらえると嬉しいです!それでは、ごきげんよう~。


  1. 最近の弊社では Arch Linux を入れている人が多いようなのです。

Input object を使ってリクエストパラメータを検証する

はじめに

Bonjour! 近ごろ Duolingo のフランス語コースにハマっている @ima1zumi です。

さて、最近リクエストパラメータにバリデーションをかけたいことがありました。そのために、ユーザの入力を検証する専用のクラスを作ると便利だったので紹介します。

動作確認環境

  • Ruby 3.0.1
  • Ruby on Rails 6.1.4

実現したいこと

ここでは例として User の一覧を表示する UsersController#index に、表示順を制御する order_keyorder_direction というリクエストパラメータを追加します。

order_keyUser モデルのカラム名を、 order_directionascdesc のみ受けつけます。これらのパラメータにバリデーションをかけて、規定の値のみ受け付けるようにします。

class UserController < ApplicationController
  def index
    # order に渡すクエリパラメータのバリデーションを行いたい
    @users = User.all.order(user_params[:order_key] => user_params[:order_direction])
  end

  private

  def user_params
    params.permit(:order_key, :order_direction)
  end
end

Input object の責務

ここではバリデーションをかけるクラスを Input object と呼ぶことにします。

これは Shopify の Upgrow (現在は非公開) の Input object の考え方を参考にしています。

このオブジェクトの責務は ユーザーの入力を検証しオブジェクト化する ことです。

参考:Upgrow: Railsアプリの保守性を高めるためのShopifyのアプローチ / Upgrow - Speaker Deck

Input object の実装

まず、 ActiveModel::ModelActiveModel::Attributesinclude したクラスを作成します。

app/inputs/user_order_input.rb

class UserOrderInput
  include ActiveModel::Model
  include ActiveModel::Attributes
end

ActiveModel::Attributes

ActiveModel::Attributesinclude すると attribute クラスメソッドを使えるようになります。 attribute で属性を宣言するとアクセサが定義されたり、 initialize でその値を渡すことができるようになります。attribute に属性名と型を渡すと型をキャストします。

class UserOrderInput
  include ActiveModel::Model
  include ActiveModel::Attributes
  # 属性の設定を宣言的に記述する
  attribute :order_key, :string
  attribute :order_direction, :string, default: 'desc'
end

ActiveModel::Attributes のドキュメントはありませんので rails のコードを直接ご覧ください。

github.com

キャストできる型はこちらに書いてあります。

github.com

サンプルコード

attribute で宣言した属性にアクセスすることができます。

user_order_input = UserOrderInput.new(order_key: 'name', order_direction: 'asc')
p user_order_input.order_key
# => name

いろいろな型にキャストしてみました。Boolean などキャストできて便利です。

class SampleInput
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :number, :integer
  attribute :flag, :boolean
  attribute :birthday, :date
end

sample = SampleInput.new(number: '1', flag: 'true', birthday: '1993-02-24')

p sample.number
# => 1
p sample.number.class
# => Integer

p sample.flag
# => true
p sample.flag.class
# => TrueClass

p sample.birthday
# => Wed, 24 Feb 1993
p sample.birthday.class
# => Date

ちなみに、ActiveModel::Attributes -> ActiveModel::Model の順番で include すると initialize メソッドの評価順がおかしくなり動かなくなるので注意してください。

class SampleInput
  include ActiveModel::Attributes
  include ActiveModel::Model

  attribute :name, :string
end
# => #<Concurrent::Map:0x00007f8ab03225e8 entries=0 default_proc=nil>
SampleInput.new(name: 'hoge')
# => /Users/mi/.asdf/installs/ruby/3.0.1/lib/ruby/gems/3.0.0/gems/activemodel-6.1.4/lib/active_model/attributes.rb:124:in `_write_attribute': undefined method `write_from_user' for nil:NilClass (NoMethodError)

ActiveModel::Model

ActiveModel::Modelinclude すると validates クラスメソッドなどのバリデーションメソッドを使えるようになります。

railsguides.jp

class UserOrderInput
  include ActiveModel::Model
  include ActiveModel::Attributes
  attribute :order_key, :string
  attribute :order_direction, :string, default: 'desc'
  # 各属性のバリデーションを記述する
  validates :order_key, inclusion: { in: User.column_names }, presence: true
  validates :order_direction, inclusion: { in: %w(asc desc) }
end

サンプルコード

インスタンスに対して valid? / invalid? でバリデーション結果を確認することが出来ます。

user_order_input = UserOrderInput.new(order_direction: 'asc')
p user_order_input.valid?
# => true
user_order_input = UserOrderInput.new(order_direction: 'hoge')
p user_order_input.valid?
# => false

コントローラから呼び出す

ここでは UsersControllerindex アクションでバリデーションをかけます。

class UserController < ApplicationController
  before_action :valid_params, only: [:index]

  def index
    @users = User.all.order(user_order_input.order_key => user_order_input.order_direction)
  end

  private

  def valid_params
    head :bad_request if user_order_input.invalid?
  end

  def user_order_input
    @user_order_input ||= UserOrderInput.new(user_params)
  end

  def user_params
    params.permit(:order_key, :order_direction)
  end
end

このようにコントローラとバリデーションの責務を分けることができました。

Input object と Form object の違い

Form object はいろいろな定義がありますが、ここでは willnet さんの定義を引用します。

form_withのmodelオプション*1にActive Record以外のオブジェクトを渡すデザインパターンです。form_withのmodelオプションに渡すオブジェクト自体もform objectと呼びます。

(参考:form objectを使ってみよう - メドピア開発者ブログ

Form object と Input object の一番の違いは、 Input object は入力値の検証とインスタンスの作成のみ行うということです。

Form object は名前や性質がフォームと強く結びついています。また Form object を使って save することをスコープに入れる場合があります。

今回紹介した方法では、フォームでなくてもよいです。また、 save は行いません。

ここでは、リクエストパラメータのバリデーションに使える Input object の実装例を紹介しました。

参考

プルリクエストのレビュー時に気をつけていること

こんにちは、 @yuki0920 です。

エンジニア歴が1年を超えたあたりから、プルリクエスト(以下、PR)をレビューしてもらうだけでなく、レビューをする機会が多くなりました。

本記事では、PRのレビュワーとして、業務で気をつけている点について記します。

前提

私がエンジニア1年目の時はPRを出すと、100や200以上の指摘をもらうことが多々ありました。 自分のレビューを受ける側としての経験から、「対応しやすかった」というレビューのポイントを抽出し、自身がレビュワーをする際に活かすことで、効率的なPRのレビューを行えるのではないかと考え実践しています。

ポイント1. 現時点の問題点の理由を伝える

「~の実装のほうが良いのではないでしょうか?」という指摘は、対応しづらいことが多いです。 なぜ現状のコードが良くないのか?がわからないためです。

そのため、現状のコードが抱える課題とその理由をできるだけ具体的に記すようにしています。

ポイント2. 提案内容をコードで示す

提案内容をコードで示すのは、レビュワーからすると自分の頭の中で描いていることをコードに落とし込むだけなので、そんなに時間をかけずにできる場合が多いはずです。 一方で、レビューを受ける側からすると、具体的なコードがないと対応方針がわからない場合でも、レビュワーから具体的なコードの提案があれば、対応方針を立てやすくなります。

私は GitHubのSuggestionを積極的に活用し、自分がより良いと思うコードを書いて、議論の足がかりとなるようにしています。

ポイント3. 断定するのではなく提案する

いくら合理的な指摘をしたとしても、断定口調で言葉使いが良くないと、指摘を受けた側は積極的に対応しづらい気持ちになります。

あくまでも、PRの提出者を尊重して、「~と対応するのはいかがでしょうか?」など、提案する風に、かつ柔らかく伝えられるように気をつけています。

まとめ

私がPRのレビュワーとして普段から気をつけている点を紹介しました。 みなさんの気をつけている点もぜひ教えていただけると幸いです。

Rails / OSS パッチ会オンライン 2021年6月のお知らせ

2021年6月の Rails / OSS パッチ会を 6月24日(木)にオンライン開催します。

この会をひとことでいうと、日頃のお仕事で使っている Rails をはじめとする OSS への upstream にパッチを送る会です。

会には Ruby と Rails のコミッターである顧問の a_matsuda もいますので、例えば Rails に送るパッチのネタがあるけれど、パッチを送るに適しているかの判断やパッチを送る流れが悩ましいときなど a_matsuda に相談して足がかりにするなどできます。

開催時間は 17:00-19:00 となりますがご都合のあう方はぜひご参加下さい。Zoom あたりのテレビ会議システムを使います。

当日の招待 URL は Idobata の esminc/rails ルームで共有する予定です。

idobata.io

特に募集ページなど設けませんが、上記理由から Idobata のアカウントが必要になると思います。

RubyKaigi Takeout 2021 や Kaigi on Rails 2021 の CFP や、最近の Ruby / Rails まわりの動向に関する話題などあるかもしれません。

その他の開催方針については以下の Gist に記していますので、ご参照ください。

Reboot Rails/OSS meetup online · GitHub