jbuilder是如何抓取数据的(jbuilder的语法规则)
示例来自官方文档
# 最简单的写法
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