~ubuntu-branches/ubuntu/vivid/ruby-sequel/vivid

« back to all changes in this revision

Viewing changes to lib/sequel/plugins/json_serializer.rb

  • Committer: Package Import Robot
  • Author(s): Dmitry Borodaenko, Dmitry Borodaenko, Cédric Boutillier
  • Date: 2013-08-10 18:38:17 UTC
  • mfrom: (1.1.8)
  • Revision ID: package-import@ubuntu.com-20130810183817-iqanz804j32i5myi
Tags: 4.1.1-1
[ Dmitry Borodaenko ]
* New upstream release.
* Standards-Version upgraded to 3.9.4 (no changes).
* Added Build-Depend on ruby-sqlite3.

[ Cédric Boutillier ]
* debian/control: remove obsolete DM-Upload-Allowed flag.
* use canonical URI in Vcs-* fields.
* debian/copyright: use DEP5 copyright-format/1.0 official URL for Format
  field.
* Update debian/watch. Thanks Bart Martens.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
require 'json'
 
2
 
1
3
module Sequel
2
 
  tsk_require 'json'
3
 
 
4
4
  module Plugins
5
5
    # The json_serializer plugin handles serializing entire Sequel::Model
6
6
    # objects to JSON, as well as support for deserializing JSON directly
36
36
    #   album.to_json(:root => true)
37
37
    #   # => '{"album":{"id":1,"name":"RF","artist_id":2}}'
38
38
    #
39
 
    # In addition to creating JSON, this plugin also enables Sequel::Model
40
 
    # objects to be automatically created when JSON is parsed:
41
 
    #
42
 
    #   json = album.to_json
43
 
    #   album = JSON.parse(json)
44
 
    #
45
 
    # In addition, you can update existing model objects directly from JSON
46
 
    # using +from_json+:
47
 
    #
48
 
    #   album.from_json(json)
49
 
    #
50
 
    # This works by parsing the JSON (which should return a hash), and then
51
 
    # calling +set+ with the returned hash.
52
 
    #
53
39
    # Additionally, +to_json+ also exists as a class and dataset method, both
54
40
    # of which return all objects in the dataset:
55
41
    #
61
47
    #
62
48
    #   Album.to_json(:array=>[Album[1], Album[2]])
63
49
    #
 
50
    # In addition to creating JSON, this plugin also enables Sequel::Model
 
51
    # classes to create instances directly from JSON using the from_json class
 
52
    # method:
 
53
    #
 
54
    #   json = album.to_json
 
55
    #   album = Album.from_json(json)
 
56
    #
 
57
    # The array_from_json class method exists to parse arrays of model instances
 
58
    # from json:
 
59
    #
 
60
    #   json = Album.filter(:artist_id=>1).to_json
 
61
    #   albums = Album.array_from_json(json)
 
62
    #
 
63
    # These does not necessarily round trip, since doing so would let users
 
64
    # create model objects with arbitrary values.  By default, from_json will
 
65
    # call set with the values in the hash.  If you want to specify the allowed
 
66
    # fields, you can use the :fields option, which will call set_fields with
 
67
    # the given fields:
 
68
    #
 
69
    #   Album.from_json(album.to_json, :fields=>%w'id name')
 
70
    #
 
71
    # If you want to update an existing instance, you can use the from_json
 
72
    # instance method:
 
73
    #
 
74
    #   album.from_json(json)
 
75
    #
 
76
    # Both of these allow creation of cached associated objects, if you provide
 
77
    # the :associations option:
 
78
    #
 
79
    #   album.from_json(json, :associations=>:artist)
 
80
    #
 
81
    # You can even provide options when setting up the associated objects:
 
82
    #
 
83
    #   album.from_json(json, :associations=>{:artist=>{:fields=>%w'id name', :associations=>:tags}})
 
84
    #
 
85
    # Note that active_support/json makes incompatible changes to the to_json API,
 
86
    # and breaks some aspects of the json_serializer plugin.  You can undo the damage
 
87
    # done by active_support/json by doing:
 
88
    #
 
89
    #   class Array
 
90
    #     def to_json(options = {})
 
91
    #       JSON.generate(self)
 
92
    #     end
 
93
    #   end
 
94
    #
 
95
    #   class Hash
 
96
    #     def to_json(options = {})
 
97
    #       JSON.generate(self)
 
98
    #     end
 
99
    #   end
 
100
    #
 
101
    # Note that this will probably cause active_support/json to no longer work
 
102
    # correctly in some cases.
 
103
    #
64
104
    # Usage:
65
105
    #
66
106
    #   # Add JSON output capability to all model subclass instances (called before loading subclasses)
97
137
        # The default opts to use when serializing model objects to JSON.
98
138
        attr_reader :json_serializer_opts
99
139
 
