ActiveResource, prefix и вложенные ресурсы

в 0:37, , рубрики: active_resource, activeresource, redmine, redmine rest, rest api, ruby

Предыстория

Я программист с очень небольшим стажем (недавно накопилось около года в трудовой).
Около полугода назад я начал работать с Ruby (вне Rails) и сразу же познакомился с Active Resource и Redmine.

Это был очень интересный опыт, сейчас мне кажется, что Ruby — практически идеальный язык (именно язык, я не задаюсь вопросом потребления памяти и скорости работы).

Однако в нем весьма много магии, которую бывает сложно понять, когда читаешь исходный код сколько-нибудь крупных проектов (ActiveResource я отношу к ним, хотя по сравнению с rails, частью которого он является, этот гем кажется каплей в море).

Проблема

Проект заключался в создании консольной утилиты (Thor-based), работающей с Redmine REST API и предоставляющей всякие ништяки (кстати, вдохновленный проектом, я в данный момент работаю над подобной утилитой, частично дублирующей функционал: https://github.com/Nondv/redmine_cli).

Если посмотреть на документацию по Versions или Issue Relations (http://www.redmine.org/projects/redmine/wiki/Rest_IssueRelations), то можно обратить внимание, что для получения списка отношений используется адрес вида issues/<id>/relations.xml, а для конкретного объекта — relations/<id>.xml.

Собственно, для того, чтобы получить список, мы находим решение в виде использования prefix:

class Relation < ActiveResource::Base
  self.user = 'yet another apikey'
  self.password = 'we dont need password when using redmine apikey'
  self.site = 'www.yet-another-redmine.com'

  # Вот и он!
  self.prefix = '/issues/:issue_id/'
end

Relation.all params: { issue_id: 1 }

Все, вроде, шикарно и работает. Но что если нам нужно получить (ну или удалить) отдельный объект?
Relation.find(id) выдает исключение ActiveResource::MissingPrefixParam: project_id prefix_option is missing, что вполне разумно, мы ведь указали, что должны обращаться по адресу с префиксом.

Решение

Redmine REST не предусмотрел возможность получить отдельный объект во вложенных ресурсах.

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

class Issue < ActiveResource::Base
  self.user = 'yet another apikey'
  self.password = 'we dont need password when using redmine apikey'
  self.site = 'http://www.yet-another-redmine.com'

  # кстати, в случае с relations, их можно получить с помощью параметра include,
  # но тогда по незапомненным мною причинам пришлось изворачиваться
  def relations
     tmp_class = Class.new(ActiveResource::Base) do
          ...
          self.site = "http://www.yet-another-redmine.com/issues/#{id}/"
          self.element_name = 'relation'
      end

      tm_class.all
  end
end

Вы уже чувствуете этот странный запах, верно?
В общем, решение работало и в общем-то было принято, т.к. альтернатив я не смог предложить.

В личном проекте (ссылка выше) я получил возможность исправить это недоразумение (надеюсь, в лучшую сторону). Решение заключается в переопределении метода ActiveResource::Base.element_path. На примере Version:


class Version < ActiveResource::Base
  ...
  self.prefix = '/projects/:project_id/'

  #
  # Собственно, код был скопирован прямо из источника:
  # https://github.com/rails/activeresource/blob/master/lib/active_resource/base.rb#L760
  # и немного отредактирован
  #
  # Praise Open Source!
  #
  def self.element_path(id, _prefix_options = {}, query_options = nil)
    "/versions/#{URI.parser.escape id.to_s}#{format_extension}#{query_string(query_options)}"
  end
end

class Project < ActiveResource::Base
  ...

  def versions
    Version.all params: { project_id: id }
  end
end

Заключение

Собственно, к чему все это? Думаю, люди, которые разбираются в ActiveResource скажут, что это очевидное решение.

Суть в том, что я, будучи только погруженным во все это, столкнулся с проблемой и не смог ее решить, даже с помощью Всезнающего. В чем была моя ошибка? В том, что я побоялся немного разобраться в исходном коде и не захотел штудировать документацию (http://www.rubydoc.info просто находка!), в которой даже посмотрев summary можно было подобрать что-нибудь для решения задачи без привлечения нано-ядерно-магическо-костыльных технологий.

Надеюсь, что если кто-то окажется в моем положении, то он не станет повторять моих ошибок.
Буквально неделю назад видел утверждение, что Ларри Уолл считает лень одним из главных достоинств программиста. Не уверен, как было написано в оригинале кэмел-бука (а это именно оттуда, полагаю), но в переводе он использовал слово "добродетель", а не "достоинство".

Каким ленивым бы я ни был, это не сделает меня хорошим программистом. Лень далеко не всегда помогает находить решение.

P.S. кажется, пост, подоходящий под формат личного мини-блога, слишком сильно растолстел в процессе написания.

Автор: Sna1L

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js