Почти полное руководство по написанию Ruby гемов



Доброго времени суток, user.

Не так давно у меня возникла задача сделать прототип для одного проекта. В него входила работа с Facebook Graph API. Поковыряв некоторые гемы, я понял, что они для меня не совсем удобные или же реализуют нужный функционал уж слишком сложно. И тут в моей голове всплыла старая идея о написании своего гема. Загуглив массу запросов по этой теме, не нашел полной информации, тем более на русскоязычных ресурсах. Вот так и возникла идея этой статьи. Руководство названо «почти полным», так как тут освещены не все аспекты, а лишь те, которые минимально необходимы и желательны для начала существования продукта вашего воображения. Прошу под кат!

Начнем с базового (спасибо Википедии):
RubyGems (от англ. gem, gems— драгоценный камень) — система управления пакетами для языка программирования Руби который предоставляет стандартный формат для программ и библиотек Руби (в самодостаточном формате «gems»), инструменты, предназначенные для простого управления установкой «gems», и сервер для их распространения.


NOTE: Прошу учесть, что именно RubyGems, а не RailsGems или RoRGems!!! Это значит, что гемы пишутся непосредственно на чистом и блестящем Ruby, а то, что помечено как Rails Gem — всего лишь Ruby Gem, но использующий, либо дополняющий Rails (кстати, кто не в курсе, Rails — тоже Ruby Gem).

Я использую для разработки IDE от JetBrains — RubyMine (v6.3). В основном из-за удобного autocomplete и интеграции с Git, SFTP и других плюшек. Поэтому все махинации буду показывать именно в этой IDE. Для приверженцев Sublime/Vim/Emacs + Terminal есть куча инструкций по генерации gem-темплейтов через консоль.

Итак, начнем.

Я являюсь создателем, пускай пока сыроватого, но функционирующего гема Fobos (current v0.0.6). Все познания я взял из процесса его разработки.

Шаг 1: Создание болванки


В RubyMine это выглядит как создание нового проекта, но с выбором Poject Type: Ruby Gem.



Далее будет выбор версии Ruby. Что, в принципе, ясно и не требует объяснения.

В итоге получаем проект с такой структурой (я убрал галочки для создания bin и test директорий).



Рассмотрим по порядку всю структуру.

  • lib — непосредственно хранит весь код (ну или почти весь) вашего приложения;
  • .gitignore — хранит список файлов и папок, которые не должны попасть под контроль версий (файлы бд, скомпилированные ассеты, логи и т.п.);
  • Gemfile, Gemfile.lock — список других гемов, которые вы используете для работы или при разработке изображения. Но в случае гема они недоступны, т.к. они указываются в файлу *.gemspec;
  • LICENSE.txt — хранит лицензию использования. В моем случае — это MIT;
  • Rakefile — хранит Rake таски. Подробнее можно легко найти в интернете;
  • REAMDE,md — «лицо» вашего приложения. Выводится на стартовых страницах GitHub и RubyDoc;
  • SomeCoolGem.gemspec — некоторый манифест, в котором хранится основная информация и зависимости приложения.


Шаг 2: создание структуры для работы приложения


Теперь будем работать непосредственно с директорией lib.
Файл SomeCoolGem.rb является стартовым. По идее в нем должна храниться вся логика, но, как показывает практика, гораздо целесообразней создать модуль и разделить приложение на классы по функциональности, а затем просто подгружать их в этот файл. Для тех, кто знаком с языками C++/Java/C# — этот файл чем-то сродним методу main().

Сразу создадим модуль HelloWorld. И в нем создадим сласс Alloha.

Так как весь гем, по сути, есть модуль то уже в файле SomeCoolGem.rb будет следующий код:

require "SomeCoolGem/version"

module SomeCoolGem
  # Your code goes here...
end



Вот так изменилась структура проекта:



Вот класс Alloha:

module SomeCoolGem
  module HelloWorld
    class Alloha

    end
  end
end


Шаг 3: Делаем нужный функционал


Добавим в класс Alloha поле, конструктор и какие-нибуль методы для демонстрации:

module SomeCoolGem
  module HelloWorld
    class Alloha

      attr_accessor :name

      def initialize(name)
        @name = name
      end

      def generate_alloha
        "Alloha, #{@name}, from SomeCoolGem!"
      end

      def say_alloha
        puts generate_alloha
      end
    end
  end
end


Шаг 4: Проверяем работоспобность


Так как гем — это модуль, то использовать мы его можем тоже как модуль. Для проверки я обычно использую IRB.

Вот пример:

2.1.2 :001 > load 'alloha.rb'
 => true 