100
 
        # Create a new model object from the hash provided by parsing
101
 
        # JSON.  Handles column values (stored in +values+), associations
102
 
        # (stored in +associations+), and other values (by calling a
103
 
        # setter method).  If an entry in the hash is not a column or
104
 
        # an association, and no setter method exists, raises an Error.
105
 
        def json_create(hash)
106
 
          obj = new
107
 
          cols = columns.map{|x| x.to_s}
108
 
          assocs = associations.map{|x| x.to_s}
109
 
          meths = obj.send(:setter_methods, nil, nil)
110
 
          hash.delete(JSON.create_id)
111
 
          hash.each do |k, v|
112
 
            if assocs.include?(k)
113
 
              obj.associations[k.to_sym] = v
114
 
            elsif meths.include?("#{k}=")
115
 
              obj.send("#{k}=", v)
116
 
            elsif cols.include?(k)
117
 
              obj.values[k.to_sym] = v
118
 
            else
119
 
              raise Error, "Entry in JSON hash not an association or column and no setter method exists: #{k}"
120
 
            end
121
 
          end
122
 
          obj
123
 
        end
124
 
 
125
 
        # Call the dataset +to_json+ method.
126
 
        def to_json(*a)
127
 
          dataset.to_json(*a)
128
 
        end
129
 
        
130
 
        # Copy the current model object's default json options into the subclass.
131
 
        def inherited(subclass)
132
 
          super
 
140
        # Attempt to parse a single instance from the given JSON string,
 
141
        # with options passed to InstanceMethods#from_json_node.
 
142
        def from_json(json, opts=OPTS)
 
143
          v = Sequel.parse_json(json)
 
144
          case v
 
145
          when self
 
146
            v
 
147
          when Hash
 
148
            new.from_json_node(v, opts)
 
149
          else
 
150
            raise Error, "parsed json doesn't return a hash or instance of #{self}"
 
151
          end
 
152
        end
 
153
 
 
154
        # Attempt to parse an array of instances from the given JSON string,
 
155
        # with options passed to InstanceMethods#from_json_node.
 
156
        def array_from_json(json, opts=OPTS)
 
157
          v = Sequel.parse_json(json)
 
158
          if v.is_a?(Array)
 
159
            raise(Error, 'parsed json returned an array containing non-hashes') unless v.all?{|ve| ve.is_a?(Hash) || ve.is_a?(self)}
 
160
            v.map{|ve| ve.is_a?(self) ? ve : new.from_json_node(ve, opts)}
 
161
          else
 
162
            raise(Error, 'parsed json did not return an array')
 
163
          end
 
164
        end
 
165
 
 
166
        Plugins.inherited_instance_variables(self, :@json_serializer_opts=>lambda do |json_serializer_opts|
133
167
          opts = {}
134
168
          json_serializer_opts.each{|k, v| opts[k] = (v.is_a?(Array) || v.is_a?(Hash)) ? v.dup : v}
135
 
          subclass.instance_variable_set(:@json_serializer_opts, opts)
136
 
        end
 
169
          opts
 
170
        end)
 
171
 
 
172
        Plugins.def_dataset_methods(self, :to_json)
137
173
      end
138
174
 
139
175
      module InstanceMethods
140
176
        # Parse the provided JSON, which should return a hash,
141
 
        # and call +set+ with that hash.
142
 
        def from_json(json, opts={})
143
 
          h = JSON.parse(json)
 
177
        # and process the hash with from_json_node.
 
178
        def from_json(json, opts=OPTS)
 
179
          from_json_node(Sequel.parse_json(json), opts)
 
180
        end
 
181
 
 
182
        # Using the provided hash, update the instance with data contained in the hash. By default, just
 
183
        # calls set with the hash values.
 
184
        # 
 
185
        # Options:
 
186
        # :associations :: Indicates that the associations cache should be updated by creating
 
187
        #                  a new associated object using data from the hash.  Should be a Symbol
 
188
        #                  for a single association, an array of symbols for multiple associations,
 
189
        #                  or a hash with symbol keys and dependent association option hash values.
 
190
        # :fields :: Changes the behavior to call set_fields using the provided fields, instead of calling set.
 
191
        def from_json_node(hash, opts=OPTS)
 
192
          unless hash.is_a?(Hash)
 
193
            raise Error, "parsed json doesn't return a hash"
 
194
          end
 
195
 
 
196
          populate_associations = {}
 
197
 
 
198
          if assocs = opts[:associations]
 
199
            assocs = case assocs
 
200
            when Symbol
 
201
              {assocs=>{}}
 
202
            when Array
 
203
              assocs_tmp = {}
 
204
              assocs.each{|v| assocs_tmp[v] = {}}
 
205
              assocs_tmp
 
206
            when Hash
 
207
              assocs
 
208
            else
 
209
              raise Error, ":associations should be Symbol, Array, or Hash if present"
 
210
            end
 
211
 
 
212
            assocs.each do |assoc, assoc_opts|
 
213
              if assoc_values = hash.delete(assoc.to_s)
 
214
                unless r = model.association_reflection(assoc)
 
215
                  raise Error, "Association #{assoc} is not defined for #{model}"
 
216
                end
 
217
 
 
218
                populate_associations[assoc] = if r.returns_array?
 
219
                  raise Error, "Attempt to populate array association with a non-array" unless assoc_values.is_a?(Array)
 
220
                  assoc_values.map{|v| v.is_a?(r.associated_class) ? v : r.associated_class.new.from_json_node(v, assoc_opts)}
 
221
                else
 
222
                  raise Error, "Attempt to populate non-array association with an array" if assoc_values.is_a?(Array)
 
223
                  assoc_values.is_a?(r.associated_class) ? assoc_values : r.associated_class.new.from_json_node(assoc_values, assoc_opts)
 
224
                end
 
225
              end
 
226
            end
 
227
          end
 
228
 
144
229
          if fields = opts[:fields]
145
 
            set_fields(h, fields, opts)
 
230
            set_fields(hash, fields, opts)
146
231
          else
147
 
            set(h)
148
 
          end
 
232
            set(hash)
 
233
          end
 
234
 
 
235
          populate_associations.each do |assoc, values|
 
236
            associations[assoc] = values
 
237
          end
 
238
 
 
239
          self
149
240
        end
150
241
 
151
242
        # Return a string in JSON format.  Accepts the following
159
250
        #             to include in the JSON output.  Using a nested
160
251
        #             hash, you can pass options to associations
161
252
        #             to affect the JSON used for associated objects.
162
 
        # :naked :: Not to add the JSON.create_id (json_class) key to the JSON
163
 
        #           output hash, so when the JSON is parsed, it
164
 
        #           will yield a hash instead of a model object.
165
253
        # :only :: Symbol or Array of Symbols of columns to only
166
254
        #          include in the JSON output, ignoring all other
167
255
        #          columns.
168
256
        # :root :: Qualify the JSON with the name of the object.
169
 
        #          Implies :naked since the object name is explicit.
170
257
        def to_json(*a)
171
258
          if opts = a.first.is_a?(Hash)
172
259
            opts = model.json_serializer_opts.merge(a.first)
180
267
          else
181
268
            vals.keys - Array(opts[:except])
182
269
          end
183
 
          h = (JSON.create_id && !opts[:naked] && !opts[:root]) ? {JSON.create_id=>model.name} : {}
 
270
 
 
271
          h = {}
 
272
 
184
273
          cols.each{|c| h[c.to_s] = send(c)}
185
274
          if inc = opts[:include]
186
275
            if inc.is_a?(Hash)
188
277
                v = v.empty? ? [] : [v]
189
278
                h[k.to_s] = case objs = send(k)
190
279
                when Array
191
 
                  objs.map{|obj| Literal.new(obj.to_json(*v))}
 
280
                  objs.map{|obj| Literal.new(Sequel.object_to_json(obj, *v))}
192
281
                else
193
 
                  Literal.new(objs.to_json(*v))
 
282
                  Literal.new(Sequel.object_to_json(objs, *v))
194
283
                end
195
284
              end
196
285
            else
198
287
            end
199
288
          end
200
289
          h = {model.send(:underscore, model.to_s) => h} if opts[:root]
201
 
          h.to_json(*a)
 
290
          Sequel.object_to_json(h, *a)
202
291
        end
203
292
      end
204
293
 
231
320
          collection_root = case opts[:root]
232
321
          when nil, false, :instance
233
322
            false
234
 
          when :collection
 
323
          when :both
 
324
            true
 
325
          else
235
326
            opts = opts.dup
236
327
            opts.delete(:root)
237
 
            opts[:naked] = true unless opts.has_key?(:naked)
238
 
            true
239
 
          else
240
328
            true
241
329
          end
242
330
 
247
335
            else
248
336
              all
249
337
            end
250
 
            array.map{|obj| Literal.new(obj.to_json(opts))}
 
338
            array.map{|obj| Literal.new(Sequel.object_to_json(obj, opts))}
251
339
           else
252
340
            all
253
341
          end
254
342
 
255
343
          if collection_root
256
 
            {model.send(:pluralize, model.send(:underscore, model.to_s)) => res}.to_json(*a)
 
344
            Sequel.object_to_json({model.send(:pluralize, model.send(:underscore, model.to_s)) => res}, *a)
257
345
          else
258
 
            res.to_json(*a)
 
346
            Sequel.object_to_json(res, *a)
259
347
          end
260
348
        end
261
349
      end