RubyGems 開発速習会

Posted on
qiita Ruby

この記事はQiitaの記事をエクスポートしたものです。内容が古くなっている可能性があります。

この記事は、RubyGem 開発速習会@Wantedly の資料として作られたものです :exclamation:


この資料は、

  • Ruby 2.3.1
  • RubyGems 2.5.1
  • Bundler 1.12.4

の環境で執筆されました。

この速習会のゴール

  • gem を一から作れるようになる
  • ただ作るだけじゃなく、テスト駆動開発を取り入れた効率のよい開発ができるようにある
  • 開発支援系のサービスに詳しくなる

gem とは

gem は、最もメジャーな Ruby ライブラリの形式です。 Ruby on Rails も1つの gem として提供されており、rails gem の中でもまた多くの gem が利用されています。 現在公開されている Ruby のソフトウェアや Ruby on Rails 上の Web サービスは、多くの gem を組み合わせることで成り立っているのです。

ちなみに www.wantedly.com では、Gemfile に書いてあるだけで__164個__の gem が使われています。

$ cat Gemfile | grep -E '^\s*gem' | wc -l
     164

gem をインストールしたり探したりするときは、gem コマンドを利用します。

$ gem install rails -v 4.2.6
$ gem search rspec-rails

gem には__コマンドラインツール__と__ライブラリ__の2種類があります。 コマンドラインツールはその名の通り実行可能なコマンドを持ち、単体で実行可能なものです。 例えば Rails や RSpec はインストールしたら rails, rspec コマンドが生成されます。 一方でライブラリは、他の Ruby アプリケーションから呼び出して使うものです。 コマンドラインツールとライブラリ、両方の性質を持つ gem もあります。

作った gem は、RubyGems.org にアップロードして公開することができます。 ここで公開しておけば、gem install yourgem でインストールできるようになります。

Bundler とは

Bundler は、アプリケーション内で複数の gem を簡単に管理できるツールです。 Bundler 自身も gem として提供されています。

以下のように Gemfile を書いて bundle install をすれば、よしなに依存関係を解決して rails, rspec-rails gem がインストールされます。

source "https://rubygems.org"

gem "rails", "4.2.6"
gem "rspec-rails"

また、gem の雛形を生成する機能もあります。 今回はこれを利用して gem を作っていきます。

gem の名前

gem の名前は、Ruby の命名規則に従ってつける必要があります。

Gem 名module 名
wantedlyWantedlyそのまま
wantedly_syncWantedlySync複数単語をつなげる
wantedly-syncWantedly::Sync拡張ライブラリなどで、既存モジュールの配下に作成する

gem の名前はダブっちゃいけないので、作る前に gem search で調べとく必要があります。

gem を作る

今回作るのは、

  • GitHub からいろんな情報を取得してうまい具合に表示する Githubkun

というツールです。

まず gem の雛形を生成します。

$ bundle gem githubkun -b -t
  • -b: exe ディレクトリ下に実行可能コマンドを生成する
  • -t: RSpec テストの雛形を生成する

初めて作るときは、以下のものをリポジトリに含めるかどうかを聞かれます。 お好みによって選んでください。

  • MIT ライセンスは「無償でいくらでも再利用して構わないが、著作権表示だけはちゃんと全部表示すること」というもので、多くの OSS で利用されています。
  • Code of Conduct (CoC) は、「コントリビュートしてくれる人に敬意を払います」「プロジェクトに参加する全ての人を差別しません」といった倫理的な行動規範を示しているものです。

ディレクトリ構成

$ tree githubkun/
githubkun/
├── CODE_OF_CONDUCT.md
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── bin
│   ├── console
│   └── setup
├── exe
│   └── githubkun
├── githubkun.gemspec
├── lib
│   ├── githubkun
│   │   └── version.rb
│   └── githubkun.rb
└── spec
    ├── githubkun_spec.rb
    └── spec_helper.rb

5 directories, 13 files
  • exe
    • 実行可能コマンドが入っている
  • githubkun.gemspec
    • この gem の仕様を書く
    • 依存している gem もここに書いていく
  • lib
    • メインのコードをここに書いていく
  • spec
    • RSpec テストコードを書いていく

とりあえずコミットしておく

$ git commit -m 'bundle gem githubkun -b -t'

実際のコードを書く前に

まず gemspec の必要箇所を埋めてあげないといけません。

# coding: utf-8
lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'githubkun/version'

