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

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

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

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

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

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

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

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

1. Настройка

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

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

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

$ rails plugin new yaffle

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

$ rails plugin new --help

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

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

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

  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

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

E

Error:
CoreExtTest#test_to_squawk_prepends_the_word_squawk:
NoMethodError: undefined method `to_squawk' for "Hello World":String


bin/test /path/to/yaffle/test/core_ext_test.rb:4

.

Finished in 0.003358s, 595.6483 runs/s, 297.8242 assertions/s.

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

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

В 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

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

  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

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

# Running:

..E

Error:
ActsAsYaffleTest#test_a_wickwalls_yaffle_text_field_should_be_last_tweet:
NameError: uninitialized constant ActsAsYaffleTest::Wickwall

bin/test /path/to/yaffle/test/acts_as_yaffle_test.rb:8

E

Error:
ActsAsYaffleTest#test_a_hickwalls_yaffle_text_field_should_be_last_squawk:
NameError: uninitialized constant ActsAsYaffleTest::Hickwall


bin/test /path/to/yaffle/test/acts_as_yaffle_test.rb:4



Finished in 0.004812s, 831.2949 runs/s, 415.6475 assertions/s.

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/rails db:migrate

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

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

class Hickwall < ApplicationRecord
  acts_as_yaffle
end

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

class Wickwall < ApplicationRecord
  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

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

class ApplicationRecord < ActiveRecord::Base
  include Yaffle::ActsAsYaffle

  self.abstract_class = true
end

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

# Running:

.E

Error:
ActsAsYaffleTest#test_a_hickwalls_yaffle_text_field_should_be_last_squawk:
NoMethodError: undefined method `yaffle_text_field' for #<Class:0x0055974ebbe9d8>


bin/test /path/to/yaffle/test/acts_as_yaffle_test.rb:4

E

Error:
ActsAsYaffleTest#test_a_wickwalls_yaffle_text_field_should_be_last_tweet:
NoMethodError: undefined method `yaffle_text_field' for #<Class:0x0055974eb8cfc8>


bin/test /path/to/yaffle/test/acts_as_yaffle_test.rb:8

.

Finished in 0.008263s, 484.0999 runs/s, 242.0500 assertions/s.

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, default: (options[:yaffle_text_field] || :last_squawk).to_s
      end
    end
  end
end

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

class ApplicationRecord < ActiveRecord::Base
  include Yaffle::ActsAsYaffle

  self.abstract_class = true
end

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

  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, default: (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

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

class ApplicationRecord < ActiveRecord::Base
  include Yaffle::ActsAsYaffle

  self.abstract_class = true
end

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

  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. Ссылки