RailsStuff — набор для разработки на рельсах
Недавно мы опубликовали гем RailsStuff. Это коллекция небольших модулей и утилит для выполнения самых разных частых задач: от организации контроллеров и генерации уникальных случайных значений до парсера параметров и хэлперов переводов. В этом посте я расскажу про некоторые из них:
- ResourcesController — облегчённая и современная версия InheritedResources;
- Трекер типов;
- Генератор уникальных случайных значений;
- Хэлперы переводов и основных ссылок.
Установка
Большинство модулей слишком маленькие, чтобы быть отдельным гемом, и для удобства было решено объединить их в один гем. При этом используется ленивая загрузка: если вы не используете какой-то модуль, он и загружен не будет. Часть модулей используются напрямую через include/extend, другие добавляют методы класса для активации и настройки. Для вторых в геме есть Rails::Engine, который автоматически загрузит их в базовые классы моделей и контроллеров (ActiveRecord::Base и ActionController::Base). Выбрать отдельные модули или отключить автоматическую загрузку полностью можно, добавив initializer:
# Не использовать автоматическую загрузку:
RailsStuff.load_modules = []
# Загрузить только определенные модули:
RailsStuff.load_modules = %i(sort_scope statusable)
Гем не имеет зависимостей, но для некоторых модулей (или даже отдельных методов) надо будет установить что-нибудь ещё. Для таких модулей зависимости указаны в документации.
ResourcesController
Это один из основных модулей в RailsStuff. Фактически, он является упрощенной и обновлённой версией InheritedResources.
InheritedResources показал мне отличный подход к написанию контроллеров. Он реализовал многие функции, которые я искал или делал сам, например:
- Доступ к ресурсам в разных контроллерах по одному имени. Хорошо, когда у тебя переменная
@user
, но уже хуже, если@payment_transaction
. Ещё хуже, если@recurring_payment_transaction
. Кто-то в таком случае начинает сокращать имена, но от этого обычно становится ещё хуже. - Отсутствие повторяющихся
before_action :set_user
/before_action :find_manager
. С длинными спискамиonly
илиexcept
, которые нужно не забывать поддерживать в актуальном состоянии. - И, конечно, меньше дублирования кода экшенов и файндеров. Да, рельсы генерируют его за вас, но он всё равно остаётся кодом в вашем репозитории, и он повторяется. При редактировании приходится читать весь код: сразу сложно сказать, что перед тобой — оригинальный код из генератора, или он уже был редактирован.
InheritedResources позволяет избежать этих проблем и сосредоточиться на бизнес-логике приложения. Ho библиотека слишком перегружена, и достаточно часто приходится искать подходящие комбинации настроек или лезть в исходные коды за названием метода, который надо переопределить, или смотреть, как именно что-то реализовано. К тому же библиотека находится в статусе deprecated уже около года. Поэтому новые проекты было решено начинать без использования InheritedResources.
Так, после использования своих модулей в нескольких новых приложениях, мы выделили их в ResourcesController, который является основной частью RailsStuff. Это облегчённая версия InheritedResources, предоставляющая базовые возможности, и в ней отсутствуют некоторые сложные функции. Но это позволяет сделать код проще и яснее как в библиотеке, так и в приложениях. Все методы в библиотеке максимально просто выполняют отдельные функции. Основной принцип — не предоставить множество настроек на все возможные случаи, а обеспечить простым, расширяемым базовым функционалом. Если требуется особенное поведение, просто напишите метод так, как вы делали бы это без ResourcesController.
Вот основные отличия:
- Одинаковые названия переменных для всех контроллеров:
@_resource
и@_collection
. Нет необходимости в громоздких конструкциях изset_collection_ivar
иget_resource_ivar
. - Отсутствуют
belongs_to
и цепочки отношений. Но естьresource_helper
и быстрый способ установитьsource_relation
:resources_controller source_relation: -> { manager.projects } # Это добавит метод #manager, который находит менеджера по params[:manager_id]: resource_helper :manager
- Улучшенная интеграция со StrongParameters (хэлпер вместо странного
def permited_params
):permit_attrs :name, project_attributes: [:id, :_destroy, :name]
respond_with
используется только в create, update, destroy. В целом, это не должно быть проблемой: переопределение.to_json
— не очень хорошая идея для форматирования ответов для API Но если это действительно необходимо, то можно допатчить модуль и добавить нужные экшены.- Хэлперы для STI-моделей: белый список типов, доступных для создания, автоматическое определение класса по названию из параметров и отдельные разрешенные параметры для каждого типа.
module Site
class UsersController < SiteController
resources_controller
permit_attrs :name, :email
end
class ProjectsController < SiteController
resources_controller sti: true
resource_helper :user
permit_attrs :name
permit_attrs_for Project::External, :company
permit_attrs_for Project::Internal, :department
def create
super(action: :index)
end
protected
def after_save_url
url_for action: :index, user_id: resource.user_id
end
def source_relation
params.key?(:user_id) ? user.projects : self.class.resource_class
end
end
end
Трекер типов
Часто при использовании STI-моделей в интерфейсе надо вывести список всех возможных типов. В таких ситуациях ленивая загрузка мешает, и нужно явно вызвать require_dependency/eager_load
для всех наследующих моделей. Да и DescendantsTracker для получения списка всех унаследованных классов в этом случае — не самое лучшее решение. Для таких задач мы используем TypesTracker. Он содержит хэлпер для загрузки всех типов для модели и хранит отдельный массив со всеми унаследованными классами: список готов и доступен в любой момент. При этом выборочно можно убрать некоторые классы из этого списка.
class Project
extend RailsStuff::TypesTracker
# ...
eager_load_types! # загрузит все .rb в app/models/project
# или указать директорию явно:
eager_load_types! 'lib/path/to/projects'
end
class Project::Big < Project
unregister_type # Убираем этот класс из списка
end
class Project::Internal < Project::Big; end
class Project::External < Project::Big; end
class Project::Small < Project; end
Project.types_list # [Internal, External, Small]
Если массив заменить кастомным хранилищем, то можно задавать флаги для каждого класса и фильтровать/сортировать по ним при выводе:
class Project
extend RailsStuff::TypesTracker
# MyTaggedArray должен предоставлять #add(klass, *args).
# В примере *args - фактически *tags.
self.types_list_class = MyTaggedArray
end
class Project::Internal < Project::Big
# Вызывает types_list.add Project::Internal, :tag_1
register_type :tag_1
end
class Project::External < Project::Big
register_type :tag_2
end
Project.types_list.with(:tag_1)
Генератор случайных уникальных значений
Иногда требуется для создаваемой записи генерировать уникальное значение. Теоретически, UUID предоставляет такую возможность, но иногда значение надо генерировать в соответствии с шаблоном, и возможны коллизии. RandomUniqAttr использует ограничения БД и автоматически перегенерирует значение, если запись не удалась. Это позволяет использовать такой генератор даже тогда, когда коллизии довольно вероятны. Вот как он работает:
- Запись сохраняется как обычно.
- Если поле уже имеет значение, то ничего не происходит.
- Генерирует значение и пробует сохранить запись.
- Если произошла ошибка
RecordNotUnique
, повторяет предыдущий шаг.
У такого подхода есть условие: поле должно быть объявлено nullable. Несмотря на это, поле не будет иметь NULL после завершения транзакции.
# По-умолчанию используется SecureRandom.hex(32)
random_uniq_attr :token
# С кастомным генератором:
random_uniq_attr(:code) { |instance| my_random(instance) }
Хэлперы
Для мультиязычных интерфейсов в RailsStuff есть хэлперы для переводов названий действий и подтверждений. Эти методы кэшируют переводы на время запроса, и для больших списков и таблиц поиск перевода будет выполнен только один раз. Перед использованием нужно добавить в файлы переводов секции helpers.actions
и/или helpers.confirmations
:
ru:
helpers:
actions:
edit: Редактировать
delete: Удалить
confirm: Точно?
confirmations:
delete: Не пожалеете?
Теперь во всех шаблонах, можно начать переводить действия одинаково:
# в хэлпер добавить модуль:
# include RailsStuff::Helpers::Translation
= translate_action(:edit) or translate_action(:delete)
- collection.each do |resource|
tr
td= resource.name
td= link_to 'x', url_for(resource),
method: :delete, data: {confirm: translate_confirmation(:delete)}
= translate_confirmation(:purge_all) # Фолбэк: 'Точно?'
Для того, чтобы в приложении все ссылки на основные действия были оформлены одинаково, в RailsStuff есть хэлперы для них:
# Настройки в хэлпере.
# Подробности можно посмотреть в модуле rails_stuff/helpers/links
include RailsStuff::Helpers::Links
ICONS = {
destroy: '<span class="glyphicon glyphicon-trash"></span>'.html_safe,
edit: 'Редактировать',
new: -> { translate_action(:new) },
}
def basic_link_icons
ICONS
end
# Использование:
link_to_edit([:edit, :scope, resource]) or link_to_edit(edit_path)
link_to_edit('url', class: 'text-info')
# Если url не указан, используется url_for(action: edit).
# Так на странице /users/1
link_to_edit # будет вести на '/users/1/edit'
# По такому же принципу работают:
link_to_destroy or link_to_new
По умолчанию в качестве текста для ссылок используются переводы экшенов. Для пользователей Bootstrap или Glyphicon есть хэлпер RailsStuff::Helpers::Bootstrap, который использует иконки для этих ссылок.
Что ещё можно найти?
- «nullify blanks»;
- модуль для работы со статусами/перечислениями (как AR::Enum, только лучше);
- обработку параметров для сортировки;
- хэлпер
response.json_body
для тестирования API; - даже хэлпер для дебага
Net::HTTP
; - и другое.
Попробовать гем можно, добавив gem 'rails_stuff', '~> 0.4.0'
в Gemfile. Больше подробностей, документация на английском и исходные коды — в репозитории на гитхабе.