示例来自官方文档

# 最简单的写法
json.content format_content(@message.content)

# call写法
json.(@message, :created_at, :updated_at)

# block写法1
json.author do
  json.name @message.creator.name.familiar
  json.email_address @message.creator.email_address_with_name
  json.url url_for(@message.creator, format: :json)
end

if current_user.admin?
  json.visitors calculate_visitors(@message)
end

# 集合处理1
json.comments @message.comments, :content, :created_at

# 集合处理2(block写法2)
json.attachments @message.attachments do |attachment|
  json.filename attachment.filename
  json.url url_for(attachment)
end


最简单的写法,也可用以下语句来直接渲染

[2] pry(main)> Jbuilder.new{ |json| json.name 'John Stobs' }.target!
=> "{\"name\":\"John Stobs\"}"


首先,JBuilder是一个ActiveSupport::ProxyObject(继承BasicObject)

Jbuilder = Class.new(begin
  require 'active_support/proxy_object'
  ActiveSupport::ProxyObject
rescue LoadError
  require 'active_support/basic_object'
  ActiveSupport::BasicObject
end)


它new的同时,会将自身传给后带的block。block里面对其任何的方法调用都会由method_missing委托到set!上

class Jbuilder
  @@key_formatter = nil
  @@ignore_nil    = false

  def initialize(options = {})
    #...
    yield self if ::Kernel.block_given?
  end

  def set!(key, value = BLANK, *args)
    result = if ::Kernel.block_given?
      if !_blank?(value)
        # json.comments @post.comments { |comment| ... }
        # { "comments": [ { ... }, { ... } ] }
        _scope{ array! value, &::Proc.new }
      else
        # json.comments { ... }
        # { "comments": ... }
        _merge_block(key){ yield self }
      end
    elsif args.empty?
      if ::Jbuilder === value
        # json.age 32
        # json.person another_jbuilder
        # { "age": 32, "person": { ...  }
        value.attributes!
      else
        # json.age 32
        # { "age": 32 }
        value
      end
    elsif _is_collection?(value)
      # json.comments @post.comments, :content, :created_at
      # { "comments": [ { "content": "hello", "created_at": "..." }, { "content": "world", "created_at": "..." } ] }
      _scope{ array! value, *args }
    else
      # json.author @post.creator, :name, :email_address
      # { "author": { "name": "David", "email_address": "david@loudthinking.com" } }
      _merge_block(key){ extract! value, *args }
    end

    _set_value key, result
  end

  def method_missing(*args)
    if ::Kernel.block_given?
      set!(*args, &::Proc.new)
    else
      set!(*args)
    end
  end


其实从set!的注释,就可得知jbuilder大部分的语法规则。需要注意的是,如果值是数组型,则要带block或数组元素的属性名字,否则会跑进第二个条件分支,直接打印出该数组的字符串形式

第二种写法,其实是属于ruby本身的语法:任意对象加句号然后再加一对含有参数的括号,则是调用该对象的call方法(proc/lambda也有call方法,即是直接调函数本身)。因此JBuilder也得定义个call方法来响应,其效果就是取object(也可以是个hash)的各个attributes来塞到当前json中,源码如下

def call(object, *attributes)
  if ::Kernel.block_given?
    array! object, &::Proc.new
  else
    extract! object, *attributes
  end
end

def extract!(object, *attributes)
  if ::Hash === object
    _extract_hash_values(object, attributes)
  else
    _extract_method_values(object, attributes)
  end
end

def _extract_hash_values(object, attributes)
  attributes.each{ |key| _set_value key, object.fetch(key) }
end

def _extract_method_values(object, attributes)
  attributes.each{ |key| _set_value key, object.public_send(key) }
end


而两个block写法则是由set!的第一个条件分支来处理。注意&::Proc.new所用的block来自其caller所带的block(这是ruby语法)。

另外,当要处理集合时,一般就用集合处理1和集合处理2,因为抓取的数据要通过_set_value塞到json这个BasicObject的@attributes中,最终通过target!将@attributes转成字符串。若想自行通过map或iterate设置@attributes也可以,就是用instance_eval/exec咯,因为是BasicObject,没有instance_variable_set、eval,不过这就无聊了。当然,也可以自行__send__(:_merge_values),不过这也很无聊,完全没利用到DSL的优势

两个block和两个集合处理的源码如下

def array!(collection = [], *attributes)
  array = if collection.nil?
    []
  elsif ::Kernel.block_given?
    _map_collection(collection, &::Proc.new)
  elsif attributes.any?
    _map_collection(collection) { |element| extract! element, *attributes }
  else
    collection.to_a
  end

  merge! array
end

def _scope
  parent_attributes, parent_formatter = @attributes, @key_formatter
  @attributes = BLANK
  yield
  @attributes
ensure
  @attributes, @key_formatter = parent_attributes, parent_formatter
end

def _merge_block(key)
  current_value = _blank? ? BLANK : @attributes.fetch(_key(key), BLANK)
  raise NullError.build(key) if current_value.nil?
  new_value = _scope{ yield self }
  _merge_values(current_value, new_value)
end

def _merge_values(current_value, updates)
  if _blank?(updates)
    current_value
  elsif _blank?(current_value) || updates.nil? || current_value.empty? && ::Array === updates
    updates
  elsif ::Array === current_value && ::Array === updates
    current_value + updates
  elsif ::Hash === current_value && ::Hash === updates
    current_value.merge(updates)
  else
    raise MergeError.build(current_value, updates)
  end
end