1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
|
module Jsonify
class Builder < BlankSlate
class << self
# Compiles the given block into a JSON string without having to instantiate a Builder.
#
# @option options [boolean] :verify Builder will verify that the compiled JSON string is parseable; this option does incur a performance penalty and generally should only be used in development
# @option options [symbol] :format Format for the resultant JSON string;
# `:pretty`, the JSON string will be output in a prettier format with new lines and indentation; this option does incur a performance penalty and generally should only be used in development
# `:plain`, no formatting (compact one-line JSON -- best for production)
#
def compile( options={} )
builder = self.new options
yield builder
builder.compile!
end
# Compiles the given block into a pretty JSON string without having to instantiate a Builder.
def pretty(&block)
compile( :format => :pretty, &block )
end
# Compiles the given block into a plain (e.g. no newlines and whitespace) JSON string without having to instantiate a Builder.
def plain(&block)
compile( :format => :plain, &block )
end
end
# Initializes a new builder. The Jsonify::Builder works by keeping a stack of +JsonValue+s.
#
# @param [Hash] options the options to create with
# @option options [boolean] :verify Builder will verify that the compiled JSON string is parseable; this option does incur a performance penalty and generally should only be used in development
# @option options [symbol] :format Format for the resultant JSON string;
# `:pretty`, the JSON string will be output in a prettier format with new lines and indentation; this option does incur a performance penalty and generally should only be used in development
# `:plain`, no formatting (compact one-line JSON -- best for production)
def initialize(options={})
@verify = options[:verify].nil? ? false : options[:verify]
@pretty = options[:format].to_s == 'pretty' ? true : false
reset!
end
# Clears the builder data
def reset!
@level = 0
@stack = []
end
# Adds a new JsonPair to the builder. Use this method if the pair "key" has spaces or other characters that prohibit creation via method_missing.
#
# @param sym [String] the key for the pair
# @param *args [arguments] If a block is passed, the first argument will be iterated over and the subsequent result will be added to a JSON array; otherwise, the arguments set value for the `JsonPair`
# @param &block a code block the result of which will be used to populate the value for the JSON pair
def tag!(sym, args=nil, &block)
method_missing(sym, *args, &block)
end
# Adds a new JsonPair for each attribute in attrs by taking attr as key and value of that attribute in object.
#
# @param object [Object] Object to take values from
# @param *attrs [Array<Symbol>] Array of attributes for JsonPair keys
def attributes!(object, *attrs)
attrs.each do |attr|
method_missing attr, object.send(attr)
end
end
# Compiles the JSON objects into a string representation.
# If initialized with +:verify => true+, the compiled result will be verified by attempting to re-parse it using +MultiJson.load+.
# If initialized with +:format => :pretty+, the compiled result will be parsed and encoded via +MultiJson.dump(<json>, :pretty => true)+
# This method can be called without any side effects. You can call +compile!+ at any time, and multiple times if desired.
#
# @raise [TypeError] only if +:verify+ is set to true
# @raise [JSON::ParseError] only if +:verify+ is set to true
def compile!
result = (@stack[0] || {}).encode_as_json
MultiJson.load(result) if @verify
result = MultiJson.dump(MultiJson.load(result), :pretty => true) if @pretty
result
end
# Stores the key and value into a JSON object
# @param key the key for the pair
# @param value the value for the pair
# @return self to allow for chaining
def store!(key, value=nil)
(@stack[@level] ||= JsonObject.new).add(key,value)
self
end
alias_method :[]=, :store!
# Append -- pushes the given object on the end of a JsonArray.
def <<(val)
__array
@stack[@level].add val
self
end
# Append -- pushes the given variable list objects on to the end of the JsonArray
def append!(*args)
__array
args.each do |arg|
@stack[@level].add arg
end
self
end
# Creates array of json objects in current element from array passed to this method.
# Accepts block which yields each array element.
#
# @example Create array in root JSON element
# json.array!(@links) do |link|
# json.rel link.first
# json.href link.last
# end
#
# @example compiles to something like ...
# [
# {
# "rel": "self",
# "href": "http://example.com/people/123"
# },
# {
# "rel": "school",
# "href": "http://gatech.edu"
# }
# ]
#
def array!(args)
__array
args.each do |arg|
@level += 1
yield arg
@level -= 1
value = @stack.pop
# If the object created was an array with a single value
# assume that just the value should be added
if (JsonArray === value && value.values.length <= 1)
value = value.values.first
end
@stack[@level].add value
end
end
# Adds a new JsonPair to the builder where the key of the pair is set to the method name
# (`sym`).
# When passed a block, the value of the pair is set to the result of that
# block; otherwise, the value is set to the argument(s) (`args`).
#
# @example Create an object literal
# json.person do
# json.first_name @person.given_name
# json.last_name @person.surname
# end
#
# @example compiles to something like ...
# "person": {
# "first_name": "George",
# "last_name": "Burdell"
# }
#
# If a block is given and an argument is passed, the argument it is assumed to be an
# Array (more specifically, an object that responds to `each`).
# The argument is iterated over and each item is yielded to the block.
# The result of the block becomes an array item of the JsonArray.
#
# @example Map an of array of links to an array of JSON objects
# json.links(@links) do |link|
# json.rel link.first
# json.href link.last
# end
#
# @example compiles to something like ...
# "links": [
# {
# "rel": "self",
# "href": "http://example.com/people/123"
# },
# {
# "rel": "school",
# "href": "http://gatech.edu"
# }
# ]
#
# @param *args [Array] iterates over the given array yielding each array item to the block; the result of which is added to a JsonArray
def method_missing(sym, args=nil, &block)
# When no block given, simply add the symbol and arg as key - value for a JsonPair to current
return __store( sym, args ) unless block
# In a block; create a JSON pair (with no value) and add it to the current object
pair = Generate.pair_value(sym)
__store pair
# Now process the block
@level += 1
if args.nil?
block.call
else
array!(args, &block)
end
# Set the value on the pair to the object at the top of the stack
pair.value = @stack[@level]
# Pop current off the top of the stack; we are done with it at this point
@stack.pop
@level -= 1
end
# Ingest a full JSON representation (either an oject or array)
# into the builder. The value is parsed, objectified, and added to the
# current value at the top of the stack.
#
# @param [String] json_string a full JSON string (e.g. from a rendered partial)
def ingest!(json_string)
return if json_string.empty?
res = Jsonify::Generate.value(MultiJson.load(json_string))
current = @stack[@level]
if current.nil?
@stack[@level] = res
elsif JsonObject === current
if JsonObject === res
@stack[@level].merge res
else
raise ArgumentError.new("Cannot add JSON array to JSON Object")
end
else # current is JsonArray
@stack[@level].add res
end
end
private
# BlankSlate requires the __<method> names
def __store(key,value=nil)
pair = (JsonPair === key ? key : JsonPair.new(key, value))
(@stack[@level] ||= JsonObject.new).add(pair)
end
def __array
@stack[@level] ||= JsonArray.new
end
end
end
|