Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

:only/:except options for expose #353

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions lib/grape_entity/entity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -187,15 +187,12 @@ def self.inherited(subclass)
# field, typically the value is a hash with two fields, type and desc.
# @option options :merge This option allows you to merge an exposed field to the root
#
# rubocop:disable Layout/LineLength
def self.expose(*args, &block)
options = merge_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 the :expose_nil on multi-attribute exposures.' if options.key?(:expose_nil)
raise ArgumentError, 'You may not use block-setting on multi-attribute exposures.' if block_given?
ensure_multi_attrs_options_valid!(args, options)
if (options.key?(:only) || options.key?(:except)) && !options.key?(:using)
raise ArgumentError, 'You cannot use the :only/:except without :using.'
end

if block_given?
Expand All @@ -214,7 +211,15 @@ def self.expose(*args, &block)
@nesting_stack ||= []
args.each { |attribute| build_exposure_for_attribute(attribute, @nesting_stack, options, block) }
end
# rubocop:enable Layout/LineLength

def self.ensure_multi_attrs_options_valid!(args, options)
return if args.size < 2
raise ArgumentError, 'You may not use the :as option on multi-attribute exposures.' if options[:as]
raise ArgumentError, 'You may not use the :expose_nil on multi-attribute exposures.' if options.key?(:expose_nil)
raise ArgumentError, 'You may not use the :only on multi-attribute exposures.' if options.key?(:only)
raise ArgumentError, 'You may not use the :except on multi-attribute exposures.' if options.key?(:except)
raise ArgumentError, 'You may not use block-setting on multi-attribute exposures.' if block_given?
end

def self.build_exposure_for_attribute(attribute, nesting_stack, options, block)
exposure_list = nesting_stack.empty? ? root_exposures : nesting_stack.last.nested_exposures
Expand Down Expand Up @@ -585,6 +590,8 @@ def to_xml(options = {})
merge
expose_nil
override
only
except
].to_set.freeze

# Merges the given options with current block options.
Expand Down
4 changes: 3 additions & 1 deletion lib/grape_entity/exposure/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module Grape
class Entity
module Exposure
class Base
attr_reader :attribute, :is_safe, :documentation, :override, :conditions, :for_merge
attr_reader :attribute, :is_safe, :documentation, :override, :conditions, :for_merge, :only, :except

def self.new(attribute, options, conditions, *args, &block)
super(attribute, options, conditions).tap { |e| e.setup(*args, &block) }
Expand All @@ -20,6 +20,8 @@ def initialize(attribute, options, conditions)
@attr_path_proc = options[:attr_path]
@documentation = options[:documentation]
@override = options[:override]
@only = options[:only]
@except = options[:except]
@conditions = conditions
end

Expand Down
2 changes: 1 addition & 1 deletion lib/grape_entity/exposure/represent_exposure.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def ==(other)
end

def value(entity, options)
new_options = options.for_nesting(key(entity))
new_options = options.for_nesting(key(entity)).with_expose(self)
using_class.represent(@subexposure.value(entity, options), new_options)
end

Expand Down
13 changes: 13 additions & 0 deletions lib/grape_entity/options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,19 @@ def merge(new_opts)
Options.new(merged)
end

def with_expose(expose)
opts_only_ary = Array(self[:only])
opts_except_ary = Array(self[:except])

only_ary = Array(expose.only)
except_ary = Array(expose.except)

merge(
only: opts_only_ary.any? || only_ary.any? ? opts_only_ary | only_ary : nil,
except: opts_except_ary.any? || except_ary.any? ? opts_except_ary | except_ary : nil
)
end

def reverse_merge(new_opts)
return self if new_opts.empty?

Expand Down
50 changes: 50 additions & 0 deletions spec/grape_entity/entity_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,28 @@
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
end
it do
expect { subject.expose :name, as: :foo }.not_to raise_error
end
it do
expect { subject.expose :name, :email, only: [:name], using: 'SomeEntity' }.to raise_error ArgumentError
end
it do
expect { subject.expose :name, only: [:name], using: 'SomeEntity' }.not_to raise_error
end
it do
expect { subject.expose :name, :email, except: [:name], using: 'SomeEntity' }.to raise_error ArgumentError
end
it do
expect { subject.expose :name, except: [:name], using: 'SomeEntity' }.not_to raise_error
end
it do
expect { subject.expose :name, only: [:name] }.to raise_error ArgumentError
end
it do
expect { subject.expose :name, except: [:name] }.to raise_error ArgumentError
end

it 'makes sure that :format_with as a proc cannot be used with a block' do
# rubocop:disable Style/BlockDelimiters
Expand Down Expand Up @@ -1680,6 +1700,36 @@ class FriendEntity < Grape::Entity
expect(rep.last.serializable_hash[:name]).to eq 'Friend 2'
end

it 'exposes only the specified fields' do
module EntitySpec
class FriendEntity < Grape::Entity
expose :name, :email
end
end

fresh_class.class_eval do
expose :friends, using: EntitySpec::FriendEntity, only: [:name]
end
rep = subject.value_for(:friends)
expect(rep.first.serializable_hash).to eq(name: 'Friend 1')
expect(rep.last.serializable_hash).to eq(name: 'Friend 2')
end

it 'exposes except the specified fields' do
module EntitySpec
class FriendEntity < Grape::Entity
expose :name, :email
end
end

fresh_class.class_eval do
expose :friends, using: EntitySpec::FriendEntity, except: [:email]
end
rep = subject.value_for(:friends)
expect(rep.first.serializable_hash).to eq(name: 'Friend 1')
expect(rep.last.serializable_hash).to eq(name: 'Friend 2')
end

it 'passes through the proc which returns an array of objects with custom options(:using)' do
module EntitySpec
class FriendEntity < Grape::Entity
Expand Down