DSL For DSL (Draft)

张金柱 @  ThePlant
Github / jinzhu
[email protected]



(Hit Esc For Overview, Navigate with and )

What IS DSL?


特定领域语言(Domain Specific Languages)

用于解决特殊领域中特殊问题表示技术和解决方案的程序设计语言

WHAT IS DSL USED FOR?


随机调查了100个程序
87.53%的程序使用DSL做配置文件
(来自统计局神奇数字)
例如Bundler, Unicorn...

配置文件用来做什么?


存数据 & 取数据

So, what is DSL?


一种让你更方便的存数据,取数据的语言

(大多情况下。。。)

实例1: Gemfile

                gem "rails", :git => "git://github.com/rails/rails.git"
gem 'capistrano-confirm', '>= 0.0.6'
gem 'paygent', :path => '~/Develop/paygent'

path "~/Develop/qor_dsl" do
  gem "qor_dsl"
end

git "git://github.com/rails/rails.git" do
  gem "activesupport"
  gem "actionpack"
end

group :development do
  gem 'pry-rails'
end

group :test do
  gem "timecop"
end

            


实例 1 解决方案


???

传统解决方案

  1. 建一个class
  2. 实现实例方法 gem, path, git, group,把调用参数存到实例变量
  3. path, git, group中的block中的仍有gem方法, 并需要保存父级的参数
  4. 抽象class
  5. 读取文件, instance_eval,搞定,收工!

代码呢? 太长了,一个slide写不开。。。

DSL FOR DSL解决方案

                    class Gemfile::Configuration
  include Qor::Dsl
  default_configs [ENV['BUNDLE_GEMFILE'], 'Gemfile']

  node :gem

  [:git, :path, :group].map do |name|
    node name do
      node :gem
    end
  end
end
              

查询数据(1)

                        # 找到第一个节点
first_node = Gemfile::Configuration.first('gem')
#=> #<Qor::Dsl::Node:0x3fb8 {name: "rails", config: :gem}>

first_node.name
#=> rails

first_node.options
#=> {git: "git://github.com/rails/rails.git"}

# 找到第一个path节点对象
first_path = Gemfile::Configuration.first('path')
#=> <Qor::Dsl::Node:0x3fd2100 {name: "~/Develop/qor_dsl", config: :path}>

# 嵌套查找
qor_dsl = first_path.first('gem', 'qor_dsl')
#=> <Qor::Dsl::Node:0x3fe4894dc288 {name: "qor_dsl", parent: "{path: ~/Develop/qor_dsl}", config: :gem}>

            

查询数据(2)

                        # 查找父节点
qor_dsl.parent
#=> <Qor::Dsl::Node:0x3fd2100 {name: "~/Develop/qor_dsl", config: :path}>

# 取出children
first_path.children
#=> [<Qor::Dsl::Node:0x3fd2100 {name: "~/Develop/qor_dsl", config: :path}>]

# 取出第一级的Gems
Gemfile::Configuration.find('gem').map(&:name)
#=> ["rails", "capistrano-confirm", "paygent"]

Gemfile::Configuration.find('gem')[-1].name
#=> paygent

            

查询数据(3)

                        # 所有级别的Gems
Gemfile::Configuration.deep_find('gem').map(&:name)
#=> ["rails", "capistrano-confirm", "paygent", "qor_dsl", "activesupport", "actionpack", "pry-rails", "timecop"]


# 取出所有Development环境下的Gem
Gemfile::Configuration.deep_find('gem') do |node|
  parent = node.parent
  parent.root? ||
    !parent.is_node?(:group) ||
    parent.is_node?(:group, :development)
end.map(&:name)
#=> ["rails", "capistrano-confirm", "paygent", "qor_dsl", "activesupport", "actionpack", "pry-rails"]

            

实例2:Nagios.rb

                        contact_emails ["[email protected]"]
mail do
  address 'smtp.example.com'
end

server 'server1' do
  host '100.200.200.10'
  username 'app'

  check_disk
  check_load :warning => '8,5,5', :critical => '20,15,10'
  check_memcached :port => 11211, :host => '10.0.0.1'
  check_mysql_slave :user => 'nagios_check', :password => 'xxxxxx'
end

server 'server2' do
  contact_emails ["[email protected]"]
  host '111.111.111.11'
  username 'app'
  port 222

  check_disk :warning => '10%', :critical => '8%'
  check_load :warning => '8,5,5', :critical => '20,15,10'
end
            


实例 2 解决方案


???

