一个 jbuilder 文件,实际对应于一个特殊的哈希对象——Jbuilder 对象,在哈希对象上调用 to_json,便会生成 json 字符串。
哈希生成原理:利用 method_missing, 转发不认识的实例方法,方法名作为键,第一参数作为值,支持块处理。
布局原理:利用源码替换技术替换掉布局文件的yield_content
子视图原理:利用语言自带的 read_file 函数,编译期读取文件
简单介绍下 Crystal 的编译期元编程, {{ }} 和 {% %}是编译期宏的语法格式,长得很像 erb 模板。{{ }}里的东西在编译期会被输出,{% %}里的是编译期控制流,用来控制代码编译的。read_file、run、id 是编译期特有的方法,用于代码生成的。
require "jbuilder"
plain = Jbuilder.new do |json|
json.null nil
json.code 200
json.msg "ok"
json.merge!({"code" => 201})
json.array! "array1", [1, 1.0, "1"]
json.array!("array2", [1, 2, 3, 4]) do |json, item|
json.code item
end
json.data do |json|
json.code 400
json.array! "array3", [1, 1.0, "1"]
end
json.set!("custom_field", %w[1 2])
end.to_json
puts plain
{
"null":null,
"code":201,
"msg":"ok",
"array1":[
1,
1.0,
"1"
],
"array2":[
{
"code":1
},
{
"code":2
},
{
"code":3
},
{
"code":4
}
],
"data":{
"code":400,
"array3":[
1,
1.0,
"1"
]
},
"custom_field":[
"1",
"2"
]
}
require "json"
require "./jbuilder/version"
require "./jbuilder/embed"
class Jbuilder
alias Integer = Int8 | Int16 | Int32 | Int64 | Int128 | UInt8 | UInt16 | UInt32 | UInt64 | UInt128
alias BasicType = Bool | Float32 | Float64 | Integer | Nil | String | Time
alias BasicObject = Array(BasicObject) | BasicType | Hash(String, BasicObject) # 定义类型
def initialize(@result = Hash(String, BasicObject).new)
end
# 实际初始化方法, 返回一个哈希
def self.new : Hash
new.tap do |json|
yield json
end.to_h
end
# 转发未定义的实例方法
macro method_missing(call)
# 如果调用非自带的感叹号方法,如merge!, set!, 则抛出编译期异常
{% if call.name.stringify.ends_with?("!") %}
{% raise "Values only allowed Array | Tuple | Hash | NamedTuple | #{BasicType}" %}
{% end %}
{% if call.block %} # 如果块存在,则新建一个jbuilder实例赋值给哈希键
set!({{call.name.stringify}}, Jbuilder.new {{call.block}})
{% else %} # 如果块不存在,直接赋值给哈希键
set!({{call.name.stringify}}, {{call.args.first}})
{% end %}
end
def array!(key : String, array) # 简单处理数组,第一参数作为键,第二参数才是值
value = [] of BasicObject
array.each do |item|
value << item
end
@result[key] = value
end
def array!(key : String, array) # 使用块处理对象数组,第一参数作为键,第二参数才是值
value = [] of BasicObject
array.each do |item|
value << Jbuilder.new do |json|
yield json, item
end
end
@result[key] = value
end
def hash!(key : String, hash)
value = {} of String => BasicObject
hash.each do |k, v|
value[k.to_s] = v
end
@result[key] = value
end
def merge!(hash : Hash)
@result.merge!(hash)
end
def set!(key : String, value : Array)
array!(key, value)
end
def set!(key : String, value : Tuple)
array!(key, value.to_a)
end
def set!(key : String, value : Hash)
hash!(key, value)
end
def set!(key : String, value : NamedTuple)
hash!(key, value.to_h)
end
def set!(key : String, value : BasicType)
@result[key] = value
end
def to_h : Hash
@result
end
end
class Jbuilder
# 支持布局
macro embed(filename, io_name, layout_file = nil)
{% if layout_file %}
# 布局文件的yield_content在编译时会被子文件的源代码覆盖
{{ io_name.id }} << {{ read_file(layout_file).gsub(/yield_content/, read_file(filename)).id }}.to_json
{{ io_name.id }}
{% else %}
# 不使用布局文件,嵌套一层Jbuilder.new
{{ io_name.id }} << {{ run("./embedder.cr", filename) }}
{{ io_name.id }}
{% end %}
end
end
puts <<-Crystal
# 读取文件
Jbuilder.new do |json|
#{File.read(ARGV[0])}
end.to_json
Crystal
Jbuilder.new do |json|
json.code "200"
json.msg "ok"
json.data do |json|
yield_content
end
end
json.array! "applies", applies do |json, apply|
{{ read_file("src/views/applies/_base_apply.jbuilder").id }}
end
json.local_updated_at local_updated_at.to_i
json.apply_id apply.apply_id.to_s
json.code apply.code.to_s
json.name apply.name.to_s
json.subtitle apply.subtitle.to_s
json.rand_number rand(100)
json.uri apply.uri
json.common_flag apply.common_flag