From 209e1dbaa5ee789163cd3dc39e8aad6518dce9d5 Mon Sep 17 00:00:00 2001 From: Robert Ross Date: Sat, 31 Mar 2012 02:42:50 -0700 Subject: [PATCH 1/7] Add format_with option to exposures + spec. --- lib/grape/entity.rb | 2 ++ spec/grape/entity_spec.rb | 17 ++++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/grape/entity.rb b/lib/grape/entity.rb index b89ee88869..0087951af7 100644 --- a/lib/grape/entity.rb +++ b/lib/grape/entity.rb @@ -202,6 +202,8 @@ def value_for(attribute, options = {}) exposure_options[:proc].call(object, options) elsif exposure_options[:using] exposure_options[:using].represent(object.send(attribute), :root => nil) + elsif exposure_options[:format_with] + self.send(exposure_options[:format_with].to_sym, object.send(attribute)) else object.send(attribute) end diff --git a/spec/grape/entity_spec.rb b/spec/grape/entity_spec.rb index 3ddc6e5bf9..b190d71a37 100644 --- a/spec/grape/entity_spec.rb +++ b/spec/grape/entity_spec.rb @@ -201,11 +201,12 @@ context 'instance methods' do let(:model){ mock(attributes) } let(:attributes){ { - :name => 'Bob Bobson', + :name => 'Bob Bobson', :email => 'bob@example.com', + :birthday => Time.new(2012, 2, 27), :friends => [ - mock(:name => "Friend 1", :email => 'friend1@example.com', :friends => []), - mock(:name => "Friend 2", :email => 'friend2@example.com', :friends => []) + mock(:name => "Friend 1", :email => 'friend1@example.com', :birthday => Time.new(2012, 2, 27), :friends => []), + mock(:name => "Friend 2", :email => 'friend2@example.com', :birthday => Time.new(2012, 2, 27), :friends => []) ] } } subject{ fresh_class.new(model) } @@ -229,6 +230,12 @@ expose :computed do |object, options| options[:awesome] end + + expose :birthday, :format_with => :timestamp + + def timestamp(date) + date.strftime('%m/%d/%Y') + end end end @@ -261,6 +268,10 @@ class FriendEntity < Grape::Entity it 'should call through to the proc if there is one' do subject.send(:value_for, :computed, :awesome => 123).should == 123 end + + it 'should return a formatted value if format_with is passed' do + subject.send(:value_for, :birthday).should == '02/27/2012' + end end describe '#key_for' do From f1401eb8af54bfea0bf9899a779a9783578a2105 Mon Sep 17 00:00:00 2001 From: Robert Ross Date: Sat, 31 Mar 2012 02:53:29 -0700 Subject: [PATCH 2/7] Add ability to use lambdas with format_with options. --- lib/grape/entity.rb | 6 +++++- spec/grape/entity_spec.rb | 11 +++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/grape/entity.rb b/lib/grape/entity.rb index 0087951af7..d351a8beee 100644 --- a/lib/grape/entity.rb +++ b/lib/grape/entity.rb @@ -203,7 +203,11 @@ def value_for(attribute, options = {}) elsif exposure_options[:using] exposure_options[:using].represent(object.send(attribute), :root => nil) elsif exposure_options[:format_with] - self.send(exposure_options[:format_with].to_sym, object.send(attribute)) + if exposure_options[:format_with].is_a? Symbol + self.send(exposure_options[:format_with].to_sym, object.send(attribute)) + elsif exposure_options[:format_with].respond_to? :call + exposure_options[:format_with].call(object.send(attribute)) + end else object.send(attribute) end diff --git a/spec/grape/entity_spec.rb b/spec/grape/entity_spec.rb index b190d71a37..a87e889749 100644 --- a/spec/grape/entity_spec.rb +++ b/spec/grape/entity_spec.rb @@ -204,9 +204,10 @@ :name => 'Bob Bobson', :email => 'bob@example.com', :birthday => Time.new(2012, 2, 27), + :fantasies => ['Unicorns', 'Double Rainbows', 'Nessy'], :friends => [ - mock(:name => "Friend 1", :email => 'friend1@example.com', :birthday => Time.new(2012, 2, 27), :friends => []), - mock(:name => "Friend 2", :email => 'friend2@example.com', :birthday => Time.new(2012, 2, 27), :friends => []) + mock(:name => "Friend 1", :email => 'friend1@example.com', :fantasies => [], :birthday => Time.new(2012, 2, 27), :friends => []), + mock(:name => "Friend 2", :email => 'friend2@example.com', :fantasies => [], :birthday => Time.new(2012, 2, 27), :friends => []) ] } } subject{ fresh_class.new(model) } @@ -236,6 +237,8 @@ def timestamp(date) date.strftime('%m/%d/%Y') end + + expose :fantasies, :format_with => lambda {|f| f.reverse } end end @@ -272,6 +275,10 @@ class FriendEntity < Grape::Entity it 'should return a formatted value if format_with is passed' do subject.send(:value_for, :birthday).should == '02/27/2012' end + + it 'should return a formatted value if format_with is passed a lambda' do + subject.send(:value_for, :fantasies).should == ['Nessy', 'Double Rainbows', 'Unicorns'] + end end describe '#key_for' do From 19d8a542ee643bfdac31844962320e8835bc8843 Mon Sep 17 00:00:00 2001 From: Robert Ross Date: Sat, 31 Mar 2012 02:54:40 -0700 Subject: [PATCH 3/7] Add some code clarity since it was getting messy. --- lib/grape/entity.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/grape/entity.rb b/lib/grape/entity.rb index d351a8beee..b4c44182e9 100644 --- a/lib/grape/entity.rb +++ b/lib/grape/entity.rb @@ -203,10 +203,12 @@ def value_for(attribute, options = {}) elsif exposure_options[:using] exposure_options[:using].represent(object.send(attribute), :root => nil) elsif exposure_options[:format_with] - if exposure_options[:format_with].is_a? Symbol - self.send(exposure_options[:format_with].to_sym, object.send(attribute)) - elsif exposure_options[:format_with].respond_to? :call - exposure_options[:format_with].call(object.send(attribute)) + format_with = exposure_options[:format_with] + + if format_with.is_a? Symbol + self.send(format_with.to_sym, object.send(attribute)) + elsif format_with.respond_to? :call + format_with.call(object.send(attribute)) end else object.send(attribute) From 976c1abe3ed12a9ad7eb1e688db5d2d8b6f02eca Mon Sep 17 00:00:00 2001 From: Robert Ross Date: Sat, 31 Mar 2012 03:00:29 -0700 Subject: [PATCH 4/7] Make sure that people can't use Procs for :format_with and the exposure. --- lib/grape/entity.rb | 2 ++ spec/grape/entity_spec.rb | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/lib/grape/entity.rb b/lib/grape/entity.rb index b4c44182e9..04169bfede 100644 --- a/lib/grape/entity.rb +++ b/lib/grape/entity.rb @@ -71,6 +71,8 @@ def self.expose(*args, &block) raise ArgumentError, "You may not use block-setting on multi-attribute exposures." if block_given? end + raise ArgumentError, "You may not use block-setting when also using " if block_given? && options[:format_with].respond_to?(:call) + options[:proc] = block if block_given? args.each do |attribute| diff --git a/spec/grape/entity_spec.rb b/spec/grape/entity_spec.rb index a87e889749..aa831e015f 100644 --- a/spec/grape/entity_spec.rb +++ b/spec/grape/entity_spec.rb @@ -24,6 +24,10 @@ expect{ subject.expose :name, :email, :as => :foo }.to raise_error(ArgumentError) expect{ subject.expose :name, :as => :foo }.not_to raise_error end + + it 'should make sure that :format_with as a proc can not be used with a block' do + expect { subject.expose :name, :format_with => Proc.new {} do |object,options| end }.to raise_error(ArgumentError) + end end context 'with a block' do From f6cd72d3bdb49ddd2d4f4122062cbbdbdd5dc513 Mon Sep 17 00:00:00 2001 From: Robert Ross Date: Sat, 31 Mar 2012 03:05:23 -0700 Subject: [PATCH 5/7] I like being redundant occasionally. --- lib/grape/entity.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/grape/entity.rb b/lib/grape/entity.rb index 04169bfede..9d73af4d49 100644 --- a/lib/grape/entity.rb +++ b/lib/grape/entity.rb @@ -208,7 +208,7 @@ def value_for(attribute, options = {}) format_with = exposure_options[:format_with] if format_with.is_a? Symbol - self.send(format_with.to_sym, object.send(attribute)) + self.send(format_with, object.send(attribute)) elsif format_with.respond_to? :call format_with.call(object.send(attribute)) end From fceab9318cc2463347b65f0ff9b15b86c3abfbc2 Mon Sep 17 00:00:00 2001 From: Robert Ross Date: Sat, 31 Mar 2012 17:34:08 -0700 Subject: [PATCH 6/7] Add ability to register a formatter on entity classes to be used with :format_with option. --- lib/grape/entity.rb | 26 +++++++++++++++++++++++++- spec/grape/entity_spec.rb | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/lib/grape/entity.rb b/lib/grape/entity.rb index 9d73af4d49..d64ecec06d 100644 --- a/lib/grape/entity.rb +++ b/lib/grape/entity.rb @@ -93,6 +93,24 @@ def self.exposures @exposures end + # This allows you to declare a Proc in which exposures can be formatted with. + # It take a block with an arity of 1 which is passed as the value of the exposed attribute. + def self.format_with(name, &block) + raise ArgumentError, "You must has a block for formatters" unless block_given? + formatters[name.to_sym] = block + end + + # Returns a hash of all formatters that are registered for this and it's ancestors. + def self.formatters + @formatters ||= {} + + if superclass.respond_to? :formatters + @formatters = superclass.formatters.merge(@formatters) + end + + @formatters + end + # This allows you to set a root element name for your representation. # # @param plural [String] the root key to use when representing @@ -173,6 +191,10 @@ def exposures self.class.exposures end + def formatters + self.class.formatters + end + # The serializable hash is the Entity's primary output. It is the transformed # hash for the given data model and is used as the basis for serialization to # JSON and other formats. @@ -207,7 +229,9 @@ def value_for(attribute, options = {}) elsif exposure_options[:format_with] format_with = exposure_options[:format_with] - if format_with.is_a? Symbol + if format_with.is_a?(Symbol) && formatters[format_with] + formatters[format_with].call(object.send(attribute)) + elsif format_with.is_a?(Symbol) self.send(format_with, object.send(attribute)) elsif format_with.respond_to? :call format_with.call(object.send(attribute)) diff --git a/spec/grape/entity_spec.rb b/spec/grape/entity_spec.rb index aa831e015f..8a56cde6b2 100644 --- a/spec/grape/entity_spec.rb +++ b/spec/grape/entity_spec.rb @@ -71,6 +71,38 @@ child_class.exposures[:name].should have_key :proc end end + + context 'register formatters' do + let(:date_formatter) { lambda {|date| date.strftime('%m/%d/%Y') }} + + it 'should register a formatter' do + subject.format_with :timestamp, &date_formatter + + subject.formatters[:timestamp].should_not be_nil + end + + it 'should inherit formatters from ancestors' do + subject.format_with :timestamp, &date_formatter + child_class = Class.new(subject) + + child_class.formatters.should == subject.formatters + end + + it 'should not allow registering a formatter without a block' do + expect{ subject.format_with :foo }.to raise_error(ArgumentError) + end + + it 'should format an exposure with a registered formatter' do + subject.format_with :timestamp do |date| + date.strftime('%m/%d/%Y') + end + + subject.expose :birthday, :format_with => :timestamp + + model = { :birthday => Time.new(2012, 2, 27) } + subject.new(mock(model)).as_json[:birthday].should == '02/27/2012' + end + end end describe '.represent' do From e9ca05551ae08791647b1cc309e27939b98dfd08 Mon Sep 17 00:00:00 2001 From: Robert Ross Date: Sat, 31 Mar 2012 17:43:47 -0700 Subject: [PATCH 7/7] Add documentation to formatters spec. --- lib/grape/entity.rb | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/lib/grape/entity.rb b/lib/grape/entity.rb index d64ecec06d..46a38a9b8a 100644 --- a/lib/grape/entity.rb +++ b/lib/grape/entity.rb @@ -95,6 +95,32 @@ def self.exposures # This allows you to declare a Proc in which exposures can be formatted with. # It take a block with an arity of 1 which is passed as the value of the exposed attribute. + # + # @param name [Symbol] the name of the formatter + # @param block [Proc] the block that will interpret the exposed attribute + # + # + # + # @example Formatter declaration + # + # module API + # module Entities + # class User < Grape::Entity + # format_with :timestamp do |date| + # date.strftime('%m/%d/%Y') + # end + # + # expose :birthday, :last_signed_in, :format_with => :timestamp + # end + # end + # end + # + # @example Formatters are available to all decendants + # + # Grape::Entity.format_with :timestamp do |date| + # date.strftime('%m/%d/%Y') + # end + # def self.format_with(name, &block) raise ArgumentError, "You must has a block for formatters" unless block_given? formatters[name.to_sym] = block