Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

Ticket #8920: add_options_to_AR_to_json.diff

File add_options_to_AR_to_json.diff, 8.6 kB (added by chuyeow, 1 year ago)

AR#to_json with options patch (with tests)

  • test/json_encoding_test.rb

    old new  
     1require 'abstract_unit' 
     2require 'fixtures/post' 
     3require 'fixtures/author' 
     4require 'fixtures/comment' 
     5 
     6class Contact < ActiveRecord::Base 
     7  # mock out self.columns so no pesky db is needed for these tests 
     8  def self.columns() @columns ||= []; end 
     9  def self.column(name, sql_type = nil, default = nil, null = true) 
     10    columns << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type.to_s, null) 
     11  end 
     12 
     13  column :name,       :string 
     14  column :age,        :integer 
     15  column :avatar,     :binary 
     16  column :created_at, :datetime 
     17  column :awesome,    :boolean 
     18  column :gender,     :string 
     19end 
     20 
     21 
     22class JsonEncodingTest < Test::Unit::TestCase 
     23 
     24  def setup 
     25    @david = Contact.new( 
     26               :name => 'David', 
     27               :age => 27, 
     28               :awesome => true, 
     29               :gender => 'Male') 
     30  end 
     31 
     32  def test_should_encode_all_encodable_attributes 
     33    json = @david.to_json 
     34 
     35    assert_match %r{name: "David"}, json 
     36    assert_match %r{age: 27}, json 
     37    assert_match %r{awesome: true}, json 
     38    assert_match %r{gender: "Male"}, json 
     39  end 
     40 
     41  def test_binary_attributes_should_be_skipped 
     42    assert_no_match %r{avatar: }, @david.to_json 
     43  end 
     44 
     45  def test_should_allow_attribute_filtering_with_only 
     46    json = @david.to_json(:only => [:age, :name]) 
     47 
     48    assert_match %r{name: "David"}, json 
     49    assert_match %r{age: 27}, json 
     50    assert_no_match %r{awesome: true}, json 
     51    assert_no_match %r{gender: "Male"}, json 
     52  end 
     53 
     54  def test_should_allow_attribute_filtering_with_except 
     55    json = @david.to_json(:except => [:age, :gender]) 
     56 
     57    assert_match %r{name: "David"}, json 
     58    assert_match %r{awesome: true}, json 
     59    assert_no_match %r{age: 27}, json 
     60    assert_no_match %r{gender: "Male"}, json 
     61  end 
     62 
     63  def test_methods_are_called_on_object 
     64    # Define methods on fixture. 
     65    def @david.label; "Father of Rails"; end 
     66    def @david.favorite_quote; "Constraints are liberating"; end 
     67 
     68    # Single method. 
     69    assert_equal %({name: "David", label: "Father of Rails"}), @david.to_json(:only => :name, :methods => :label) 
     70 
     71    # Both methods. 
     72    methods_json = @david.to_json(:only => :name, :methods => [:label, :favorite_quote]) 
     73    assert_match %r{label: "Father of Rails"}, methods_json 
     74    assert_match %r{favorite_quote: "Constraints are liberating"}, methods_json 
     75  end 
     76end 
     77 
     78 
     79class DatabaseConnectedJsonEncodingTest < Test::Unit::TestCase 
     80  fixtures :authors, :posts, :comments 
     81 
     82  def setup 
     83    @david = authors(:david) 
     84  end 
     85 
     86  def test_includes_uses_association_name 
     87    json = @david.to_json(:include => :posts) 
     88 
     89    assert_match %r{id: 1}, json 
     90    assert_match %r{name: "David"}, json 
     91    assert_match %r{posts: \[}, json 
     92 
     93    assert_match %r{author_id: 1}, json 
     94    assert_match %r{title: "Welcome to the weblog"}, json 
     95    assert_match %r{body: "Such a lovely day"}, json 
     96 
     97    assert_match %r{title: "So I was thinking"}, json 
     98    assert_match %r{body: "Like I hopefully always am"}, json 
     99  end 
     100 
     101  def test_includes_uses_association_name_and_applies_attribute_filters 
     102    json = @david.to_json(:include => { :posts => { :only => :title } }) 
     103 
     104    assert_match %r{name: "David"}, json 
     105    assert_match %r{posts: \[}, json 
     106 
     107    assert_match %r{title: "Welcome to the weblog"}, json 
     108    assert_no_match %r{body: "Such a lovely day"}, json 
     109 
     110    assert_match %r{title: "So I was thinking"}, json 
     111    assert_no_match %r{body: "Like I hopefully always am"}, json 
     112  end 
     113 
     114  def test_includes_fetches_second_level_associations 
     115    json = @david.to_json(:include => { :posts => { :include => { :comments => { :only => :body } } } }) 
     116 
     117    assert_match %r{name: "David"}, json 
     118    assert_match %r{posts: \[}, json 
     119 
     120    assert_match %r{comments: \[}, json 
     121    assert_match %r{\{body: "Thank you again for the welcome"\}}, json 
     122    assert_match %r{\{body: "Don't think too hard"\}}, json 
     123    assert_no_match %r{post_id: }, json 
     124  end 
     125 
     126  def test_should_not_call_methods_on_associations_that_dont_respond 
     127    def @david.favorite_quote; "Constraints are liberating"; end 
     128    json = @david.to_json(:include => :posts, :methods => :favorite_quote) 
     129 
     130    assert !@david.posts.first.respond_to?(:favorite_quote) 
     131    assert_match %r{favorite_quote: "Constraints are liberating"}, json 
     132    assert_equal %r{favorite_quote: }.match(json).size, 1 
     133  end 
     134end 
  • lib/active_record/json_encoding.rb

    old new  
     1module ActiveRecord #:nodoc: 
     2  module JsonEncoding 
     3    def to_json(options = {}) 
     4      hashifier = JsonHashifier.new(self, options) 
     5      hashifier.to_hash.to_json 
     6    end 
     7 
     8 
     9    class JsonHashifier #:nodoc: 
     10      attr_reader :options 
     11 
     12      def initialize(record, options = {}) 
     13        @record, @options = record, options.dup 
     14      end 
     15 
     16      # Outputs AR record instance method as a hash that can be easily 
     17      # encoded as JSON. 
     18      def to_hash 
     19        hash = {} 
     20 
     21        hash.merge!(simple_attributes) 
     22        hash.merge!(method_attributes) 
     23        hash.merge!(association_attributes) 
     24 
     25        hash 
     26      end 
     27 
     28      # Returns 1st level attributes as a hash. 
     29      def simple_attributes 
     30        attribute_names = @record.attribute_names 
     31 
     32        if options[:only] 
     33          options.delete(:except) 
     34          attribute_names = attribute_names & Array(options[:only]).collect(&:to_s) 
     35        else 
     36          options[:except] = Array(options[:except]) | Array(@record.class.inheritance_column) 
     37          attribute_names = attribute_names - options[:except].collect(&:to_s) 
     38        end 
     39 
     40        attribute_names.reject! { |n| binary_attribute?(n) } # Don't JSON-ify binary fields! 
     41 
     42        @record.attributes(:only => attribute_names) 
     43      end 
     44 
     45      # Returns 1st level methods as a hash. 
     46      def method_attributes 
     47        Array(options[:methods]).inject({}) do |method_attributes, name| 
     48          method_attributes.merge!({ name.to_s => @record.send(name.to_s) }) if @record.respond_to?(name.to_s) 
     49          method_attributes 
     50        end 
     51      end 
     52 
     53      # Returns 1st level associations as a hash. Recursively "hashifies" 
     54      # associations so that nth level associations are converted to JSON as well. 
     55      def association_attributes 
     56        hash = {} 
     57 
     58        if include_associations = options.delete(:include) 
     59          base_only_or_except = { :except => options[:except], 
     60                                  :only => options[:only] } 
     61 
     62          include_has_options = include_associations.is_a?(Hash) 
     63 
     64          for association in include_has_options ? include_associations.keys : Array(include_associations) 
     65            association_options = include_has_options ? include_associations[association] : base_only_or_except 
     66 
     67            opts = options.merge(association_options) 
     68 
     69            case @record.class.reflect_on_association(association).macro 
     70            when :has_many, :has_and_belongs_to_many 
     71              records = @record.send(association).to_a 
     72              unless records.empty? 
     73                hash[association] = records.collect { |r| JsonHashifier.new(r, opts).to_hash } 
     74              end 
     75            when :has_one, :belongs_to 
     76              if record = @record.send(association) 
     77                hash[association] = JsonHashifier.new(record, opts).to_hash 
     78              end 
     79            end 
     80          end 
     81 
     82          options[:include] = include_associations 
     83        end 
     84 
     85        hash 
     86      end 
     87 
     88      protected 
     89 
     90        def binary_attribute?(name) 
     91          !@record.class.serialized_attributes.has_key?(name) && @record.class.columns_hash[name].type == :binary 
     92        end 
     93    end 
     94 
     95  end 
     96end 
  • lib/active_record.rb

    old new  
    5252require 'active_record/schema' 
    5353require 'active_record/calculations' 
    5454require 'active_record/xml_serialization' 
     55require 'active_record/json_encoding' 
    5556require 'active_record/attribute_methods' 
    5657 
    5758ActiveRecord::Base.class_eval do 
     
    7071  include ActiveRecord::Acts::NestedSet 
    7172  include ActiveRecord::Calculations 
    7273  include ActiveRecord::XmlSerialization 
     74  include ActiveRecord::JsonEncoding 
    7375  include ActiveRecord::AttributeMethods 
    7476end 
    7577