Ticket #9677: add_includes_to_serializer.diff
| File add_includes_to_serializer.diff, 13.4 kB (added by chuyeow, 9 months ago) |
|---|
-
activerecord/test/json_serialization_test.rb
old new 1 require 'abstract_unit' 2 require 'fixtures/contact' 3 require 'fixtures/post' 4 require 'fixtures/author' 5 require 'fixtures/tagging' 6 require 'fixtures/tag' 7 require 'fixtures/comment' 8 9 class JsonSerializationTest < Test::Unit::TestCase 10 def setup 11 # Quote all keys (so that we can test against strictly valid JSON). 12 ActiveSupport::JSON.unquote_hash_key_identifiers = false 13 14 @contact = Contact.new( 15 :name => 'Konata Izumi', 16 :age => 16, 17 :avatar => 'binarydata', 18 :created_at => Time.utc(2006, 8, 1), 19 :awesome => true, 20 :preferences => { :shows => 'anime' } 21 ) 22 end 23 24 def test_should_encode_all_encodable_attributes 25 json = @contact.to_json 26 27 assert_match %r{"name": "Konata Izumi"}, json 28 assert_match %r{"age": 16}, json 29 assert_match %r{"created_at": #{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}}, json 30 assert_match %r{"awesome": true}, json 31 assert_match %r{"preferences": \{"shows": "anime"\}}, json 32 end 33 34 def test_should_allow_attribute_filtering_with_only 35 json = @contact.to_json(:only => [:name, :age]) 36 37 assert_match %r{"name": "Konata Izumi"}, json 38 assert_match %r{"age": 16}, json 39 assert_no_match %r{"awesome": true}, json 40 assert_no_match %r{"created_at": #{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}}, json 41 assert_no_match %r{"preferences": \{"shows": "anime"\}}, json 42 end 43 44 def test_should_allow_attribute_filtering_with_except 45 json = @contact.to_json(:except => [:name, :age]) 46 47 assert_no_match %r{"name": "Konata Izumi"}, json 48 assert_no_match %r{"age": 16}, json 49 assert_match %r{"awesome": true}, json 50 assert_match %r{"created_at": #{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}}, json 51 assert_match %r{"preferences": \{"shows": "anime"\}}, json 52 end 53 54 def test_methods_are_called_on_object 55 # Define methods on fixture. 56 def @contact.label; "Has cheezburger"; end 57 def @contact.favorite_quote; "Constraints are liberating"; end 58 59 # Single method. 60 assert_match %r{"label": "Has cheezburger"}, @contact.to_json(:only => :name, :methods => :label) 61 62 # Both methods. 63 methods_json = @contact.to_json(:only => :name, :methods => [:label, :favorite_quote]) 64 assert_match %r{"label": "Has cheezburger"}, methods_json 65 assert_match %r{"favorite_quote": "Constraints are liberating"}, methods_json 66 end 67 end 68 69 class DatabaseConnectedJsonEncodingTest < Test::Unit::TestCase 70 fixtures :authors, :posts, :comments, :tags, :taggings 71 72 def setup 73 ActiveSupport::JSON.unquote_hash_key_identifiers = false 74 75 @david = authors(:david) 76 end 77 78 def test_includes_uses_association_name 79 json = @david.to_json(:include => :posts) 80 81 assert_match %r{"posts": \[}, json 82 83 assert_match %r{"id": 1}, json 84 assert_match %r{"name": "David"}, json 85 86 assert_match %r{"author_id": 1}, json 87 assert_match %r{"title": "Welcome to the weblog"}, json 88 assert_match %r{"body": "Such a lovely day"}, json 89 90 assert_match %r{"title": "So I was thinking"}, json 91 assert_match %r{"body": "Like I hopefully always am"}, json 92 end 93 94 def test_includes_uses_association_name_and_applies_attribute_filters 95 json = @david.to_json(:include => { :posts => { :only => :title } }) 96 97 assert_match %r{"name": "David"}, json 98 assert_match %r{"posts": \[}, json 99 100 assert_match %r{"title": "Welcome to the weblog"}, json 101 assert_no_match %r{"body": "Such a lovely day"}, json 102 103 assert_match %r{"title": "So I was thinking"}, json 104 assert_no_match %r{"body": "Like I hopefully always am"}, json 105 end 106 107 def test_includes_fetches_second_level_associations 108 json = @david.to_json(:include => { :posts => { :include => { :comments => { :only => :body } } } }) 109 110 assert_match %r{"name": "David"}, json 111 assert_match %r{"posts": \[}, json 112 113 assert_match %r{"comments": \[}, json 114 assert_match %r{\{"body": "Thank you again for the welcome"\}}, json 115 assert_match %r{\{"body": "Don't think too hard"\}}, json 116 assert_no_match %r{"post_id": }, json 117 end 118 119 def test_includes_fetches_nth_level_associations 120 json = @david.to_json( 121 :include => { 122 :posts => { 123 :include => { 124 :taggings => { 125 :include => { 126 :tag => { :only => :name } 127 } 128 } 129 } 130 } 131 }) 132 133 assert_match %r{"name": "David"}, json 134 assert_match %r{"posts": \[}, json 135 136 assert_match %r{"taggings": \[}, json 137 assert_match %r{"tag": \{"name": "General"\}}, json 138 end 139 140 def test_should_not_call_methods_on_associations_that_dont_respond 141 def @david.favorite_quote; "Constraints are liberating"; end 142 json = @david.to_json(:include => :posts, :methods => :favorite_quote) 143 144 assert !@david.posts.first.respond_to?(:favorite_quote) 145 assert_match %r{"favorite_quote": "Constraints are liberating"}, json 146 assert_equal %r{"favorite_quote": }.match(json).size, 1 147 end 148 end -
activerecord/lib/active_record/serialization.rb
old new 2 2 module Serialization 3 3 class Serializer #:nodoc: 4 4 attr_reader :options 5 5 6 6 def initialize(record, options = {}) 7 7 @record, @options = record, options.dup 8 8 end … … 23 23 options[:except] = Array(options[:except]) | Array(@record.class.inheritance_column) 24 24 attribute_names = attribute_names - options[:except].collect { |n| n.to_s } 25 25 end 26 26 27 27 attribute_names 28 28 end 29 29 30 30 def serializable_method_names 31 31 Array(options[:methods]).inject([]) do |method_attributes, name| 32 method_attributes << :name if @record.respond_to?(name.to_s)32 method_attributes << name if @record.respond_to?(name.to_s) 33 33 method_attributes 34 34 end 35 35 end 36 36 37 37 def serializable_names 38 38 serializable_attribute_names + serializable_method_names 39 39 end 40 40 41 # Add associations specified via the :includes option. 42 # Expects a block that takes as arguments: 43 # +association+ - name of the association 44 # +records+ - the association record(s) to be serialized 45 # +opts+ - options for the association records 46 def add_includes(&block) 47 if include_associations = options.delete(:include) 48 base_only_or_except = { :except => options[:except], 49 :only => options[:only] } 50 51 include_has_options = include_associations.is_a?(Hash) 52 associations = include_has_options ? include_associations.keys : Array(include_associations) 53 54 for association in associations 55 records = case @record.class.reflect_on_association(association).macro 56 when :has_many, :has_and_belongs_to_many 57 @record.send(association).to_a 58 when :has_one, :belongs_to 59 @record.send(association) 60 end 61 62 unless records.nil? 63 association_options = include_has_options ? include_associations[association] : base_only_or_except 64 opts = options.merge(association_options) 65 yield(association, records, opts) 66 end 67 end 68 69 options[:include] = include_associations 70 end 71 end 72 41 73 def serializable_record 42 74 returning(serializable_record = {}) do 43 75 serializable_names.each { |name| serializable_record[name] = @record.send(name) } 76 add_includes do |association, records, opts| 77 if records.is_a?(Enumerable) 78 serializable_record[association] = records.collect { |r| self.class.new(r, opts).serializable_record } 79 else 80 serializable_record[association] = self.class.new(records, opts).serializable_record 81 end 82 end 44 83 end 45 84 end 46 85 47 86 def serialize 48 87 # overwrite to implement 49 end 50 88 end 89 51 90 def to_s(&block) 52 91 serialize(&block) 53 92 end -
activerecord/lib/active_record/serializers/xml_serializer.rb
old new 141 141 builder.instruct! 142 142 options[:skip_instruct] = true 143 143 end 144 144 145 145 builder 146 146 end 147 147 end … … 150 150 root = (options[:root] || @record.class.to_s.underscore).to_s 151 151 dasherize? ? root.dasherize : root 152 152 end 153 153 154 154 def dasherize? 155 155 !options.has_key?(:dasherize) || options[:dasherize] 156 156 end … … 179 179 end 180 180 end 181 181 182 def add_includes183 if include_associations = options.delete(:include)184 root_only_or_except = { :except => options[:except],185 :only => options[:only] }186 187 include_has_options = include_associations.is_a?(Hash)188 189 for association in include_has_options ? include_associations.keys : Array(include_associations)190 association_options = include_has_options ? include_associations[association] : root_only_or_except191 192 opts = options.merge(association_options)193 194 case @record.class.reflect_on_association(association).macro195 when :has_many, :has_and_belongs_to_many196 records = @record.send(association).to_a197 tag = association.to_s198 tag = tag.dasherize if dasherize?199 if records.empty?200 builder.tag!(tag, :type => :array)201 else202 builder.tag!(tag, :type => :array) do203 association_name = association.to_s.singularize204 records.each do |record|205 record.to_xml opts.merge(206 :root => association_name,207 :type => (record.class.to_s.underscore == association_name ? nil : record.class.name)208 )209 end210 end211 end212 when :has_one, :belongs_to213 if record = @record.send(association)214 record.to_xml(opts.merge(:root => association))215 end216 end217 end218 219 options[:include] = include_associations220 end221 end222 223 182 def add_procs 224 183 if procs = options.delete(:procs) 225 184 [ *procs ].each do |proc| … … 228 187 end 229 188 end 230 189 231 232 190 def add_tag(attribute) 233 191 builder.tag!( 234 192 dasherize? ? attribute.name.dasherize : attribute.name, … … 237 195 ) 238 196 end 239 197 198 def add_associations(association, records, opts) 199 if records.is_a?(Enumerable) 200 tag = association.to_s 201 tag = tag.dasherize if dasherize? 202 if records.empty? 203 builder.tag!(tag, :type => :array) 204 else 205 builder.tag!(tag, :type => :array) do 206 association_name = association.to_s.singularize 207 records.each do |record| 208 record.to_xml opts.merge( 209 :root => association_name, 210 :type => (record.class.to_s.underscore == association_name ? nil : record.class.name) 211 ) 212 end 213 end 214 end 215 else 216 if record = @record.send(association) 217 record.to_xml(opts.merge(:root => association)) 218 end 219 end 220 end 221 240 222 def serialize 241 223 args = [root] 242 224 if options[:namespace] 243 225 args << {:xmlns=>options[:namespace]} 244 226 end 245 227 246 228 if options[:type] 247 229 args << {:type=>options[:type]} 248 230 end 249 231 250 232 builder.tag!(*args) do 251 233 add_attributes 252 add_includes 234 add_includes { |association, records, opts| add_associations(association, records, opts) } 253 235 add_procs 254 236 yield builder if block_given? 255 237 end 256 end 238 end 257 239 258 240 class Attribute #:nodoc: 259 241 attr_reader :name, :value, :type 260 242 261 243 def initialize(name, record) 262 244 @name, @record = name, record 263 245 264 246 @type = compute_type 265 247 @value = compute_value 266 248 end … … 277 259 def needs_encoding? 278 260 ![ :binary, :date, :datetime, :boolean, :float, :integer ].include?(type) 279 261 end 280 262 281 263 def decorations(include_types = true) 282 264 decorations = {} 283 265 284 266 if type == :binary 285 267 decorations[:encoding] = 'base64' 286 268 end 287 269 288 270 if include_types && type != :string 289 271 decorations[:type] = type 290 272 end 291 273 292 274 decorations 293 275 end 294 276 295 277 protected 296 278 def compute_type 297 279 type = @record.class.serialized_attributes.has_key?(name) ? :yaml : @record.class.columns_hash[name].type … … 305 287 type 306 288 end 307 289 end 308 290 309 291 def compute_value 310 292 value = @record.send(name) 311 293 312 294 if formatter = Hash::XML_FORMATTING[type.to_s] 313 295 value ? formatter.call(value) : nil 314 296 else