Crystal 100 行 Crystal 代码实现 Jbuilder

ThxFly · 2020年07月05日 · 2360 次阅读

一、原理

一个 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"
  ]
}

四、源代码

src/jbuilder.cr

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

src/jbuilder/embed.cr

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

src/jbuilder/embedder.cr

puts <<-Crystal
  # 读取文件
  Jbuilder.new do |json|
    #{File.read(ARGV[0])}
  end.to_json
Crystal

五、实际生产书写的代码如下

布局文件 src/views/layouts/application.jbuilder

Jbuilder.new do |json|
  json.code "200"
  json.msg  "ok"
  json.data do |json|
    yield_content
  end
end

主视图 src/views/applies/my_apply.jbuilder

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

局部视图 src/views/applies/_base_apply.jbuilder

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

项目地址: https://github.com/shootingfly/jbuilder

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请 注册新账号