Пишем простой DSL на Ruby
Трудно найти хоть одного рубиста, ни разу не использовавшего какой-либо DSL: Sinatra, RSpec, ActiveModel — все они так или иначе являются примерами сей замечательной штуки.
Для лучшего понимания вещей, которые лежат в их основе, мы с вами склепаем небольшой класс, позволяющий объявлять хеши вот таким образом:
Основа
Объявим DSLHash, а в нем пустой статический метод build:
В дальнейшем этот метод будет возвращать нам готовый хеш.
Блоки, Proc’ы, и так далее
Для начала определим в build, действительно ли нам скормили блок. Для этого в рубях есть метод “block_given?” Изменим наш build следующим образом:
Почему не “yield”? Про это ниже.
instance_eval и method_missing
Возможно, вы видели возникающий, казалось бы, из ниоткуда params в Rails или Sinatra, содержащий параметры запроса, но явно в функцию не передаваемый. Что за магия? А магия эта именуется instance_eval
instance_eval позволяет нам запускать код в контексте определенного инстанса класса. В этом и есть главное отличие от yield, запускающего блок в текущем контексте.
Добавим вложенный класс в DSLHash:
В нем мы объявляем пустой хеш и геттер для него.
Посмотрим на наш пример и подумаем, чего же нам не хватает. Например, до сих пор не ясно, как first_name “Andrey” превратится в :first_name => “Andrey” внутри хеша.
Тут нам поможет своеобразный коллбек method_missing, который нужно задать в нашем классе-контексте. Каждый раз, когда мы вызываем несуществующий метод, Ruby будет вызвать method_missing. Конечно, если он есть.
Добавим его в наш DSLHashContext:
В name нам заботливо прилетит символ (для тех, кто не знает что эта штука называется символами, выглядит оно так: :first_name), равный имени вызванного метода.
В args нас будут ждать аргументы, а в block — соответственно, блок.
И так, теперь каждый раз, когда внутри DSLHashContext мы будем вызывать несуществующий метод, в консоль будет писаться его имя.
Теперь можно перейти и к instance_eval. Изменим наш метод build следующим образом:
Теперь мы можем попробовать запустить пример из начала статьи. И увидим мы вот что:

Прикольно, но мы не видим того, что прячется внутри social. Немного изменим method_missing:
И вот, совсем другое дело:

Скучные мелочи
Про method_missing, block_given? и instance_eval я вам уже рассказал, осталось из всего что у нас есть “добить” наш DSLHash до запланированного функционала.
Вся магия будет происходить внутри method_missing, вот так он будет выглядеть после всех метаморфоз:
Контрольный прогон:

Just as planned 👌
Репозиторий GitHub с полным кодом из этой статьи и парочкой дополнительных примеров: https://github.com/4ndv/dslhash
Угостить меня чаем или чем покрепче всегда можно тут: http://andv.xyz/buymeacoffee