2.1.2 :002 > object = SomeCoolGem::HelloWorld::Alloha.new('User')
 => #<SomeCoolGem::HelloWorld::Alloha:0x00000000e7f950 @name="User"> 
2.1.2 :003 > object.generate_alloha
 => "Alloha, User, from SomeCoolGem!" 
2.1.2 :004 > object.say_alloha
Alloha, User, from SomeCoolGem!
 => nil 
2.1.2 :005 > 


Как видим, мало чем отличается от работы обычно проекта. Грубо говоря, при использовании гема ваше приложение просто подключает модуль с вложенными модулями/классами и вы можете использовать их как и обычные модули/класссы Ruby.

Шаг 5: Подключаем все к главному файлу


Для этого нужно немного изменить наш стартовый файл SomeCoolGem.rb:

require "SomeCoolGem/version"

module SomeCoolGem
  require 'SomeCoolGem/hello_world/alloha'
end



Теперь при указании нашего гема в Gemfile приложения мы сможем использовать его как обычный модуль Ruby.

Шаг 6: Сборка гема и его версионизация


В файле version.rb хранится текущая версия нашего гема:

module SomeCoolGem
  VERSION = "0.0.1"
end


Перед каждым релизом нужно будет менять это значение.

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

Чтобы собрать версию гема, вам нужно выполнить команду:

gem build SomeCoolGem.gemspec


В корне проекта появится файл SomeCoolGem-0.0.1.gem.

Поздравляю. Первая версия гема собрана!

Шаг 7: Пушим гем


Подробная и свежая инструкция всегда лежит здесь.

Так же поный набор инструкций находится здесь.

Там все хорошо и подробно описано.

Шаг 8: Просветление


Теперь я бы хотел поговорить о некоторых плюшках, которые многим интересны.

1. Можно проапдейтить профиль гема на RubyGems. Там есть настройки, в которых можно добавить описание, линки и прочее.
2. Бейджи. Бейджи заслуживают отдельной небольшой темы. Но в двух словах — это обычные картинки, которые вы вешаются в ваш README файл. Они отображают статус пройденных тестов на различных сервисах. Например, проверка чистоты кода. Более подробно можно прочитать здесь.
3. README файл вы должны составлять сами. Старайтесь указывать, какие версии с какими совместимы или нет. Если были сделаны глобальные изменения или закрыта часть старого функционала, меняйте не минорную версию, а постарше, в зависимости от глобальности изменений.
4. Описывайте качественную документацию. Зачастую именно от нее зависит желание использовать вашу наработку или нет.
5. Старайтесь реализовывать функционал с минимальными зависимостями от других гемов и следите за тем, чтобы использовать новые версии в своих наработках.

На этом, пожалуй, все. Удачи всем тем, кто начинает свой путь в этом направлении. Буду рад критике и уточнениям.

UPD:
Уделяйте больше времени файлам *.gemspec и README.

Для редактирования *.gemspec файла можно почитать статью вот здесь

Так же учтите, что все гемы, которые вы используете в своем приложении должны быть указаны тоже в файле *.gemspec
Поделиться публикацией

Похожие публикации

