RailsStuff — набор для разработки на рельсах

image

Недавно мы опубликовали гем 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-моделей: белый список типов, доступных для создания, автоматическое определение класса по названию из параметров и отдельные разрешенные параметры для каждого типа.


Примеры контроллеров из спеков RailsStuff
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 использует ограничения БД и автоматически перегенерирует значение, если запись не удалась. Это позволяет использовать такой генератор даже тогда, когда коллизии довольно вероятны. Вот как он работает:

  1. Запись сохраняется как обычно.
  2. Если поле уже имеет значение, то ничего не происходит.
  3. Генерирует значение и пробует сохранить запись.
  4. Если произошла ошибка 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. Больше подробностей, документация на английском и исходные коды — в репозитории на гитхабе.

© Habrahabr.ru