传统解决方案


  1. 建一个class
  2. 实现实例方法 contact_emails, mail, server,把调用参数存到实例变量
  3. 抽象class, 实现对mail的block的支持
  4. 抽象class, 实现对server的block的支持
  5. 读取文件, instance_eval,搞定,收工!

DSL FOR DSL解决方案

                        class Nagios::Configuration
  include Qor::Dsl
  node :contact_emails

  node :mail do
    node :address
  end

  node :server do
    node :contact_emails, inherit: true
    node :host
    node :port, default_value: 22
    node :username, default_value: 'app'

    node :check_disk, default_options: {warning: '10%', critical: '8%'}
    node :check_load
    node :check_memcached
    node :check_mysql_slave
  end
end
              

查询数据

                        # 继承父级
Nagios::Configuration.first('server', 'server1').first(:contact_emails).value
#=> "[email protected]"

# 重写继承
Nagios::Configuration.first('server', 'server2').first(:contact_emails).value
#=> [email protected]

# 默认值
Nagios::Configuration.first('server', 'server1').first(:port).value
#=> 22

Nagios::Configuration.first('server', 'server1').first(:check_disk).options
#=> {warning: '10%', critical: '8%'}

            


实例1,实例2传统解决方案总结


简单?


相似!


枯燥 && 乏味 && 重复


Don't Repeat Yourself!


DSL FOR DSL

Meta Programming?

Writing Code That Writes Code



DSL FOR DSL

Writing DSL That Writes DSL

幕后英雄 - Qor DSL


  • 从程序中抽象而来
  • 减少重复工作(DRY)

如何使用

  1. 将qor_dsl加入Gemfile 或 Gemspec中
  2. 创建一个Configuration的class
  3. include Qor::DSL
  4. 定义存数据
  5. 然后取数据



用例1:Gemfile.rb
用例2:Nagios.rb
更多    :README
更更多:GITHUB源码

API

            = Class methods:
first
find
deep_find

default_configs
load

= Instance methods:
name
options
default_options
value
default_value
block
default_block

parent
children
root
root?
dummy?
is_node?

            

Qor DSL可用来做什么?

  • 做程序的配置文件
  • 把程序做成配置文件

Sinatra的样例

require 'sinatra'

get '/hi' do
  "Hello World!"
end

            

Qor DSL & Rack的实现

# 代码实现
require 'qor_dsl'

class SimpleWeb
  include Qor::Dsl
  node :get
end

run proc { |env| [200, {'Content-Type' => 'text'}, [SimpleWeb.first(:get, env["PATH_INFO"]).block.call]] }

# 路由
SimpleWeb.load do
  get '/hi' do
    'Hello world!'
  end
end

            

稍复杂一点


get '/hi' do
  "Hello #{env["PATH_INFO"]}"
end

            
class SimpleWeb
  class Configuration
    include Qor::Dsl
    node :get
  end

  class Request
    attr_accessor :env

    def initialize env
      self.env = env
    end

    def body
      instance_eval(&SimpleWeb::Configuration.first(:get, env["PATH_INFO"]).block)
    end

    def call
      [200, {'Content-Type' => 'text'}, [body]]
    end
  end
end

run proc { |env| SimpleWeb::Request.new(env).call }

            

再复杂一点

class Available
  def initialize(url, option)
    @url, @option = url, option
  end

  def match(url)
    (@url == url) &&
      (@option[:from]..@option[:to]).include?(Time.now.hour)
  end
end

get %r(/hello/(.*)) do
  "Hello World!"
end

get Available.new("/now", from: 6, to: 18) do
  "Day Time!"
end

get "/now" do
  "Night Time!"
end

            
class SimpleWeb
  class Request
    attr_accessor :env

    def initialize env
      self.env = env
    end

    def route
      SimpleWeb::Configuration.first(:get) do |n|
        name, path = n.name, env["PATH_INFO"]
        name.respond_to?(:match) ? name.match(path) : name == path
      end
    end

    def body
      instance_eval(&route.block)
    end

    def call
      [200, {'Content-Type' => 'text'}, [body]]
    end
  end
end

run proc { |env| SimpleWeb::Request.new(env).call }

            

更幕后英雄
Qor Open Development Platform


  • 强健代码,解决不兼容,升级难问题
  • 简化开发过程常见问题
  • 开放 & 开源

如何使用

  1. 起步中,未完成。。。 -> F*ck
  2. Coming soon! Hopefully!

Thanks!


Jinzhu @ ThePlant
[email protected]