Gem::Specification.new do |spec|
  spec.name          = "githubkun"
  spec.version       = Githubkun::VERSION
  spec.authors       = ["Daisuke Fujita"]
  spec.email         = ["dtanshi45@gmail.com"]

  spec.summary       = %q{TODO: Write a short summary, because Rubygems requires one.}
  spec.description   = %q{TODO: Write a longer description or delete this line.}
  spec.homepage      = "TODO: Put your gem's website or public repo URL here."
  spec.license       = "MIT"

  # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
  # to allow pushing to a single host or delete this section to allow pushing to any host.
  if spec.respond_to?(:metadata)
    spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
  else
    raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
  end

  spec.files         = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
  spec.bindir        = "exe"
  spec.executables   = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
  spec.require_paths = ["lib"]

  spec.add_development_dependency "bundler", "~> 1.12"
  spec.add_development_dependency "rake", "~> 10.0"
  spec.add_development_dependency "rspec", "~> 3.0"
end

TODO になっているところを埋めます。 summarydescription はよしなに、homepage は GitHub リポジトリを指定します。

spec.summary       = %q{A good CLI tool to manipulate GitHub.}
spec.description   = %q{Githubkun is a good CLI tool to manipulate GitHub.}
spec.homepage      = "https://github.com/dtan4/githubkun"

これで bundle install が実行できるようになります。

$ bundle install

続いて、テストを実行してみると何もしてないのにコケると思います。

$ bundle exec rspec

Githubkun
  has a version number
  does something useful (FAILED - 1)

Failures:

  1) Githubkun does something useful
     Failure/Error: expect(false).to eq(true)

       expected: true
            got: false

       (compared using ==)
     # ./spec/githubkun_spec.rb:9:in `block (2 levels) in <top (required)>'

Finished in 0.0229 seconds (files took 0.12248 seconds to load)
2 examples, 1 failure

Failed examples:

rspec ./spec/githubkun_spec.rb:8 # Githubkun does something useful

bundle gem では、spec/githubkun_spec.rb にわざと失敗するテストが生成されます。 要らないのでさっさと消しましょう。

require 'spec_helper'

describe Githubkun do
  it 'has a version number' do
    expect(Githubkun::VERSION).not_to be nil
  end

  it 'does something useful' do # <===== Remove!
    expect(false).to eq(true)
  end
end

Guard

Guard は、ファイルの変更を検知して自動でコマンドを実行するツールです。 いちいち手動でテストを走らせるのも面倒なので、コードを書いたら自動でテストが走るようにしましょう。

Gemfile に以下を追記して bundle install します。 この gem 自体には関係ない(開発環境でのみ使う)ので、gemspec に含めず Gemfile に外出しします。

group :development do
  gem "guard"
  gem "guard-rspec", "~> 4.7.0"
end

続いて、Guardfile を生成します。

$ bundle exec guard init rspec

Rails 系の設定はいらないので消しちゃいましょう。

guard :rspec, cmd: "bundle exec rspec" do
  require "guard/rspec/dsl"
  dsl = Guard::RSpec::Dsl.new(self)

  # RSpec files
  rspec = dsl.rspec
  watch(rspec.spec_helper) { rspec.spec_dir }
  watch(rspec.spec_support) { rspec.spec_dir }
  watch(rspec.spec_files)

  # Ruby files
  ruby = dsl.ruby
  dsl.watch_spec_files_for(ruby.lib_files)
end

Guard を起動すると監視が始まり、lib 以下もしくは spec 以下のファイルを変更するとテストが走るようになります。

$ bundle exec guard

gem におけるバージョンの考え方

殆どの gem のバージョンは、セマンティックバージョニング にしたがって付けられています。 x.y.z の3つの数字で表現し、

  • x: メジャーバージョン。後方互換を損なう機能追加の時に上げる
  • y: マイナーバージョン。後方互換を損なわない機能追加の時に上げる
  • z: パッチバージョン。後方互換を損なわないバグ修正の時に上げる

Gemfilegemspec では、セマンティックバージョニングに従った依存 gem のバージョン固定が可能です。 基本は等号不等号ですが、gem 特有の比較演算子として ~> があります。

バージョン 1.2.3バージョン 1.3.4バージョン 2.0.0
> 1.2.3含まない含む含む
>= 1.2.3含む含む含む
>= 1.2.4含まない含む含む
~> 1.2.3含む含まない含まない
~> 1.2含む含む含まない
~> 1含む含む含む

gem "guard-rspec", ~> 4.7.0 は、guard-rspec のバージョン 4.7.x (x >= 0) に絞ってインストールするという意味です。

バージョンを固定してないと、bundle update したときにメジャーバージョンが上がって泣くことがあります。 なるべく固定しましょう。

