From 39bee48ae63c78cd11b72ec9d1543d6c9c3dd22f Mon Sep 17 00:00:00 2001 From: Aaron Renoir Date: Sun, 26 Oct 2014 13:04:14 -0700 Subject: [PATCH 1/4] implement sparse fieldsets http://jsonapi.org/format/#fetching-sparse-fieldsets --- lib/active_model/serializer.rb | 9 ++- lib/active_model/serializer/adapter.rb | 1 + .../serializer/adapter/json_api.rb | 10 +++- lib/active_model/serializer/fieldset.rb | 33 +++++++++++ lib/active_model_serializers.rb | 1 + test/adapter/json_api/fieldset_test.rb | 56 +++++++++++++++++++ test/serializers/attributes_test.rb | 6 +- 7 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 lib/active_model/serializer/fieldset.rb create mode 100644 test/adapter/json_api/fieldset_test.rb diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index 7050dc38..0f1fc2eb 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -124,7 +124,14 @@ module ActiveModel end def attributes(options = {}) - self.class._attributes.dup.each_with_object({}) do |name, hash| + attributes = + if options[:fields] + self.class._attributes & options[:fields] + else + self.class._attributes.dup + end + + attributes.each_with_object({}) do |name, hash| hash[name] = send(name) end end diff --git a/lib/active_model/serializer/adapter.rb b/lib/active_model/serializer/adapter.rb index f0a82573..b8a8b9df 100644 --- a/lib/active_model/serializer/adapter.rb +++ b/lib/active_model/serializer/adapter.rb @@ -17,6 +17,7 @@ module ActiveModel end def to_json(options = {}) + options[:fieldset] = ActiveModel::Serializer::Fieldset.new(serializer, options[:fields]) serializable_hash(options).to_json end end diff --git a/lib/active_model/serializer/adapter/json_api.rb b/lib/active_model/serializer/adapter/json_api.rb index 71327b3c..8f09b82b 100644 --- a/lib/active_model/serializer/adapter/json_api.rb +++ b/lib/active_model/serializer/adapter/json_api.rb @@ -10,9 +10,12 @@ module ActiveModel def serializable_hash(options = {}) @root = (options[:root] || serializer.json_key).to_s.pluralize.to_sym @hash = {} + @fieldset = options[:fieldset] if serializer.respond_to?(:each) - @hash[@root] = serializer.map{|s| self.class.new(s).serializable_hash[@root] } + opt = @fieldset ? {fieldset: @fieldset} : {} + + @hash[@root] = serializer.map{|s| self.class.new(s).serializable_hash(opt)[@root] } else @hash[@root] = attributes_for_serializer(serializer, {}) @@ -57,6 +60,11 @@ module ActiveModel private def attributes_for_serializer(serializer, options) + + if fields = @fieldset && @fieldset.fields_for(serializer) + options[:fields] = fields + end + attributes = serializer.attributes(options) attributes[:id] = attributes[:id].to_s if attributes[:id] attributes diff --git a/lib/active_model/serializer/fieldset.rb b/lib/active_model/serializer/fieldset.rb new file mode 100644 index 00000000..e7d288f7 --- /dev/null +++ b/lib/active_model/serializer/fieldset.rb @@ -0,0 +1,33 @@ +module ActiveModel + class Serializer + class Fieldset + + attr_accessor :fields, :root + + def initialize(serializer, fields = {}) + @root = serializer.json_key + @fields = parse(fields) + end + + def fields_for(serializer) + key = serializer.json_key || serializer.class.root_name + fields[key] + end + + private + + def parse(fields) + if fields.is_a?(Hash) + fields.inject({}) { |h,(k,v)| h[k.to_s] = v.map(&:to_sym); h} + elsif fields.is_a?(Array) + hash = {} + hash[root.to_s] = fields.map(&:to_sym) + hash + else + {} + end + end + + end + end +end \ No newline at end of file diff --git a/lib/active_model_serializers.rb b/lib/active_model_serializers.rb index c96b90a9..5b0ed1b1 100644 --- a/lib/active_model_serializers.rb +++ b/lib/active_model_serializers.rb @@ -1,6 +1,7 @@ require "active_model" require "active_model/serializer/version" require "active_model/serializer" +require "active_model/serializer/fieldset" begin require 'action_controller' diff --git a/test/adapter/json_api/fieldset_test.rb b/test/adapter/json_api/fieldset_test.rb new file mode 100644 index 00000000..03619724 --- /dev/null +++ b/test/adapter/json_api/fieldset_test.rb @@ -0,0 +1,56 @@ +require 'test_helper' + +module ActiveModel + class Serializer + class Adapter + class JsonApi + class FieldsetTest < Minitest::Test + def setup + @post = Post.new(title: 'New Post', body: 'Body') + @first_comment = Comment.new(id: 1, body: 'ZOMG A COMMENT') + @second_comment = Comment.new(id: 2, body: 'ZOMG ANOTHER COMMENT') + @post.comments = [@first_comment, @second_comment] + @first_comment.post = @post + @second_comment.post = @post + + @serializer = PostSerializer.new(@post) + @adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer) + end + + def teardown + @serializer = nil + @adapter = nil + end + + def test_fieldset_with_fields_array + fieldset = ActiveModel::Serializer::Fieldset.new(@serializer, ['title']) + + assert_equal( + {:title=>"New Post", :links=>{:comments=>["1", "2"]}}, + @adapter.serializable_hash({fieldset: fieldset})[:posts] + ) + end + + def test_fieldset_with_hash + fieldset = ActiveModel::Serializer::Fieldset.new(@serializer, {post: [:body]}) + + assert_equal( + {:body=>"Body", :links=>{:comments=>["1", "2"]}}, + @adapter.serializable_hash({fieldset: fieldset})[:posts] + ) + end + + def test_fieldset_with_multiple_hashes + fieldset = ActiveModel::Serializer::Fieldset.new(@serializer, {post: [:title], comment: [:body]}) + + assert_equal( + [{:body=>"ZOMG A COMMENT" }, {:body=>"ZOMG ANOTHER COMMENT"}], + @adapter.serializable_hash({fieldset: fieldset})[:linked][:comments] + ) + end + + end + end + end + end +end \ No newline at end of file diff --git a/test/serializers/attributes_test.rb b/test/serializers/attributes_test.rb index f9467b18..7c4bbc8b 100644 --- a/test/serializers/attributes_test.rb +++ b/test/serializers/attributes_test.rb @@ -12,7 +12,11 @@ module ActiveModel assert_equal([:name, :description], @profile_serializer.class._attributes) end + + def test_attributes_with_fields_option + assert_equal({name: 'Name 1'}, + @profile_serializer.attributes( { fields: [:name] } ) ) + end end end end - From 34f08477e4cc6838668a5a87fe3f61460bac5164 Mon Sep 17 00:00:00 2001 From: Aaron Renoir Date: Sun, 26 Oct 2014 14:41:14 -0700 Subject: [PATCH 2/4] fix tests, but need to understand how the serializer class attribute _associations was getting changed. --- test/adapter/json_api/fieldset_test.rb | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/test/adapter/json_api/fieldset_test.rb b/test/adapter/json_api/fieldset_test.rb index 03619724..ded4e4a9 100644 --- a/test/adapter/json_api/fieldset_test.rb +++ b/test/adapter/json_api/fieldset_test.rb @@ -7,19 +7,13 @@ module ActiveModel class FieldsetTest < Minitest::Test def setup @post = Post.new(title: 'New Post', body: 'Body') - @first_comment = Comment.new(id: 1, body: 'ZOMG A COMMENT') - @second_comment = Comment.new(id: 2, body: 'ZOMG ANOTHER COMMENT') - @post.comments = [@first_comment, @second_comment] - @first_comment.post = @post - @second_comment.post = @post + comment_1 = Comment.new(id: 1, body: 'comment one') + comment_2 = Comment.new(id: 2, body: 'comment two') + @post.comments = [comment_1, comment_2] @serializer = PostSerializer.new(@post) @adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer) - end - def teardown - @serializer = nil - @adapter = nil end def test_fieldset_with_fields_array @@ -44,9 +38,13 @@ module ActiveModel fieldset = ActiveModel::Serializer::Fieldset.new(@serializer, {post: [:title], comment: [:body]}) assert_equal( - [{:body=>"ZOMG A COMMENT" }, {:body=>"ZOMG ANOTHER COMMENT"}], + [{:body=>"comment one" }, {:body=>"comment two"}], @adapter.serializable_hash({fieldset: fieldset})[:linked][:comments] ) + + #don't understand how this is getting set. + @serializer.class._associations[:comments][:options] = {} + end end From be54e0bc4fa6eb0be130ee146614701a0e709e7d Mon Sep 17 00:00:00 2001 From: Aaron Renoir Date: Mon, 27 Oct 2014 15:24:19 -0700 Subject: [PATCH 3/4] remove serializer dependency from fieldset --- lib/active_model/serializer/adapter.rb | 5 ++++- lib/active_model/serializer/fieldset.rb | 15 ++++++++------ test/adapter/json_api/fieldset_test.rb | 6 +++--- test/serializers/fieldset_test.rb | 26 +++++++++++++++++++++++++ 4 files changed, 42 insertions(+), 10 deletions(-) create mode 100644 test/serializers/fieldset_test.rb diff --git a/lib/active_model/serializer/adapter.rb b/lib/active_model/serializer/adapter.rb index b8a8b9df..de46f12a 100644 --- a/lib/active_model/serializer/adapter.rb +++ b/lib/active_model/serializer/adapter.rb @@ -17,7 +17,10 @@ module ActiveModel end def to_json(options = {}) - options[:fieldset] = ActiveModel::Serializer::Fieldset.new(serializer, options[:fields]) + if fields = options.delete(:fields) + options[:fieldset] = ActiveModel::Serializer::Fieldset.new(fields, serializer.json_key) + end + serializable_hash(options).to_json end end diff --git a/lib/active_model/serializer/fieldset.rb b/lib/active_model/serializer/fieldset.rb index e7d288f7..dc3ab857 100644 --- a/lib/active_model/serializer/fieldset.rb +++ b/lib/active_model/serializer/fieldset.rb @@ -2,26 +2,29 @@ module ActiveModel class Serializer class Fieldset - attr_accessor :fields, :root + attr_reader :fields, :root - def initialize(serializer, fields = {}) - @root = serializer.json_key + def initialize(fields, root = nil) + @root = root @fields = parse(fields) end def fields_for(serializer) key = serializer.json_key || serializer.class.root_name - fields[key] + fields[key.to_sym] end private def parse(fields) if fields.is_a?(Hash) - fields.inject({}) { |h,(k,v)| h[k.to_s] = v.map(&:to_sym); h} + fields.inject({}) { |h,(k,v)| h[k.to_sym] = v.map(&:to_sym); h} elsif fields.is_a?(Array) + if root.nil? + raise ArgumentError, 'The root argument must be specified if the fileds argument is an array.' + end hash = {} - hash[root.to_s] = fields.map(&:to_sym) + hash[root.to_sym] = fields.map(&:to_sym) hash else {} diff --git a/test/adapter/json_api/fieldset_test.rb b/test/adapter/json_api/fieldset_test.rb index ded4e4a9..cc08ba11 100644 --- a/test/adapter/json_api/fieldset_test.rb +++ b/test/adapter/json_api/fieldset_test.rb @@ -17,7 +17,7 @@ module ActiveModel end def test_fieldset_with_fields_array - fieldset = ActiveModel::Serializer::Fieldset.new(@serializer, ['title']) + fieldset = ActiveModel::Serializer::Fieldset.new(['title'], 'post') assert_equal( {:title=>"New Post", :links=>{:comments=>["1", "2"]}}, @@ -26,7 +26,7 @@ module ActiveModel end def test_fieldset_with_hash - fieldset = ActiveModel::Serializer::Fieldset.new(@serializer, {post: [:body]}) + fieldset = ActiveModel::Serializer::Fieldset.new({post: [:body]}) assert_equal( {:body=>"Body", :links=>{:comments=>["1", "2"]}}, @@ -35,7 +35,7 @@ module ActiveModel end def test_fieldset_with_multiple_hashes - fieldset = ActiveModel::Serializer::Fieldset.new(@serializer, {post: [:title], comment: [:body]}) + fieldset = ActiveModel::Serializer::Fieldset.new({post: [:title], comment: [:body]}) assert_equal( [{:body=>"comment one" }, {:body=>"comment two"}], diff --git a/test/serializers/fieldset_test.rb b/test/serializers/fieldset_test.rb new file mode 100644 index 00000000..05439177 --- /dev/null +++ b/test/serializers/fieldset_test.rb @@ -0,0 +1,26 @@ +require 'test_helper' + +module ActiveModel + class Serializer + class FieldsetTest < Minitest::Test + + def test_fieldset_with_hash + fieldset = ActiveModel::Serializer::Fieldset.new({'post' => ['id', 'title'], 'coment' => ['body']}) + + assert_equal( + {:post=>[:id, :title], :coment=>[:body]}, + fieldset.fields + ) + end + + def test_fieldset_with_array_of_fields_and_root_name + fieldset = ActiveModel::Serializer::Fieldset.new(['title'], 'post') + + assert_equal( + {:post => [:title]}, + fieldset.fields + ) + end + end + end +end \ No newline at end of file From fc1562c04ad5df16a264c2a5b7b15f507d5588f7 Mon Sep 17 00:00:00 2001 From: Aaron Renoir Date: Wed, 5 Nov 2014 18:10:37 -0800 Subject: [PATCH 4/4] add fields to adapter initialize function, pull in master, add tests using includes with fields --- lib/action_controller/serialization.rb | 2 +- lib/active_model/serializer/adapter.rb | 6 +-- .../serializer/adapter/json_api.rb | 10 ++-- test/adapter/json_api/belongs_to_test.rb | 5 ++ test/adapter/json_api/collection_test.rb | 9 ++++ test/adapter/json_api/fieldset_test.rb | 54 ------------------- test/adapter/json_api/has_many_test.rb | 8 +++ 7 files changed, 31 insertions(+), 63 deletions(-) delete mode 100644 test/adapter/json_api/fieldset_test.rb diff --git a/lib/action_controller/serialization.rb b/lib/action_controller/serialization.rb index 2b460cb4..aa872ae9 100644 --- a/lib/action_controller/serialization.rb +++ b/lib/action_controller/serialization.rb @@ -6,7 +6,7 @@ module ActionController include ActionController::Renderers - ADAPTER_OPTION_KEYS = [:include, :root] + ADAPTER_OPTION_KEYS = [:include, :fields, :root] [:_render_option_json, :_render_with_renderer_json].each do |renderer_method| define_method renderer_method do |resource, options| diff --git a/lib/active_model/serializer/adapter.rb b/lib/active_model/serializer/adapter.rb index 31b6ea57..84649285 100644 --- a/lib/active_model/serializer/adapter.rb +++ b/lib/active_model/serializer/adapter.rb @@ -18,11 +18,7 @@ module ActiveModel end def as_json(options = {}) - if fields = options.delete(:fields) - options[:fieldset] = ActiveModel::Serializer::Fieldset.new(fields, serializer.json_key) - end - - serializable_hash(options).to_json + serializable_hash(options) end end end diff --git a/lib/active_model/serializer/adapter/json_api.rb b/lib/active_model/serializer/adapter/json_api.rb index d3d4dd17..ea74fc8c 100644 --- a/lib/active_model/serializer/adapter/json_api.rb +++ b/lib/active_model/serializer/adapter/json_api.rb @@ -7,15 +7,20 @@ module ActiveModel serializer.root = true @hash = {} @top = @options.fetch(:top) { @hash } + + if fields = options.delete(:fields) + @fieldset = ActiveModel::Serializer::Fieldset.new(fields, serializer.json_key) + else + @fieldset = options[:fieldset] + end end def serializable_hash(options = {}) @root = (@options[:root] || serializer.json_key.to_s.pluralize).to_sym - @fieldset = options[:fieldset] if serializer.respond_to?(:each) @hash[@root] = serializer.map do |s| - self.class.new(s, @options.merge(top: @top)).serializable_hash[@root] + self.class.new(s, @options.merge(top: @top, fieldset: @fieldset)).serializable_hash[@root] end else @hash[@root] = attributes_for_serializer(serializer, @options) @@ -94,7 +99,6 @@ module ActiveModel private def attributes_for_serializer(serializer, options) - if fields = @fieldset && @fieldset.fields_for(serializer) options[:fields] = fields end diff --git a/test/adapter/json_api/belongs_to_test.rb b/test/adapter/json_api/belongs_to_test.rb index e25a71d8..048b55b7 100644 --- a/test/adapter/json_api/belongs_to_test.rb +++ b/test/adapter/json_api/belongs_to_test.rb @@ -34,6 +34,11 @@ module ActiveModel assert_equal([{id: "42", title: 'New Post', body: 'Body'}], @adapter.serializable_hash[:linked][:posts]) end + def test_limiting_linked_post_fields + @adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer, include: 'post', fields: {post: [:title]}) + assert_equal([{title: 'New Post'}], @adapter.serializable_hash[:linked][:posts]) + end + def test_include_nil_author serializer = PostSerializer.new(@anonymous_post) adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer) diff --git a/test/adapter/json_api/collection_test.rb b/test/adapter/json_api/collection_test.rb index 922103ea..acea7790 100644 --- a/test/adapter/json_api/collection_test.rb +++ b/test/adapter/json_api/collection_test.rb @@ -25,6 +25,15 @@ module ActiveModel { title: "New Post", body: "Body", id: "2", links: { comments: [], author: "1" } } ], @adapter.serializable_hash[:posts]) end + + def test_limiting_fields + @adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer, fields: ['title']) + assert_equal([ + { title: "Hello!!", links: { comments: [], author: "1" } }, + { title: "New Post", links: { comments: [], author: "1" } } + ], @adapter.serializable_hash[:posts]) + end + end end end diff --git a/test/adapter/json_api/fieldset_test.rb b/test/adapter/json_api/fieldset_test.rb deleted file mode 100644 index cc08ba11..00000000 --- a/test/adapter/json_api/fieldset_test.rb +++ /dev/null @@ -1,54 +0,0 @@ -require 'test_helper' - -module ActiveModel - class Serializer - class Adapter - class JsonApi - class FieldsetTest < Minitest::Test - def setup - @post = Post.new(title: 'New Post', body: 'Body') - comment_1 = Comment.new(id: 1, body: 'comment one') - comment_2 = Comment.new(id: 2, body: 'comment two') - @post.comments = [comment_1, comment_2] - - @serializer = PostSerializer.new(@post) - @adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer) - - end - - def test_fieldset_with_fields_array - fieldset = ActiveModel::Serializer::Fieldset.new(['title'], 'post') - - assert_equal( - {:title=>"New Post", :links=>{:comments=>["1", "2"]}}, - @adapter.serializable_hash({fieldset: fieldset})[:posts] - ) - end - - def test_fieldset_with_hash - fieldset = ActiveModel::Serializer::Fieldset.new({post: [:body]}) - - assert_equal( - {:body=>"Body", :links=>{:comments=>["1", "2"]}}, - @adapter.serializable_hash({fieldset: fieldset})[:posts] - ) - end - - def test_fieldset_with_multiple_hashes - fieldset = ActiveModel::Serializer::Fieldset.new({post: [:title], comment: [:body]}) - - assert_equal( - [{:body=>"comment one" }, {:body=>"comment two"}], - @adapter.serializable_hash({fieldset: fieldset})[:linked][:comments] - ) - - #don't understand how this is getting set. - @serializer.class._associations[:comments][:options] = {} - - end - - end - end - end - end -end \ No newline at end of file diff --git a/test/adapter/json_api/has_many_test.rb b/test/adapter/json_api/has_many_test.rb index 975c5b63..e4ce2f62 100644 --- a/test/adapter/json_api/has_many_test.rb +++ b/test/adapter/json_api/has_many_test.rb @@ -40,6 +40,14 @@ module ActiveModel ], @adapter.serializable_hash[:linked][:comments]) end + def test_limit_fields_of_linked_comments + @adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer, include: 'comments', fields: {comment: [:body]}) + assert_equal([ + {body: 'ZOMG A COMMENT'}, + {body: 'ZOMG ANOTHER COMMENT'} + ], @adapter.serializable_hash[:linked][:comments]) + end + def test_no_include_linked_if_comments_is_empty serializer = PostSerializer.new(@post_without_comments) adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer)