Bundler появился в Ruby значительно позже RubyGems. Из-за этого устройство библиотек (гемов) и проектов немного отличается. В этом уроке мы немного заглянем внутрь гемов, чтобы понять принципы их работы. Изучим команду bundle gem
, которая создает структуру для нового гема и которая, с небольшими модификациями, подходит для своих проектов. Она в себя уже включает настроенный линтер, тесты и интеграцию с Github Actions.
Для работы прикладных проектов в Ruby ничего кроме Bundler не нужно. Достаточно создать Gemfile, указать зависимости и подключить Bundler в своем коде. Дальше можно не отвлекаться и писать прикладной код.
Для гемов все чуть сложнее. У гемов есть метаданные, например, версия, имя, описание и т.п. У гемов есть зависимости, которые должны учитываться RubyGems при установке. Для всего этого нужен специальный файл, называемый спецификацией гема. Кроме того, у гемов есть определенные соглашения по организации кода, которые важны для удобной работы c require
.
Гем можно создать полностью руками, но лучше воспользоваться готовой командой bundle gem
, которая сделает все сама. Ниже мы пройдемся по всем этапам создания гема и изучим организацию кода внутри. Все это поможет понять работу Ruby и научит эффективно организовывать разработку как гемов, так и своих проектов.
bundle gem
Начнем с генерации. Для этого запустим команду bundle gem
, которая задаст несколько вопросов и затем создаст директорию с названием гема и нужными файлами внутри.
# hexletgem - произвольное имя гема
bundle gem hexletgem
Creating gem 'hexletgem'...
Do you want to generate tests with your gem?
Future `bundle gem` calls will use your choice. This setting can be changed anytime with `bundle config gem.test`.
# Выбираем как фреймворк либо rspec либо minitest
# Здесь рассматриваем minitest, чтобы лучше разобраться в основах
Enter a test framework. rspec/minitest/test-unit/(none): minitest
Do you want to set up continuous integration for your gem? Supported services:
* CircleCI: https://circleci.com/
* GitHub Actions: https://github.com/features/actions
* GitLab CI: https://docs.gitlab.com/ee/ci/
* Travis CI: https://travis-ci.org/
Future `bundle gem` calls will use your choice. This setting can be changed anytime with `bundle config gem.ci`.
# Выбираем Github Actions как самый удобный вариант при хостинге на Github
Enter a CI service. github/travis/gitlab/circle/(none): github
# Да, если ваш код не коммерческий
Do you want to license your code permissively under the MIT license?
This means that any other developer or company will be legally allowed to use your code for free as long as they admit you created it. You can read more about the MIT license at https://choosealicense.com/licenses/mit. y/(n): y
# Полезная штука, выбираем да
Codes of conduct can increase contributions to your project by contributors who prefer collaborative, safe spaces. You can read more about the code of conduct at contributor-covenant.org. Having a code of conduct means agreeing to the responsibility of enforcing it, so be sure that you are prepared to do that. Be sure that your email address is specified as a contact in the generated code of conduct so that people know who to contact in case of a violation. For suggestions about how to enforce codes of conduct, see https://bit.ly/coc-enforcement. y/(n): y
# Тут смотрите сами
Do you want to include a changelog?
A changelog is a file which contains a curated, chronologically ordered list of notable changes for each version of a project. To make it easier for users and contributors to see precisely what notable changes have been made between each release (or version) of the project. Whether consumers or developers, the end users of software are human beings who care about whats in the software. When the software changes, people want to know why and how. see https://keepachangelog.com y/(n): y
# Линтер нужен обязательно, ставим rubocop
Do you want to add a code linter and formatter to your gem? Supported Linters:
* RuboCop: https://rubocop.org
* Standard: https://github.com/testdouble/standard
Future `bundle gem` calls will use your choice. This setting can be changed anytime with `bundle config gem.linter`.
Enter a linter. rubocop/standard/(none): rubocop
# Немного изучите структуру, посмотрите содержимое файлов
Initializing git repo in /private/tmp/hexletgem
create hexletgem/Gemfile
create hexletgem/lib/hexlet/example.rb
create hexletgem/lib/hexlet/example/version.rb
create hexletgem/hexletgem.gemspec
create hexletgem/Rakefile
create hexletgem/README.md
create hexletgem/bin/console
create hexletgem/bin/setup
create hexletgem/.gitignore
create hexletgem/test/test_helper.rb
create hexletgem/test/hexlet/example_test.rb
create hexletgem/.github/workflows/main.yml
create hexletgem/LICENSE.txt
create hexletgem/CODE_OF_CONDUCT.md
create hexletgem/CHANGELOG.md
create hexletgem/.rubocop.yml
Gem 'hexletgem' was successfully created. For more information on making a RubyGem visit https://bundler.io/guides/creating_gem.html
Спецификация
Спецификация гема описывается Ruby кодом. Внутри нее что-то уже заполнено автоматически, но есть места где стоит TODO. Их нужно заполнить, иначе с кодом не получится работать, он начнет просить заполнения.
# Показана только та часть где есть TODO
Gem::Specification.new do |spec|
spec.summary = "TODO: Write a short summary, because RubyGems requires one."
spec.description = "TODO: Write a longer description or delete this line."
spec.homepage = "TODO: Put your gem's website or public repo URL here."
spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
spec.metadata["homepage_uri"] = spec.homepage
spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here."
spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
end
Для работы с гемом нужно сделать две вещи:
- Заполнить summary и description. Для простоты просто сотрите TODO.
- Все остальные TODO закомментировать включая homepage_uri, который ссылается на homepage
Зависимости
RubyGems в своей работе опирается только на спецификацию гема. Сюда входят и зависимости. То есть все что нужно для работы гема, должно быть указано в gemspec-файле:
Gem::Specification.new do |spec|
# Uncomment to register a new dependency of your gem
# spec.add_dependency "example-gem", "~> 1.0"
# spec.add_dependency "another-gem", "> 3.2"
end
Здесь же указывались и зависимости для разработки, но с появлением Bundler это изменилось. Все зависимости, необходимые во время разработки и тестирования, переехали в Gemfile.
source 'https://rubygems.org'
# Specify your gem's dependencies in hexletgem.gemspec
gemspec
gem 'rake', '~> 13.0'
gem 'minitest', '~> 5.0'
gem 'rubocop', '~> 1.21'
И чтобы не дублировать зависимости самого гема, в Bundler добавили функцию gemspec
(в примере выше), которая подтягивает рантайм-зависимости из спецификации в Gemfile.
Следующий шаг в разработке гема — установка зависимостей. Для этого выполняется команда bin/setup
:
bin/setup
bundle install
+ bundle install
Fetching gem metadata from https://rubygems.org/.........
Resolving dependencies...
Using rake 13.0.6
Using ast 2.4.2
Using bundler 2.2.32
Using hexletgem 0.1.0 from source at `.`
Using minitest 5.15.0
Using rexml 3.2.5
Using unicode-display_width 2.1.0
Using parallel 1.21.0
Using parser 3.1.1.0
Fetching rainbow 3.1.1
Using ruby-progressbar 1.11.0
Fetching regexp_parser 2.2.1
Fetching rubocop-ast 1.16.0
Installing rubocop-ast 1.16.0
Installing regexp_parser 2.2.1
Installing rainbow 3.1.1
Using rubocop 1.25.1
Bundle complete! 4 Gemfile dependencies, 14 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
# Do any other automated setup that you need to do here
После установки зависимостей можно начинать разрабатывать, запускать линтер, тесты и другие команды. Начнем с линтера. У Rubocop есть свой исполняемый файл rubocop. Каким образом его запустить? Правильный способ через команду bundle exec. Эта команда запускает исполняемые файлы из зависимостей текущего проекта игнорируя глобально установленные программы.
bundle exec rubocop
Inspecting 8 files
......C.
Offenses:
test/hexletgem_test.rb:5:7: C: [Correctable] Style/ClassAndModuleChildren: Use nested module/class definitions instead of compact style.
class HexletgemTest < Minitest::Test
^^^^^^^^^^^^^^^^^^^
8 files inspected, 1 offense detected, 1 offense auto-correctable
Точно так же запускаются любые другие утилиты, которые устанавливаются как зависимости в проект.
Rake
Еще один важный элемент экосистемы Ruby - Rake. Rake это Ruby + Make, фреймворк для описания и запуска разнообразных задач связанных с проектом. Список доступных задач можно посмотреть через утилиту rake
:
# -T - список задач
bundle exec rake -T
rake build # Build hexletgem-0.1.0.gem into the pkg directory
rake build:checksum # Generate SHA512 checksum if hexletgem-0.1.0.gem into the checksums directory
rake clean # Remove any temporary products
rake clobber # Remove any generated files
rake install # Build and install hexletgem-0.1.0.gem into system gems
rake install:local # Build and install hexletgem-0.1.0.gem into system gems without network access
rake release[remote] # Create tag v0.1.0 and build and push hexletgem-0.1.0.gem to TODO: Set to your gem server...
rake rubocop # Run RuboCop
rake rubocop:auto_correct # Auto-correct RuboCop offenses
rake test # Run tests
Эти задачи не встроены в rake
, они добавлены, во время генерации гема, в файл Rakefile.
# Задачи связанные с публикацией гема
require "bundler/gem_tasks"
# Подгружает Rake::TestTask, которым описывается запуск тестов ниже
require "rake/testtask"
# Задача rake test для запуска тестов
Rake::TestTask.new(:test) do |t|
t.libs << "test"
t.libs << "lib"
t.test_files = FileList["test/**/*_test.rb"]
end
# Задачи для запуска линтера
require "rubocop/rake_task"
# Задача по умолчанию, запускается так: bundle exec rake
task default: %i[test rubocop]
Часть задач подгружается из гемов, но некоторые описаны прямо здесь, например запуск тестов. Rubocop тоже присутствует здесь, то есть линтер можно запускать и так:
# В данном случае идентично bundle exec rubocop
bundle exec rake rubocop
......C.
Rake важная часть экосистемы Ruby. Практически ни один проект не обходится без него. Например, в Rails из коробки идет несколько десятков таких команд. Плюс всегда можно написать свои.
Структура проекта
Код гема находится в директории lib и организован определенным образом:
lib
├── hexletgem
│ └── version.rb
└── hexletgem.rb
Внутри lib лежит Ruby-файл и директория по имени гема. Ruby-файл hexletgem.rb содержит входной модуль в гем, в нашем случае он называется Hexletgem:
require_relative 'hexletgem/version'
module Hexletgem
class Error < StandardError; end
# Your code goes here...
end
Именно через этот модуль пользователи гема будут получать доступ к его содержимому. Это важное соглашение, которое позволяет интуитивным образом понимать, как можно взаимодействовать с гемом после его включения в проект:
Имя гема | Имя модуля | Имя файла |
---|---|---|
rake | Rake | lib/rake.rb |
faraday | Faraday | lib/faraday.rb |
faker | Faker | lib/faker.rb |
slim_lint | SlimLint | lib/slim_lint.rb |
web-console | Web::Console | lib/web/console.rb |
Директория lib/hexletgem содержит файл version.rb в котором указана версия гема:
module Hexletgem
VERSION = "0.1.0"
end
Все остальные файлы с кодом располагают рядом с этим файлом, внутри директории hexletgem и ее поддиректориях.
Превращение гема в проект
Генерация гема создает удобную структуру не только для разработки гемов, но и для прикладных проектов. Для готовности нужно сделать два шага:
- Удалить файл с версией
- Удалить gemspec-файл
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
- Статья «Как учиться и справляться с негативными мыслями»
- Статья «Ловушки обучения»
- Статья «Сложные простые задачи по программированию»
- Вебинар «Как самостоятельно учиться»
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.