Основы создания плагинов Rails

Плагин Rails - это либо расширение, либо изменение основного фреймворка. Плагины представляют:

  • Способ для разработчиков делиться новыми идеями без затрагивания стабильного кода.
  • Сегментную архитектуру, такую, что часть кода может быть исправлена или изменена по своему собственному графику.
  • Решение для разработчиков ядра приложения, чтобы не включать каждую новую особенность в свой код.

После прочтения этого руководства, вы узнаете:

  • Как создать плагин с нуля.
  • Как написать и запустить тесты для плагина.

Это руководство описывает, как создать плагин, движимый тестами (TDD), который будет:

  • Расширять классы ядра Ruby, такие как Hash и String.
  • Добавлять методы в ActiveRecord::Base в традициях плагинов 'acts_as'.
  • Представлять информацию о том, где разместить генераторы в вашем плагине.

Для целей этого руководства представьте на момент, что вы заядлый любитель птиц. Вашей любимой птицей является дятел (Yaffle), и вы хотите создать плагин, позволяющий другим разработчикам пользоваться особенностями дятлов.

1. Настройка

В настоящий момент плагины Rails создаются как гемы, gem. Они могут использоваться различными приложениями с помощью RubyGems и Bundler.

1.1. Создание гема.

Rails поставляется с командой rails plugin new, создающей скелет для разработки любого типа расширения Rails со способностью запуска интеграционных тестов с помощью приложения-заглушки Rails. Создайте свой плагин с помощью команды:

$ rails plugin new yaffle

Как ее использовать и ее опции смотрите:

$ rails plugin new --help

2. Тестирование своего нового плагина

Можете перейти в директорию, содержащую плагин, запустить команду bundle install, и запустить сгенерированный тест с использованием команды rake.

Вы должны увидеть:

  1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

Это сообщает вам, что все сгенерировалось правильно, и вы можете начать добавлять функционал.

3. Расширение классов ядра

Этот раздел объясняет, как добавить метод в String, который будет доступен везде в вашем приложении rails.

В следующем примере мы добавим метод в String с именем to_squawk. Для начала создайте новый файл теста с несколькими утверждениями:

# yaffle/test/core_ext_test.rb

require 'test_helper'

class CoreExtTest < ActiveSupport::TestCase
  def test_to_squawk_prepends_the_word_squawk
    assert_equal "squawk! Hello World", "Hello World".to_squawk
  end
end

Запустите rake для запуска теста. Этот тест должен провалиться, так как мы еще не реализовали метод to_squawk:

    1) Error:
  CoreExtTest#test_to_squawk_prepends_the_word_squawk:
  NoMethodError: undefined method `to_squawk' for "Hello World":String
    /path/to/yaffle/test/core_ext_test.rb:5:in `test_to_squawk_prepends_the_word_squawk'

Отлично - теперь мы готовы начать разработку.

В lib/yaffle.rb добавьте require 'yaffle/core_ext':

# yaffle/lib/yaffle.rb

require 'yaffle/core_ext'

module Yaffle
end

Наконец, создайте файл core_ext.rb и добавьте метод to_squawk:

# yaffle/lib/yaffle/core_ext.rb

String.class_eval do
  def to_squawk
    "squawk! #{self}".strip
  end
end

Чтобы проверить, что этот метод делает то, что нужно, запустите юнит тесты с помощью rake из директории плагина.

  2 runs, 2 assertions, 0 failures, 0 errors, 0 skips

Чтобы увидеть его в действии, измените директорию на test/dummy, запустите консоль и начните squawking:

$ bin/rails console
>> "Hello World".to_squawk
=> "squawk! Hello World"

4. Добавление метода "acts_as" в Active Record

Обычным паттерном для плагинов является добавление в модель метода с именем acts_as_something. В нашем случае мы хотим написать метод с именем acts_as_yaffle, добавляющий метод squawk в модель Active Record.

Для начала настройте свои файлы, вам нужны:

# yaffle/test/acts_as_yaffle_test.rb

require 'test_helper'

class ActsAsYaffleTest < ActiveSupport::TestCase
end

# yaffle/lib/yaffle.rb

require 'yaffle/core_ext'
require 'yaffle/acts_as_yaffle'

module Yaffle
end

# yaffle/lib/yaffle/acts_as_yaffle.rb

module Yaffle
  module ActsAsYaffle
    # your code will go here
  end
end

4.1. Добавление метода класса

Этот плагин ожидает, что мы добавим в модель метод с именем last_squawk. Однако, у пользователей плагина уже может быть определен метод в модели last_squawk, который они используют для чего-то иного. Этот плагин позволит имени быть измененным, добавив метод класса yaffle_text_field.

Для начала напишем падающий тест, показывающий нужное нам поведение:

# yaffle/test/acts_as_yaffle_test.rb

require 'test_helper'

class ActsAsYaffleTest < ActiveSupport::TestCase

  def test_a_hickwalls_yaffle_text_field_should_be_last_squawk
    assert_equal "last_squawk", Hickwall.yaffle_text_field
  end

  def test_a_wickwalls_yaffle_text_field_should_be_last_tweet
    assert_equal "last_tweet", Wickwall.yaffle_text_field
  end

end

При запуске rake вы увидите следующее:

    1) Error:
  ActsAsYaffleTest#test_a_hickwalls_yaffle_text_field_should_be_last_squawk:
  NameError: uninitialized constant ActsAsYaffleTest::Hickwall
    /path/to/yaffle/test/acts_as_yaffle_test.rb:6:in `test_a_hickwalls_yaffle_text_field_should_be_last_squawk'

    2) Error:
  ActsAsYaffleTest#test_a_wickwalls_yaffle_text_field_should_be_last_tweet:
  NameError: uninitialized constant ActsAsYaffleTest::Wickwall
    /path/to/yaffle/test/acts_as_yaffle_test.rb:10:in `test_a_wickwalls_yaffle_text_field_should_be_last_tweet'

  4 runs, 2 assertions, 0 failures, 2 errors, 0 skips

Это сообщает нам об отсутствии необходимых моделей (Hickwall и Wickwall), которые мы пытаемся протестировать. Эти модели можно с легкостью создать в нашем "dummy" приложении Rails, запустив следующие команды в директории test/dummy:

$ cd test/dummy
$ bin/rails generate model Hickwall last_squawk:string
$ bin/rails generate model Wickwall last_squawk:string last_tweet:string

Теперь можно создать необходимые таблицы в вашей тестовой базе данных, перейдя в приложение-заглушку и мигрировав базу данных. Сначала запустите:

$ cd test/dummy
$ bin/rake db:migrate

Пока вы тут, измените модели Hickwall и Wickwall так, чтобы они знали, что они должны действовать как дятлы.

# test/dummy/app/models/hickwall.rb

class Hickwall < ActiveRecord::Base
  acts_as_yaffle
end

# test/dummy/app/models/wickwall.rb

class Wickwall < ActiveRecord::Base
  acts_as_yaffle yaffle_text_field: :last_tweet
end


Также добавим код, определяющий метод acts_as_yaffle.

# yaffle/lib/yaffle/acts_as_yaffle.rb
module Yaffle
  module ActsAsYaffle
    extend ActiveSupport::Concern

    included do
    end

    module ClassMethods
      def acts_as_yaffle(options = {})
        # тут будет ваш код
      end
    end
  end
end

ActiveRecord::Base.include(Yaffle::ActsAsYaffle)

Затем можно вернуться в корневую директорию плагина (cd ../..) и перезапустить тесты с помощью rake.

    1) Error:
  ActsAsYaffleTest#test_a_hickwalls_yaffle_text_field_should_be_last_squawk:
  NoMethodError: undefined method `yaffle_text_field' for #<Class:0x007fd105e3b218>
    activerecord (4.1.5) lib/active_record/dynamic_matchers.rb:26:in `method_missing'
    /path/to/yaffle/test/acts_as_yaffle_test.rb:6:in `test_a_hickwalls_yaffle_text_field_should_be_last_squawk'

    2) Error:
  ActsAsYaffleTest#test_a_wickwalls_yaffle_text_field_should_be_last_tweet:
  NoMethodError: undefined method `yaffle_text_field' for #<Class:0x007fd105e409c0>
    activerecord (4.1.5) lib/active_record/dynamic_matchers.rb:26:in `method_missing'
    /path/to/yaffle/test/acts_as_yaffle_test.rb:10:in `test_a_wickwalls_yaffle_text_field_should_be_last_tweet'

  4 runs, 2 assertions, 0 failures, 2 errors, 0 skips


Подбираемся ближе... Теперь мы реализуем код метода acts_as_yaffle, чтобы тесты проходили.

# yaffle/lib/yaffle/acts_as_yaffle.rb

module Yaffle
  module ActsAsYaffle
   extend ActiveSupport::Concern

    included do
    end

    module ClassMethods
      def acts_as_yaffle(options = {})
        cattr_accessor :yaffle_text_field
        self.yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s
      end
    end
  end
end

ActiveRecord::Base.include(Yaffle::ActsAsYaffle)

Когда запустите rake, все тесты должны пройти:

  4 runs, 4 assertions, 0 failures, 0 errors, 0 skips

4.2. Добавление метода экземпляра

Этот плагин добавит метод 'squawk' в любой объект Active Record, который вызовет 'acts_as_yaffle'. Метод 'squawk' просто установит значение одному из полей в базе данных.

Для начала напишем падающий тест, показывающий желаемое поведение:

# yaffle/test/acts_as_yaffle_test.rb
require 'test_helper'

class ActsAsYaffleTest < ActiveSupport::TestCase

  def test_a_hickwalls_yaffle_text_field_should_be_last_squawk
    assert_equal "last_squawk", Hickwall.yaffle_text_field
  end

  def test_a_wickwalls_yaffle_text_field_should_be_last_tweet
    assert_equal "last_tweet", Wickwall.yaffle_text_field
  end

  def test_hickwalls_squawk_should_populate_last_squawk
    hickwall = Hickwall.new
    hickwall.squawk("Hello World")
    assert_equal "squawk! Hello World", hickwall.last_squawk
  end

  def test_wickwalls_squawk_should_populate_last_tweet
    wickwall = Wickwall.new
    wickwall.squawk("Hello World")
    assert_equal "squawk! Hello World", wickwall.last_tweet
  end
end

Запустите тест, чтобы убедиться, что последние два теста упадут с ошибкой, содержащей "NoMethodError: undefined method `squawk'", затем обновите 'acts_as_yaffle.rb', чтобы он выглядел так:

# yaffle/lib/yaffle/acts_as_yaffle.rb

module Yaffle
  module ActsAsYaffle
    extend ActiveSupport::Concern

    included do
    end

    module ClassMethods
      def acts_as_yaffle(options = {})
        cattr_accessor :yaffle_text_field
        self.yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s

        include Yaffle::ActsAsYaffle::LocalInstanceMethods
      end
    end

    module LocalInstanceMethods
      def squawk(string)
        write_attribute(self.class.yaffle_text_field, string.to_squawk)
      end
    end
  end
end

ActiveRecord::Base.include(Yaffle::ActsAsYaffle)

Запустите rake в последний раз, вы должны увидеть:

  6 runs, 6 assertions, 0 failures, 0 errors, 0 skips

Использование write_attribute для записи в поле модели - это всего лишь пример того, как плагин может взаимодействовать с моделью, но не всегда правильный метод для использования. Например, также можно использовать:

send("#{self.class.yaffle_text_field}=", string.to_squawk)

5. Генераторы

Генераторы могут быть включены в ваш гем простым их добавлением в директорию lib/generators вашего плагина. Подробнее о создании генераторов смотрите в руководстве по генераторам

6. Публикация вашего гема

Плагины в виде гемов, которые в текущий момент в разработке, могут с легкостью быть доступны из любого репозитория Git. Чтобы поделиться гемом Yaffle с другими, просто передайте код в репозиторий Git (такой как GitHub) и добавьте строчку в Gemfile требуемого приложения:

gem 'yaffle', git: 'git://github.com/yaffle_watcher/yaffle.git'

После запуска bundle install функционал вашего гема будет доступен в приложении.

Когда гем будет готов стать доступным в виде формального релиза, он может быть опубликован на RubyGems. Подробнее о публикации гемов на RubyGems смотрите: Creating and Publishing Your First Ruby Gem

7. Документация RDoc

Как только ваш плагин станет стабильным, и вы будете готовы его разместить, сделайте хорошее дело, документировав его! К счастью, написание документации для вашего плагина - это очень просто.

Первым шагом является обновление файла README детальной информацией о том, как использовать ваш плагин. Ключевые вещи, которые следует включить, следующие:

  • Ваше имя
  • Как установить
  • Как добавить функционал в приложение (несколько примеров обычных ситуаций использования)
  • Предупреждения, хитрости или подсказки, которые могут помочь пользователям и сохранить их время

Как только README готов, пройдитесь и добавьте комментарии rdoc ко всем методам, которые будут использовать разработчики. Также принято добавить комментарии '#:nodoc:' к тем частям кода, которые не включены в публичный API.

Как только ваши комментарии закончены, перейдите в директорию плагины и запустите:

$ bundle exec rake rdoc

7.1. Ссылки