62
48
# Album.to_json(:array=>[Album[1], Album[2]])
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
54
# json = album.to_json
55
# album = Album.from_json(json)
57
# The array_from_json class method exists to parse arrays of model instances
60
# json = Album.filter(:artist_id=>1).to_json
61
# albums = Album.array_from_json(json)
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
69
# Album.from_json(album.to_json, :fields=>%w'id name')
71
# If you want to update an existing instance, you can use the from_json
74
# album.from_json(json)
76
# Both of these allow creation of cached associated objects, if you provide
77
# the :associations option:
79
# album.from_json(json, :associations=>:artist)
81
# You can even provide options when setting up the associated objects:
83
# album.from_json(json, :associations=>{:artist=>{:fields=>%w'id name', :associations=>:tags}})
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:
90
# def to_json(options = {})
96
# def to_json(options = {})
101
# Note that this will probably cause active_support/json to no longer work
102
# correctly in some cases.
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
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)
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)
112
if assocs.include?(k)
113
obj.associations[k.to_sym] = v
114
elsif meths.include?("#{k}=")
116
elsif cols.include?(k)
117
obj.values[k.to_sym] = v
119
raise Error, "Entry in JSON hash not an association or column and no setter method exists: #{k}"
125
# Call the dataset +to_json+ method.
130
# Copy the current model object's default json options into the subclass.
131
def inherited(subclass)
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)
148
new.from_json_node(v, opts)
150
raise Error, "parsed json doesn't return a hash or instance of #{self}"
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)
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)}
162
raise(Error, 'parsed json did not return an array')
166
Plugins.inherited_instance_variables(self, :@json_serializer_opts=>lambda do |json_serializer_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)
172
Plugins.def_dataset_methods(self, :to_json)
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={})
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)
182
# Using the provided hash, update the instance with data contained in the hash. By default, just
183
# calls set with the hash values.
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"
196
populate_associations = {}
198
if assocs = opts[:associations]
204
assocs.each{|v| assocs_tmp[v] = {}}
209
raise Error, ":associations should be Symbol, Array, or Hash if present"
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}"
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)}
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)
144
229
if fields = opts[:fields]
145
set_fields(h, fields, opts)
230
set_fields(hash, fields, opts)
235
populate_associations.each do |assoc, values|
236
associations[assoc] = values
151
242
# Return a string in JSON format. Accepts the following