githubkun list コマンドを実装するぞ

  • GitHub API 叩いて
  • 自分のリポジトリ一覧を撮ってきて
  • ターミナルに表示する

まず、GitHub API を叩いたりするクラス Github を作ります。 ファイル名は、さっきの命名規則に従ってつけます。 原則1ファイル1クラスです。

lib/githubkun/github.rb

module Githubkun
  class Github
    def initialize

    end

    def repositories

    end
  end
end

このクラスを読み込むように require してあげます。

lib/githubkun.rb

require "githubkun/github"
require "githubkun/version"

module Githubkun
  # Your code goes here...
end

テストを書く

せっかくなので、テスト駆動開発を実践しましょう! Guard を起動しておいてください。

GitHub クラスのテストを書いていきます。 テストファイルの名前は #{テスト対象ファイル名}_spec.rb の形になります。

#repositories メソッドでテストしたいことは

  • GitHub API /user/repos を叩くこと
  • 配列が返されること
  • 配列の中身はリポジトリ名であること

です。

spec/githubkun/github_spec.rb

このファイルは長いので、こちらを御覧ください :bow: https://github.com/dtan4/githubkun/blob/e06b23778348480e4b40a02f1824f98826e67ee2/spec/githubkun/github_spec.rb

WebMock

WebMock は、HTTP 通信をモックするためのライブラリです。

githubkun.gemspec に以下の一文を追加して bundle install し、(guard も再起動し、)

spec.add_development_dependency "webmock", "~> 2.0.3"

spec/spec_helper.rb に以下を追加すれば使えるようになります。

require "webmock/rspec"

下の例だと、https://api.github.com/user/repos に対して Authorization ヘッダをつけた GET リクエストが来た時に、body というレスポンスを返すよう宣言しています。

stub_request(:get, "https://api.github.com/user/repos").
  with(headers: { "Authorization" => "token 1234abcd" }).
  to_return(body: "hoge")

WebMock を有効にすると、デフォルトですべての HTTP 通信がインターセプトされます。 stub_request してないリクエストが来た場合、その時点でテストが失敗します。

本体実装

#repositories メソッドは実装されてないので、当然テストが失敗してます。 実装しましょう。

API 通信には rest-client gem を使うようにします。

githubkun.gemspec

spec.add_dependency "rest-client", "~> 1.8.0"

lib/githubkun.rb 先頭

require "rest-client"

lib/githubkun/github.rb

module Githubkun
  class Github
    API_ENDPOINT = "https://api.github.com"

    def initialize(github_token)
      @github_token = github_token
    end

    def repositories
      json = RestClient.get("#{API_ENDPOINT}/user/repos", "Authorization" => "token #{@github_token}", accept: :json).body
      JSON.parse(json, symbolize_names: true).map { |repository| repository[:name] }
    end
  end
end

これでテストが通るはずです!

コマンドの実装

コマンドの中身はできたので、次は list コマンド自体をつくっていきます。

Thor

Thor は、コマンドラインツールを簡単に作るためのフレームワークです。 サブコマンド + フラグ形式に対応しています。 コマンドのパースだけでなく、テンプレートからファイルの生成とかも簡単にできます。 Bundler や Rails も使ってます

Thor でコマンドを実装する

例によって Thor をインストールします。

githubkun.gemspec

spec.add_dependency "thor", "~> 0.19.1"

lib/githubkun.rb 先頭

require "thor"

Thor クラスを継承する形で、CLI クラスを実装します。 とりあえずリポジトリ名を表示するだけです。

module Githubkun
  class CLI < Thor
    desc "list GITHUB_TOKEN", "List repositories"
    option :github_token
    def list
      github = Githubkun::Github.new(options[:github_token])

      github.repositories.each do |repository|
        puts repository
      end
    end
  end
end

lib/githubkun.rb

require "githubkun/cli"

最初に exe ディレクトリに実行可能コマンドを生成しました。 この中身を実装していきます。 といっても、1行加えるだけです。

exe/githubkun

#!/usr/bin/env ruby

require "githubkun"

Githubkun::CLI.start(ARGV)

以上で実装終わりです :exclamation:

実行してみる

githubkun list コマンドの実行には GitHub トークンが必要です。 https://github.com/settings/tokens/new にアクセスし、repos もしくは public_repo のスコープでトークンを発行してください。

まずは何も考えず githubkun コマンドを実行してみます。

$ bundle exec exe/githubkun

いい具合にヘルプが表示されたと思います。 Thor でコマンドを実装すると、ヘルプも自動生成されるのです。

さて、list コマンドを実行してみましょう。

$ bundle exec exe/githubkun list --github_token YOUR_TOKEN

リポジトリ一覧が表示されましたね?

この辺でコミットしておきましょう。

$ git add .
$ git commit -m 'Implement list command'

さて、毎回 bundle exec するのは面倒です。 rake install で、gem install と同じようにインストールすることができます。

$ rake install
$ githubkun list --github_token YOUR_TOKEN

ちなみに、実際のツールではトークンをコマンドライン引数で渡すのはよろしくないです。 環境変数で渡すようにしましょう。

クラウドサービスを活用して開発を加速させる

Build Status Code Climate Test Coverage Dependency Status Gem Version

GitHub の README にいろいろバッジが貼ってあるの、見たことありますよね? あれは、リポジトリに連携した様々な開発支援系サービスのステータスを表しています。 例えばテスト結果やテストカバレッジ、依存ライブラリが新鮮かどうかがひと目で分かるようになっています。

この記事も参考にしてみてください: クラウドサービスを活用して README にバッジをペタペタ貼る - Qiita

Travis CI Build Status

Travis CI は、OSS 界隈で最も広く使われている CI as a Service です。 簡単に言うと、GitHub push にフックしてテストを実行してくれます。

使い方

まず Travis CI 上でアカウントを作りましょう。GitHub アカウントでサインアップできるので便利です。

実際のブラウザ上で設定もできるのですが、コマンドラインツール (gem!) があるのでそれを使いましょう。 travis gem をインストールして、ログインしてください。

$ gem install travis
$ travis login

リポジトリルートで以下のコマンドをうち、Travis CI 上でテストが走るようにします。

$ travis enable

実際にテストを走らせるには .travis.yml をおいておく必要があります。 が、bundle gem コマンドは自動で .travis.yml を生成してくれるのです。便利!

とりあえず、今手元の Ruby バージョンにてテストが実行されるようになっています。

sudo: false
language: ruby
rvm:
  - 2.3.1
before_install: gem install bundler -v 1.12.4

gem を作るにあたって考慮しないといけないのは、ユーザの実行している Ruby のバージョンはまちまちだということです。 少なくとも、現在公式サポートされているバージョンでは別け隔てなく実行できるようにしておきたいです。

Travis CI では、複数の Ruby バージョン上でテストを実行させることができます。 というわけで、現在サポートされている Ruby 2.1, 2.2 でもテストを走らせるようにしましょう。

sudo: false
language: ruby
rvm:
  - 2.3.1
  - 2.2.5   # Added!
  - 2.1.10  # Added!
before_install: gem install bundler -v 1.12.4

これで、git push したら Travis CI でテストが走るようになります。

Code Climate Code Climate Test Coverage

Code Climate は、ソースコードの品質チェックやテストカバレッジの測定を行ってくれるサービスです。

使い方

まずアカウントを作り、リポジトリを追加します。

自動でソースコードの解析が実行されます。

シンプルなコードなので、なんの問題も検出されませんでした。 よかった!

ちなみに、がっつり書いてあるリポジトリだといろんな問題が出てきちゃいます: https://codeclimate.com/github/dtan4/terraforming/code

Gemnasium Dependency Status

Gemnasium は、gemspecGemfile(.lock) の中身を解析し、依存している gem のバージョンが古くないかどうかをチェックしてくれるサービスです。

Gem を公開する

せっかく作った gem も、自分が使うだけじゃもったいないです。 同じような問題を解決したい人は他にもいるはずです。 ぜひ公開して gem install できるようにし、多くの人に使ってもらいましょう!

gem の公開はシンプルで簡単です。 rubygems.org のアカウントを作って、以下のコマンドを打つだけです。

$ rake release

rake release は、

  • GitHub 上でタグ付け
    • gemspecspec.version を見に行く
  • 手元のファイルを .gem ファイル(実体は .zip)にパッケージング
  • パッケージングした .gem を rubygems.org にリリース

リリースされた gem は Twitter: @rubygems で流れます。

Appendix: @dtan4 が作った gem

https://rubygems.org/profiles/dtan4

CLI しか作ってなかった!

NameDescription
Terraforming既存の AWS リソースから Terraform のコードを生成
MadoMarkdown のリアルタイムプレビューをするためのサーバ
Ramesh東京アメッシュの画像を取得
Nowtvテレビ番組表を取得。API 廃止されて使えなくなった
Md2pukiwikiMarkdown -> Pukiwiki
SpotdogEC2 Spot Instance の価格変動を Datadog に投げる
terraforming-dnsimpleTerraforming の DNSimple 対応版
Photomosaicフォトモザイク作成ツール
MemotDropbox 内の Markdown をレンダリングして Evernote に同期