File: builder.rb

package info (click to toggle)
ruby-jsonify 0.4.1-4
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 240 kB
  • sloc: ruby: 748; makefile: 2
file content (255 lines) | stat: -rw-r--r-- 9,233 bytes parent folder | download | duplicates (4)
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