diff --git a/lib/grape_entity/entity.rb b/lib/grape_entity/entity.rb index 7b1de79a..b1353c68 100644 --- a/lib/grape_entity/entity.rb +++ b/lib/grape_entity/entity.rb @@ -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? @@ -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 @@ -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. diff --git a/lib/grape_entity/exposure/base.rb b/lib/grape_entity/exposure/base.rb index 18f7ff0e..c7a452e5 100644 --- a/lib/grape_entity/exposure/base.rb +++ b/lib/grape_entity/exposure/base.rb @@ -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) } @@ -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 diff --git a/lib/grape_entity/exposure/represent_exposure.rb b/lib/grape_entity/exposure/represent_exposure.rb index b63aae27..4a5a1b57 100644 --- a/lib/grape_entity/exposure/represent_exposure.rb +++ b/lib/grape_entity/exposure/represent_exposure.rb @@ -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 diff --git a/lib/grape_entity/options.rb b/lib/grape_entity/options.rb index 6487a956..861e6fc9 100644 --- a/lib/grape_entity/options.rb +++ b/lib/grape_entity/options.rb @@ -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? diff --git a/spec/grape_entity/entity_spec.rb b/spec/grape_entity/entity_spec.rb index 026b05b5..dc0cc135 100644 --- a/spec/grape_entity/entity_spec.rb +++ b/spec/grape_entity/entity_spec.rb @@ -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 @@ -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