Комментарии 28
    +2
    Хочется добавить про волшебную команду bundle console, которая запускает irb с уже загруженным гемом.
      0
      Хм. Действительно. Спасибо) Просто первый раз от вас про нее слышу.
      +2
      Проверять работоспособность нужно тестами, а не в консоли.
        0
        Согласен. Но я считаю, что те, кто используют, если не TDD, а хоть просто тесты, то они в полной силе сами освоить и сделать это) Спасибо за комментарий.
          0
          Я думаю, что написать в консоли `bundle gem` может даже тот, кто не умеет писать тестов.
        +2
        Бейдж Travis CI для гема без тестов выглядит по меньшей мере странно. Для чего он?
        Собственно я сначала задался вопросом, почему вопрос тестирования в статье пропущен, потом глянул репозиторий — стало понятно.
        Ну и в принципе версионирование без тестирования регрессии попахивает.
          0
          Я указал, что гем развивается. Просто, конкретно я сам, не использовал TDD. Но сейчас как раз занимаюсь тестами. Гем сыроват, так что все делаю по порядку. Он был нужен для быстрого решения задачи. С этим он справился. В следующем релизе уже будет RSpec)
            +1
            Гем пускай развивается, это правильно. Но какой смысл релизить гем, который даже не бета?
            Вы же знаете, что необязательно релизить гем, чтобы его в Gemfile прицепить?
              0
              Знаю. Так и делал по началу. Но свелось к тому, что нужно было его использовать не на локальной машине. И в итоге принял решение сделать все вот так.
                +1
                А чем не устроило

                gem "gem-i-create", github: "mynameis/mygem"
                

                ?
                  0
                  Та вроде надо же обновлять его постоянно через bundle. Я изначально использовал path: и настроил SFTP с development сервером. Но это было не так удобно. Я понимаю, что это немного неправильный подход, но опять таки повторюсь, что изначально юзал для своих задач. И уже после задумался над его продвижением и развитием. Все же, спасибо большое за замечание, я их обязательно учту в следующих релизах и в будущем =)
                    0
                    Если гем всё-таки покинул локалхост и пошёл хотя бы в тестовое окружение, а уж тем более в продакшн, то указывать github в гемфайле не очень хорошо, так как в итоге остаётся привязка к репозиторию конкретного пользователя и если он уволился из компании и переименует/удалит репку, то будут проблемы. С RubyGems же удалить что-либо очень сложно.
                      0
                      Я имел в виду для целей тестирования во время разработки и интеграции в другие приложения) Я указывал сугубо в своем же прототипе.
                        0
                        А засорять rubygems непротестированными бета-версиями это отличный план, ага.
                          0
                          Согласен, бета-версиями засорять не стоит. Но первую рабочую версию, если она представляет какой-либо интерес для других разработчиков, выложить всё-таки стоит. Наверняка это сэкономит кому-то время на написании собственного велосипеда.
                          0
                          Ну пусть это будет гитхаб компании, какие проблемы-то?
                          Ну и ещё вариант — использовать какой-нибудь GemFury и публиковать гемы в свой приватный репозиторий, зачем их на паблик-то недоделанные такие светить? (Ну и плюс гемы не только общедоступные бывают...)
                            0
                            Прощу прощения, не хотел призывать к захламлению RubyGems недоделками. Как писал выше, имел цель сказать, что если есть минимальный функционал, достойный первой версии, то его стоит выложить в открытый доступ, конечно же если политика компании позволяет и нет прочих ограничений.
                              0
                              Вы уже донесли мою мысль. Спасибо большое)
                              0
                              В том была одна из целей. Предоставить его общественности. Никто же не засталяет людей использовать его, но если он их заинтересовал, то почему бы и нет.

                              Вот еще одна соль в том, что там скрывать то нечего. Он будет развиваться, а там может еще какие контрибьюторы подтянуться. В этом то суть опен-сорса.
                  0
                  .
                    0
                    Сугубо личное мнение, но всё же: для написания гемов вполне хватает 'bundle gem' и примеров исходников хороших уже существующих гемов (для RoR — это, например, rubocop; как бонус мы получаем хороший кодстандарт от товарища bbatsov). Если в существующих гемах ничего не понятно, то и свой лучше не писать по вполне понятным причинам. А если понятно, то и руководства другого не нужно.
                      0
                      В целом, не могу не согласиться. Но всплывает вопрос. Если вам нужно реализовать какую-то кастомную фичу, или же подход используемый в существующем решении вас не совсе устраивает, то разве вы не станете пытаться сделать свою реализацию?) Есть вариант делать обертки для существующих решений, но думаю в полне очевидно, что это чистой воды костыль.

                      P.S:Надеюсь, я правильно понял ваш комментарий.
                        0
                        Не совсем, я имел в виду саму структуру гема, способ организации файлов и способы патча уже существующих библиотек. Например, кто-то из старообрядцев пишет версию прямо в gemspec, кто-то использует init.rb в корне гема, кто-то пишет весь код внутри единственного файла 'lib/gem-name.rb', да и ещё много чего. Про реализацию самого функционала ничего говорить не хочу, ибо это уже творчество, которое сильно зависит от автора и его хода мыслей.
                          0
                          Вот с этим полностью согласен. Потому что видел пару случаев когда все описано в «lib/gem-name.rb». Считаю, что если гем реализует чуть более чем один вид функционала, то лучше разносить. Хотя я думаю, что и так оно неплохо работает)
                            0
                            Вы правы, если гем реализует чуть больше, чем один вид функционала, то это два гема. Швейцарские ножи среди гемов — это редкий зверь сомнительной ценности.

                            Ещё не хватает кодстандарта для гемов, а очень хотелось бы что-то типа такого. Сам, если что-то надо, читаю исходники популярных гемов, но, подозреваю, у начинающих с этим проблемы.
                              0
                              Как один из их числа, не могу с вами не согласиться)
                      +1
                      А еще файлы и директории должны именоваться в snake_case, т.е. не SomeCoolGem.rb, а some_cool_gem.rb.
                        0
                        Это я знаю. Провтыкал спецификацию. Извиня.сь перед всеми, кто перенял эту мою ошибку.

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое