Предыстория
Я программист с очень небольшим стажем (недавно накопилось около года в трудовой).
Около полугода назад я начал работать с 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. кажется, пост, подоходящий под формат личного мини-блога, слишком сильно растолстел в процессе написания.