Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Demystifying DSLs for better analysis and understanding

Demystifying DSLs for better analysis and understanding

Presented at RubyKaigi 2021

The ability to create DSLs is one of the biggest strengths of Ruby. They allow us to write easy to use interfaces and reduce the need for boilerplate code. On the flip side, DSLs encapsulate complex logic which makes it hard for developers to understand what’s happening under the covers.

Surfacing DSLs as static artifacts makes working with them much easier. Generating RBI/RBS files that declare the methods which are dynamically created at runtime, allows static analyzers like Sorbet or Steep to work with DSLs. This also allows for better developer tooling.

Ufuk Kayserilioglu

September 09, 2021
Tweet

More Decks by Ufuk Kayserilioglu

Other Decks in Programming

Transcript

  1. $

  2. create create README.md create Rakefile create .ruby-version create config.ru create

    .gitignore create .gitattributes create Gemfile run git init from "." ... create tmp $ rails new myapp $
  3. invoke active_record create db/migrate/20120909070000_create_users.rb create app/models/user.rb (myapp) $ bat app/models/user.rb

    bin/rails generate model User name:string role:string (myapp) $ ───────┬────────────────────────────────────────────────────────────────────── │ File: app/models/user.rb ───────┼────────────────────────────────────────────────────────────────────── 1 │ class User < ApplicationRecord 2 │ end ───────┴──────────────────────────────────────────────────────────────────────
  4. invoke active_record create db/migrate/20120909070000_create_users.rb create app/models/user.rb (myapp) $ bat app/models/user.rb

    bin/rails generate model User name:string role:string (myapp) $ ───────┬────────────────────────────────────────────────────────────────────── │ File: app/models/user.rb ───────┼────────────────────────────────────────────────────────────────────── 1 │ class User < ApplicationRecord 2 │ end ───────┴────────────────────────────────────────────────────────────────────── 🤔
  5. module Encryptable def self.included(mod) mod.extend(ClassMethods) end module ClassMethods def attr_encrypted(attr_name)

    attr_accessor(attr_name) encrypted_attr_name = "#{attr_name}_encrypted" define_method(encrypted_attr_name) do value = send(attr_name) encrypt(value) end define_method("#{encrypted_attr_name}=") do |value| send("#{attr_name}=", decrypt(value)) end end end private def encrypt(value) value.unpack("H*").first end def decrypt(value) [value].pack("H*") end end
  6. module Encryptable def self.included(mod) mod.extend(ClassMethods) end module ClassMethods def attr_encrypted(attr_name)

    attr_accessor(attr_name) encrypted_attr_name = "#{attr_name}_encrypted" define_method(encrypted_attr_name) do value = send(attr_name) encrypt(value) end define_method("#{encrypted_attr_name}=") do |value| send("#{attr_name}=", decrypt(value)) end end end private def encrypt(value) value.unpack("H*").first end def decrypt(value) [value].pack("H*") end end
  7. module Encryptable def self.included(mod) mod.extend(ClassMethods) end module ClassMethods def attr_encrypted(attr_name)

    attr_accessor(attr_name) encrypted_attr_name = "#{attr_name}_encrypted" define_method(encrypted_attr_name) do value = send(attr_name) encrypt(value) end define_method("#{encrypted_attr_name}=") do |value| send("#{attr_name}=", decrypt(value)) end end end private def encrypt(value) value.unpack("H*").first end def decrypt(value) [value].pack("H*") end end
  8. module Encryptable def self.included(mod) mod.extend(ClassMethods) end module ClassMethods def attr_encrypted(attr_name)

    attr_accessor(attr_name) encrypted_attr_name = "#{attr_name}_encrypted" define_method(encrypted_attr_name) do value = send(attr_name) encrypt(value) end define_method("#{encrypted_attr_name}=") do |value| send("#{attr_name}=", decrypt(value)) end end end private def encrypt(value) value.unpack("H*").first end def decrypt(value) [value].pack("H*") end end
  9. module Encryptable def self.included(mod) mod.extend(ClassMethods) end module ClassMethods def attr_encrypted(attr_name)

    attr_accessor(attr_name) encrypted_attr_name = "#{attr_name}_encrypted" define_method(encrypted_attr_name) do value = send(attr_name) encrypt(value) end define_method("#{encrypted_attr_name}=") do |value| send("#{attr_name}=", decrypt(value)) end end end private def encrypt(value) value.unpack("H*").first end def decrypt(value) [value].pack("H*") end end
  10. module Encryptable def self.included(mod) mod.extend(ClassMethods) end module ClassMethods def attr_encrypted(attr_name)

    attr_accessor(attr_name) encrypted_attr_name = "#{attr_name}_encrypted" define_method(encrypted_attr_name) do value = send(attr_name) encrypt(value) end define_method("#{encrypted_attr_name}=") do |value| send("#{attr_name}=", decrypt(value)) end end end private def encrypt(value) value.unpack("H*").first end def decrypt(value) [value].pack("H*") end end
  11. module Encryptable def self.included(mod) mod.extend(ClassMethods) end module ClassMethods def attr_encrypted(attr_name)

    attr_accessor(attr_name) encrypted_attr_name = "#{attr_name}_encrypted" define_method(encrypted_attr_name) do value = send(attr_name) encrypt(value) end define_method("#{encrypted_attr_name}=") do |value| send("#{attr_name}=", decrypt(value)) end end end private def encrypt(value) value.unpack("H*").first end def decrypt(value) [value].pack("H*") end end card = CreditCard.new card.number = "1234 5678 9012 3456" p card.number # = > "1234 5678 9012 3456" p card.number_encrypted # = > "31323334203536373820393031322033343536" card.number_encrypted = "33343536" p card.number # = > "3456"
  12. # message.rb require "smart_properties" class Message include SmartProperties property! :subject,

    accepts: String, default: "(No Subject)" property :body, accepts: String property :time, accepts: Time, default: - > { Time.now } end message = Message.new(subject: "New Message", body: "Lorem ipsum dolor sit amet") puts "Message '#{message.subject}' created at #{message.time} has body: #{message.body?}"
  13. # message.rb require "smart_properties" class Message include SmartProperties property! :subject,

    accepts: String, default: "(No Subject)" property :body, accepts: String property :time, accepts: Time, default: - > { Time.now } end message = Message.new(subject: "New Message", body: "Lorem ipsum dolor sit amet") puts "Message '#{message.subject}' created at #{message.time} has body: #{message.body?}" $ ruby message.rb message.rb:15:in `<main>': undefined method `body?' for #<Message:0x00007f88db853a48 @subject="New Message", @time=2021-09-09 07:00:00Z, @body="Lorem ipsum dolor sit amet"> (NoMethodError) Did you mean? body body=
  14. # message.rbi # typed: true class Message def subject; end

    def subject=(value); end def body; end def body=(value); end def time; end def time=(value); end end
  15. $ bundle exec srb tc message.rb:15: Method body? does not

    exist on Message https://srb.help/7003 15 |puts "Message '#{message.subject}' created at #{message.time} has body: #{message.body?}" ^^^^^^^^^^^^^ # message.rbi # typed: true class Message def subject; end def subject=(value); end def body; end def body=(value); end def time; end def time=(value); end end
  16. class Tapioca : : Compilers :: Dsl :: Base extend

    T :: Sig extend T :: Helpers abstract! sig { abstract.params(tree: RBI : : Tree, constant: T.untyped).void } def decorate(tree, constant); end sig { abstract.returns(T :: Enumerable[Module]) } def gather_constants; end # ... end
  17. class SmartPropertiesGenerator < Tapioca :: Compilers : : Dsl :

    : Base def decorate(root, smart_prop) properties = smart_prop.properties return if properties.keys.empty? root.create_path(smart_prop) do |klass| properties.values.each do |property| name = property.name.to_s klass.create_method(name) klass.create_method("#{name}=", parameters: [create_param("value", type: "T.untyped")]) end end end def gather_constants ObjectSpace.each_object(Class).select { |c| c < :: SmartProperties } end end
  18. # message.rbi # typed: true class Message sig { returns("T.untyped")

    } def subject; end sig { params(value: "T.untyped").returns("T.untyped") } def subject=(value); end sig { returns("T.untyped") } def body; end sig { params(value: "T.untyped").returns("T.untyped") } def body=(value); end sig { returns("T.untyped") } def time; end sig { params(value: "T.untyped").returns("T.untyped") } def time=(value); end end
  19. $ bin/tapioca dsl User Loading Rails application... Done Loading DSL

    generator classes... Done Compiling DSL RBI files... Wrote: sorbet/rbi/dsl/user.rbi Done All operations performed in working directory. Please review changes and commit them.
  20. # DO NOT EDIT MANUALLY # This is an autogenerated

    file for dynamic methods in `User`. # Please instead update this file by running `bin/tapioca dsl User`. # typed: true class User include GeneratedAttributeMethods module GeneratedAttributeMethods sig { returns(T.nilable( : : ActiveSupport :: TimeWithZone)) } def created_at; end sig { params(value: :: ActiveSupport :: TimeWithZone).returns( :: ActiveSupport :: TimeWithZone) } def created_at=(value); end # ... sig { returns(T.nilable( : : String)) } def name; end sig { params(value: T.nilable( :: String)).returns(T.nilable( :: String)) } def name=(value); end # ... sig { returns(T.nilable( : : String)) } def role; end sig { params(value: T.nilable( :: String)).returns(T.nilable( :: String)) } def role=(value); end # ... end