diff --git a/grape.gemspec b/grape.gemspec index 5b41129742..94ff8f319e 100644 --- a/grape.gemspec +++ b/grape.gemspec @@ -18,7 +18,8 @@ Gem::Specification.new do |s| s.add_runtime_dependency 'rack-mount' s.add_runtime_dependency 'rack-accept' s.add_runtime_dependency 'activesupport' - # s.add_runtime_dependency 'rack-jsonp' + s.add_runtime_dependency 'grape-entity', '>= 0.2.0' + # s.add_runtime_dependency 'rack-jsonp', s.add_runtime_dependency 'multi_json', '>= 1.3.2' s.add_runtime_dependency 'multi_xml', '>= 0.5.2' s.add_runtime_dependency 'hashie', '~> 1.2' diff --git a/lib/grape/entity.rb b/lib/grape/entity.rb deleted file mode 100644 index dd4ae3f34f..0000000000 --- a/lib/grape/entity.rb +++ /dev/null @@ -1,389 +0,0 @@ -module Grape - # An Entity is a lightweight structure that allows you to easily - # represent data from your application in a consistent and abstracted - # way in your API. Entities can also provide documentation for the - # fields exposed. - # - # @example Entity Definition - # - # module API - # module Entities - # class User < Grape::Entity - # expose :first_name, :last_name, :screen_name, :location - # expose :field, :documentation => {:type => "string", :desc => "describe the field"} - # expose :latest_status, :using => API::Status, :as => :status, :unless => {:collection => true} - # expose :email, :if => {:type => :full} - # expose :new_attribute, :if => {:version => 'v2'} - # expose(:name){|model,options| [model.first_name, model.last_name].join(' ')} - # end - # end - # end - # - # Entities are not independent structures, rather, they create - # **representations** of other Ruby objects using a number of methods - # that are convenient for use in an API. Once you've defined an Entity, - # you can use it in your API like this: - # - # @example Usage in the API Layer - # - # module API - # class Users < Grape::API - # version 'v2' - # - # desc 'User index', { :object_fields => API::Entities::User.documentation } - # get '/users' do - # @users = User.all - # type = current_user.admin? ? :full : :default - # present @users, :with => API::Entities::User, :type => type - # end - # end - # end - class Entity - attr_reader :object, :options - - # The Entity DSL allows you to mix entity functionality into - # your existing classes. - module DSL - def self.included(base) - base.extend ClassMethods - ancestor_entity_class = base.ancestors.detect{|a| a.entity_class if a.respond_to?(:entity_class)} - base.const_set(:Entity, Class.new(ancestor_entity_class || Grape::Entity)) unless const_defined?(:Entity) - end - - module ClassMethods - # Returns the automatically-created entity class for this - # Class. - def entity_class(search_ancestors=true) - klass = const_get(:Entity) if const_defined?(:Entity) - klass ||= ancestors.detect{|a| a.entity_class(false) if a.respond_to?(:entity_class) } if search_ancestors - klass - end - - # Call this to make exposures to the entity for this Class. - # Can be called with symbols for the attributes to expose, - # a block that yields the full Entity DSL (See Grape::Entity), - # or both. - # - # @example Symbols only. - # - # class User - # include Grape::Entity::DSL - # - # entity :name, :email - # end - # - # @example Mixed. - # - # class User - # include Grape::Entity::DSL - # - # entity :name, :email do - # expose :latest_status, using: Status::Entity, if: :include_status - # expose :new_attribute, :if => {:version => 'v2'} - # end - # end - def entity(*exposures, &block) - entity_class.expose *exposures if exposures.any? - entity_class.class_eval(&block) if block_given? - entity_class - end - end - - # Instantiates an entity version of this object. - def entity - self.class.entity_class.new(self) - end - end - - # This method is the primary means by which you will declare what attributes - # should be exposed by the entity. - # - # @option options :as Declare an alias for the representation of this attribute. - # @option options :if When passed a Hash, the attribute will only be exposed if the - # runtime options match all the conditions passed in. When passed a lambda, the - # lambda will execute with two arguments: the object being represented and the - # options passed into the representation call. Return true if you want the attribute - # to be exposed. - # @option options :unless When passed a Hash, the attribute will be exposed if the - # runtime options fail to match any of the conditions passed in. If passed a lambda, - # it will yield the object being represented and the options passed to the - # representation call. Return true to prevent exposure, false to allow it. - # @option options :using This option allows you to map an attribute to another Grape - # Entity. Pass it a Grape::Entity class and the attribute in question will - # automatically be transformed into a representation that will receive the same - # options as the parent entity when called. Note that arrays are fine here and - # will automatically be detected and handled appropriately. - # @option options :proc If you pass a Proc into this option, it will - # be used directly to determine the value for that attribute. It - # will be called with the represented object as well as the - # runtime options that were passed in. You can also just supply a - # block to the expose call to achieve the same effect. - # @option options :documentation Define documenation for an exposed - # field, typically the value is a hash with two fields, type and desc. - def self.expose(*args, &block) - options = args.last.is_a?(Hash) ? args.pop : {} - - if args.size > 1 - raise ArgumentError, "You may not use the :as option on multi-attribute exposures." if options[:as] - 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 format_with" if block_given? && options[:format_with].respond_to?(:call) - - options[:proc] = block if block_given? - - args.each do |attribute| - exposures[attribute.to_sym] = options - end - end - - # Returns a hash of exposures that have been declared for this Entity or ancestors. The keys - # are symbolized references to methods on the containing object, the values are - # the options that were passed into expose. - def self.exposures - @exposures ||= {} - - if superclass.respond_to? :exposures - @exposures = superclass.exposures.merge(@exposures) - end - - @exposures - end - - # Returns a hash, the keys are symbolized references to fields in the entity, - # the values are document keys in the entity's documentation key. When calling - # #docmentation, any exposure without a documentation key will be ignored. - def self.documentation - @documentation ||= exposures.inject({}) do |memo, value| - unless value[1][:documentation].nil? || value[1][:documentation].empty? - memo[value[0]] = value[1][:documentation] - end - memo - end - - if superclass.respond_to? :documentation - @documentation = superclass.documentation.merge(@documentation) - end - - @documentation - 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. - # - # @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 pass 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 - # a collection of objects. If missing or nil, no root key will be used - # when representing collections of objects. - # @param singular [String] the root key to use when representing - # a single object. If missing or nil, no root key will be used when - # representing an individual object. - # - # @example Entity Definition - # - # module API - # module Entities - # class User < Grape::Entity - # root 'users', 'user' - # expose :id - # end - # end - # end - # - # @example Usage in the API Layer - # - # module API - # class Users < Grape::API - # version 'v2' - # - # # this will render { "users": [ {"id":"1"}, {"id":"2"} ] } - # get '/users' do - # @users = User.all - # present @users, :with => API::Entities::User - # end - # - # # this will render { "user": {"id":"1"} } - # get '/users/:id' do - # @user = User.find(params[:id]) - # present @user, :with => API::Entities::User - # end - # end - # end - def self.root(plural, singular=nil) - @collection_root = plural - @root = singular - end - - # This convenience method allows you to instantiate one or more entities by - # passing either a singular or collection of objects. Each object will be - # initialized with the same options. If an array of objects is passed in, - # an array of entities will be returned. If a single object is passed in, - # a single entity will be returned. - # - # @param objects [Object or Array] One or more objects to be represented. - # @param options [Hash] Options that will be passed through to each entity - # representation. - # - # @option options :root [String] override the default root name set for the - #  entity. Pass nil or false to represent the object or objects with no - # root name even if one is defined for the entity. - def self.represent(objects, options = {}) - inner = if objects.respond_to?(:to_ary) - objects.to_ary().map{|o| self.new(o, {:collection => true}.merge(options))} - else - self.new(objects, options) - end - - root_element = if options.has_key?(:root) - options[:root] - else - objects.respond_to?(:to_ary) ? @collection_root : @root - end - root_element ? { root_element => inner } : inner - end - - def initialize(object, options = {}) - @object, @options = object, options - end - - def exposures - self.class.exposures - end - - def documentation - self.class.documentation - 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. - # - # @param runtime_options [Hash] Any options you pass in here will be known to the entity - # representation, this is where you can trigger things from conditional options - # etc. - def serializable_hash(runtime_options = {}) - return nil if object.nil? - opts = options.merge(runtime_options || {}) - exposures.inject({}) do |output, (attribute, exposure_options)| - if (exposure_options.has_key?(:proc) || object.respond_to?(attribute)) && conditions_met?(exposure_options, opts) - partial_output = value_for(attribute, opts) - output[key_for(attribute)] = - if partial_output.respond_to? :serializable_hash - partial_output.serializable_hash(runtime_options) - elsif partial_output.kind_of?(Array) && !partial_output.map {|o| o.respond_to? :serializable_hash}.include?(false) - partial_output.map {|o| o.serializable_hash} - else - partial_output - end - end - output - end - end - - alias :as_json :serializable_hash - - def to_json(options = {}) - options = options.to_h if options && options.respond_to?(:to_h) - MultiJson.dump(serializable_hash(options)) - end - - def to_xml(options = {}) - options = options.to_h if options && options.respond_to?(:to_h) - serializable_hash(options).to_xml(options) - end - - protected - - def key_for(attribute) - exposures[attribute.to_sym][:as] || attribute.to_sym - end - - def value_for(attribute, options = {}) - exposure_options = exposures[attribute.to_sym] - - if exposure_options[:proc] - exposure_options[:proc].call(object, options) - elsif exposure_options[:using] - using_options = options.dup - using_options.delete(:collection) - using_options[:root] = nil - exposure_options[:using].represent(object.send(attribute), using_options) - elsif exposure_options[:format_with] - format_with = exposure_options[:format_with] - - 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)) - end - else - object.send(attribute) - end - end - - def conditions_met?(exposure_options, options) - if_condition = exposure_options[:if] - unless_condition = exposure_options[:unless] - - case if_condition - when Hash; if_condition.each_pair{|k,v| return false if options[k.to_sym] != v } - when Proc; return false unless if_condition.call(object, options) - end - - case unless_condition - when Hash; unless_condition.each_pair{|k,v| return false if options[k.to_sym] == v} - when Proc; return false if unless_condition.call(object, options) - end - - true - end - end -end diff --git a/spec/grape/entity_spec.rb b/spec/grape/entity_spec.rb deleted file mode 100644 index 0daed86615..0000000000 --- a/spec/grape/entity_spec.rb +++ /dev/null @@ -1,579 +0,0 @@ -require 'spec_helper' - -describe Grape::Entity do - let(:fresh_class) { Class.new(Grape::Entity) } - - context 'class methods' do - subject { fresh_class } - - describe '.expose' do - context 'multiple attributes' do - it 'is able to add multiple exposed attributes with a single call' do - subject.expose :name, :email, :location - subject.exposures.size.should == 3 - end - - it 'sets the same options for all exposures passed' do - subject.expose :name, :email, :location, :foo => :bar - subject.exposures.values.each{|v| v.should == {:foo => :bar}} - end - end - - context 'option validation' do - it 'makes sure that :as only works on single attribute calls' do - expect{ subject.expose :name, :email, :as => :foo }.to raise_error(ArgumentError) - expect{ subject.expose :name, :as => :foo }.not_to raise_error - end - - it 'makes sure that :format_with as a proc can not be used with a block' do - expect { subject.expose :name, :format_with => Proc.new {} do |_| end }.to raise_error(ArgumentError) - end - end - - context 'with a block' do - it 'errors out if called with multiple attributes' do - expect{ subject.expose(:name, :email) do - true - end }.to raise_error(ArgumentError) - end - - it 'sets the :proc option in the exposure options' do - block = lambda{|_| true } - subject.expose :name, &block - subject.exposures[:name][:proc].should == block - end - end - - context 'inherited exposures' do - it 'returns exposures from an ancestor' do - subject.expose :name, :email - child_class = Class.new(subject) - - child_class.exposures.should eq(subject.exposures) - end - - it 'returns exposures from multiple ancestor' do - subject.expose :name, :email - parent_class = Class.new(subject) - child_class = Class.new(parent_class) - - child_class.exposures.should eq(subject.exposures) - end - - it 'returns descendant exposures as a priority' do - subject.expose :name, :email - child_class = Class.new(subject) - child_class.expose :name do |_| - 'foo' - end - - subject.exposures[:name].should_not have_key :proc - 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 'registers a formatter' do - subject.format_with :timestamp, &date_formatter - - subject.formatters[:timestamp].should_not be_nil - end - - it 'inherits formatters from ancestors' do - subject.format_with :timestamp, &date_formatter - child_class = Class.new(subject) - - child_class.formatters.should == subject.formatters - end - - it 'does not allow registering a formatter without a block' do - expect{ subject.format_with :foo }.to raise_error(ArgumentError) - end - - it 'formats 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.gm(2012, 2, 27) } - subject.new(mock(model)).as_json[:birthday].should == '02/27/2012' - end - end - end - - describe '.represent' do - it 'returns a single entity if called with one object' do - subject.represent(Object.new).should be_kind_of(subject) - end - - it 'returns a single entity if called with a hash' do - subject.represent(Hash.new).should be_kind_of(subject) - end - - it 'returns multiple entities if called with a collection' do - representation = subject.represent(4.times.map{Object.new}) - representation.should be_kind_of Array - representation.size.should == 4 - representation.reject{|r| r.kind_of?(subject)}.should be_empty - end - - it 'adds the :collection => true option if called with a collection' do - representation = subject.represent(4.times.map{Object.new}) - representation.each{|r| r.options[:collection].should be_true} - end - end - - describe '.root' do - context 'with singular and plural root keys' do - before(:each) do - subject.root 'things', 'thing' - end - - context 'with a single object' do - it 'allows a root element name to be specified' do - representation = subject.represent(Object.new) - representation.should be_kind_of Hash - representation.should have_key 'thing' - representation['thing'].should be_kind_of(subject) - end - end - - context 'with an array of objects' do - it 'allows a root element name to be specified' do - representation = subject.represent(4.times.map{Object.new}) - representation.should be_kind_of Hash - representation.should have_key 'things' - representation['things'].should be_kind_of Array - representation['things'].size.should == 4 - representation['things'].reject{|r| r.kind_of?(subject)}.should be_empty - end - end - - context 'it can be overridden' do - it 'can be disabled' do - representation = subject.represent(4.times.map{Object.new}, :root=>false) - representation.should be_kind_of Array - representation.size.should == 4 - representation.reject{|r| r.kind_of?(subject)}.should be_empty - end - it 'can use a different name' do - representation = subject.represent(4.times.map{Object.new}, :root=>'others') - representation.should be_kind_of Hash - representation.should have_key 'others' - representation['others'].should be_kind_of Array - representation['others'].size.should == 4 - representation['others'].reject{|r| r.kind_of?(subject)}.should be_empty - end - end - end - - context 'with singular root key' do - before(:each) do - subject.root nil, 'thing' - end - - context 'with a single object' do - it 'allows a root element name to be specified' do - representation = subject.represent(Object.new) - representation.should be_kind_of Hash - representation.should have_key 'thing' - representation['thing'].should be_kind_of(subject) - end - end - - context 'with an array of objects' do - it 'allows a root element name to be specified' do - representation = subject.represent(4.times.map{Object.new}) - representation.should be_kind_of Array - representation.size.should == 4 - representation.reject{|r| r.kind_of?(subject)}.should be_empty - end - end - end - - context 'with plural root key' do - before(:each) do - subject.root 'things' - end - - context 'with a single object' do - it 'allows a root element name to be specified' do - subject.represent(Object.new).should be_kind_of(subject) - end - end - - context 'with an array of objects' do - it 'allows a root element name to be specified' do - representation = subject.represent(4.times.map{Object.new}) - representation.should be_kind_of Hash - representation.should have_key('things') - representation['things'].should be_kind_of Array - representation['things'].size.should == 4 - representation['things'].reject{|r| r.kind_of?(subject)}.should be_empty - end - end - end - end - - describe '#initialize' do - it 'takes an object and an optional options hash' do - expect{ subject.new(Object.new) }.not_to raise_error - expect{ subject.new }.to raise_error(ArgumentError) - expect{ subject.new(Object.new, {}) }.not_to raise_error - end - - it 'has attribute readers for the object and options' do - entity = subject.new('abc', {}) - entity.object.should == 'abc' - entity.options.should == {} - end - end - end - - context 'instance methods' do - - let(:model){ mock(attributes) } - - let(:attributes) { { - :name => 'Bob Bobson', - :email => 'bob@example.com', - :birthday => Time.gm(2012, 2, 27), - :fantasies => ['Unicorns', 'Double Rainbows', 'Nessy'], - :friends => [ - mock(:name => "Friend 1", :email => 'friend1@example.com', :fantasies => [], :birthday => Time.gm(2012, 2, 27), :friends => []), - mock(:name => "Friend 2", :email => 'friend2@example.com', :fantasies => [], :birthday => Time.gm(2012, 2, 27), :friends => []) - ] - } } - - subject{ fresh_class.new(model) } - - describe '#serializable_hash' do - - it 'does not throw an exception if a nil options object is passed' do - expect{ fresh_class.new(model).serializable_hash(nil) }.not_to raise_error - end - - it 'does not blow up when the model is nil' do - fresh_class.expose :name - expect{ fresh_class.new(nil).serializable_hash }.not_to raise_error - end - - it 'does not throw an exception when an attribute is not found on the object' do - fresh_class.expose :name, :nonexistent_attribute - expect{ fresh_class.new(model).serializable_hash }.not_to raise_error - end - - it "does not expose attributes that don't exist on the object" do - fresh_class.expose :email, :nonexistent_attribute, :name - - res = fresh_class.new(model).serializable_hash - res.should have_key :email - res.should_not have_key :nonexistent_attribute - res.should have_key :name - end - - it "does not expose attributes that don't exist on the object, even with criteria" do - fresh_class.expose :email - fresh_class.expose :nonexistent_attribute, :if => lambda { false } - fresh_class.expose :nonexistent_attribute2, :if => lambda { true } - - res = fresh_class.new(model).serializable_hash - res.should have_key :email - res.should_not have_key :nonexistent_attribute - res.should_not have_key :nonexistent_attribute2 - end - - it "exposes attributes that don't exist on the object only when they are generated by a block" do - fresh_class.expose :nonexistent_attribute do |model, _| - "well, I do exist after all" - end - res = fresh_class.new(model).serializable_hash - res.should have_key :nonexistent_attribute - end - - it "does not expose attributes that are generated by a block but have not passed criteria" do - fresh_class.expose :nonexistent_attribute, :proc => lambda {|model, _| - "I exist, but it is not yet my time to shine" - }, :if => lambda { |model, _| false } - res = fresh_class.new(model).serializable_hash - res.should_not have_key :nonexistent_attribute - end - - context '#serializable_hash' do - - module EntitySpec - class EmbeddedExample - def serializable_hash(opts = {}) - { :abc => 'def' } - end - end - class EmbeddedExampleWithMany - def name - "abc" - end - def embedded - [ EmbeddedExample.new, EmbeddedExample.new ] - end - end - class EmbeddedExampleWithOne - def name - "abc" - end - def embedded - EmbeddedExample.new - end - end - end - - it 'serializes embedded objects which respond to #serializable_hash' do - fresh_class.expose :name, :embedded - presenter = fresh_class.new(EntitySpec::EmbeddedExampleWithOne.new) - presenter.serializable_hash.should == {:name => "abc", :embedded => {:abc => "def"}} - end - - it 'serializes embedded arrays of objects which respond to #serializable_hash' do - fresh_class.expose :name, :embedded - presenter = fresh_class.new(EntitySpec::EmbeddedExampleWithMany.new) - presenter.serializable_hash.should == {:name => "abc", :embedded => [{:abc => "def"}, {:abc => "def"}]} - end - - end - - end - - describe '#value_for' do - before do - fresh_class.class_eval do - expose :name, :email - expose :friends, :using => self - expose :computed do |_, options| - options[:awesome] - end - - expose :birthday, :format_with => :timestamp - - def timestamp(date) - date.strftime('%m/%d/%Y') - end - - expose :fantasies, :format_with => lambda {|f| f.reverse } - end - end - - it 'passes through bare expose attributes' do - subject.send(:value_for, :name).should == attributes[:name] - end - - it 'instantiates a representation if that is called for' do - rep = subject.send(:value_for, :friends) - rep.reject{|r| r.is_a?(fresh_class)}.should be_empty - rep.first.serializable_hash[:name].should == 'Friend 1' - rep.last.serializable_hash[:name].should == 'Friend 2' - end - - context 'child representations' do - it 'disables root key name for child representations' do - - module EntitySpec - class FriendEntity < Grape::Entity - root 'friends', 'friend' - expose :name, :email - end - end - - fresh_class.class_eval do - expose :friends, :using => EntitySpec::FriendEntity - end - - rep = subject.send(:value_for, :friends) - rep.should be_kind_of Array - rep.reject{|r| r.is_a?(EntitySpec::FriendEntity)}.should be_empty - rep.first.serializable_hash[:name].should == 'Friend 1' - rep.last.serializable_hash[:name].should == 'Friend 2' - end - - it 'passes through custom options' do - module EntitySpec - class FriendEntity < Grape::Entity - root 'friends', 'friend' - expose :name - expose :email, :if => { :user_type => :admin } - end - end - - fresh_class.class_eval do - expose :friends, :using => EntitySpec::FriendEntity - end - - rep = subject.send(:value_for, :friends) - rep.should be_kind_of Array - rep.reject{|r| r.is_a?(EntitySpec::FriendEntity)}.should be_empty - rep.first.serializable_hash[:email].should be_nil - rep.last.serializable_hash[:email].should be_nil - - rep = subject.send(:value_for, :friends, { :user_type => :admin }) - rep.should be_kind_of Array - rep.reject{|r| r.is_a?(EntitySpec::FriendEntity)}.should be_empty - rep.first.serializable_hash[:email].should == 'friend1@example.com' - rep.last.serializable_hash[:email].should == 'friend2@example.com' - end - - it 'ignores the :collection parameter in the source options' do - module EntitySpec - class FriendEntity < Grape::Entity - root 'friends', 'friend' - expose :name - expose :email, :if => { :collection => true } - end - end - - fresh_class.class_eval do - expose :friends, :using => EntitySpec::FriendEntity - end - - rep = subject.send(:value_for, :friends, { :collection => false }) - rep.should be_kind_of Array - rep.reject{|r| r.is_a?(EntitySpec::FriendEntity)}.should be_empty - rep.first.serializable_hash[:email].should == 'friend1@example.com' - rep.last.serializable_hash[:email].should == 'friend2@example.com' - end - - end - - it 'calls through to the proc if there is one' do - subject.send(:value_for, :computed, :awesome => 123).should == 123 - end - - it 'returns a formatted value if format_with is passed' do - subject.send(:value_for, :birthday).should == '02/27/2012' - end - - it 'returns a formatted value if format_with is passed a lambda' do - subject.send(:value_for, :fantasies).should == ['Nessy', 'Double Rainbows', 'Unicorns'] - end - end - - describe '#documentation' do - it 'returns an empty hash is no documentation is provided' do - fresh_class.expose :name - - subject.documentation.should == {} - end - - it 'returns each defined documentation hash' do - doc = {:type => "foo", :desc => "bar"} - fresh_class.expose :name, :documentation => doc - fresh_class.expose :email, :documentation => doc - fresh_class.expose :birthday - - subject.documentation.should == {:name => doc, :email => doc} - end - end - - describe '#key_for' do - it 'returns the attribute if no :as is set' do - fresh_class.expose :name - subject.send(:key_for, :name).should == :name - end - - it 'returns a symbolized version of the attribute' do - fresh_class.expose :name - subject.send(:key_for, 'name').should == :name - end - - it 'returns the :as alias if one exists' do - fresh_class.expose :name, :as => :nombre - subject.send(:key_for, 'name').should == :nombre - end - end - - describe '#conditions_met?' do - it 'only passes through hash :if exposure if all attributes match' do - exposure_options = {:if => {:condition1 => true, :condition2 => true}} - - subject.send(:conditions_met?, exposure_options, {}).should be_false - subject.send(:conditions_met?, exposure_options, :condition1 => true).should be_false - subject.send(:conditions_met?, exposure_options, :condition1 => true, :condition2 => true).should be_true - subject.send(:conditions_met?, exposure_options, :condition1 => false, :condition2 => true).should be_false - subject.send(:conditions_met?, exposure_options, :condition1 => true, :condition2 => true, :other => true).should be_true - end - - it 'only passes through proc :if exposure if it returns truthy value' do - exposure_options = {:if => lambda{|_,opts| opts[:true]}} - - subject.send(:conditions_met?, exposure_options, :true => false).should be_false - subject.send(:conditions_met?, exposure_options, :true => true).should be_true - end - - it 'only passes through hash :unless exposure if any attributes do not match' do - exposure_options = {:unless => {:condition1 => true, :condition2 => true}} - - subject.send(:conditions_met?, exposure_options, {}).should be_true - subject.send(:conditions_met?, exposure_options, :condition1 => true).should be_false - subject.send(:conditions_met?, exposure_options, :condition1 => true, :condition2 => true).should be_false - subject.send(:conditions_met?, exposure_options, :condition1 => false, :condition2 => true).should be_false - subject.send(:conditions_met?, exposure_options, :condition1 => true, :condition2 => true, :other => true).should be_false - subject.send(:conditions_met?, exposure_options, :condition1 => false, :condition2 => false).should be_true - end - - it 'only passes through proc :unless exposure if it returns falsy value' do - exposure_options = {:unless => lambda{|_,options| options[:true] == true}} - - subject.send(:conditions_met?, exposure_options, :true => false).should be_true - subject.send(:conditions_met?, exposure_options, :true => true).should be_false - end - end - - describe '::DSL' do - subject{ Class.new } - - it 'creates an Entity class when called' do - subject.should_not be_const_defined :Entity - subject.send(:include, Grape::Entity::DSL) - subject.should be_const_defined :Entity - end - - context 'pre-mixed' do - before{ subject.send(:include, Grape::Entity::DSL) } - - it 'is able to define entity traits through DSL' do - subject.entity do - expose :name - end - - subject.entity_class.exposures.should_not be_empty - end - - it 'is able to expose straight from the class' do - subject.entity :name, :email - subject.entity_class.exposures.size.should == 2 - end - - it 'is able to mix field and advanced exposures' do - subject.entity :name, :email do - expose :third - end - subject.entity_class.exposures.size.should == 3 - end - - context 'instance' do - let(:instance){ subject.new } - - describe '#entity' do - it 'is an instance of the entity class' do - instance.entity.should be_kind_of(subject.entity_class) - end - - it 'has an object of itself' do - instance.entity.object.should == instance - end - end - end - end - end - end -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 91d5f3abca..4afcff3743 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -5,6 +5,7 @@ # $stdout = StringIO.new require 'grape' +require 'grape-entity' require 'rubygems' require 'bundler'