From 356bfe2e45eeae90e716dac1669505b1b7bb53cc Mon Sep 17 00:00:00 2001 From: Andrey Koleshko Date: Tue, 22 Jun 2021 15:10:36 +0200 Subject: [PATCH 1/2] :only/:except options for expose --- lib/grape_entity/entity.rb | 9 +++- lib/grape_entity/exposure/base.rb | 4 +- .../exposure/represent_exposure.rb | 2 +- lib/grape_entity/options.rb | 13 +++++ spec/grape_entity/entity_spec.rb | 50 +++++++++++++++++++ 5 files changed, 75 insertions(+), 3 deletions(-) diff --git a/lib/grape_entity/entity.rb b/lib/grape_entity/entity.rb index 9324e3ee..edc73910 100644 --- a/lib/grape_entity/entity.rb +++ b/lib/grape_entity/entity.rb @@ -192,12 +192,17 @@ 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 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 + if (options.key?(:only) || options.key?(:except)) && !options.key?(:using) + raise ArgumentError, 'You cannot use the :only/:except without :using.' + end + if block_given? if options[:format_with].respond_to?(:call) raise ArgumentError, 'You may not use block-setting when also using format_with' @@ -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..8d7c05df 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 8121fd5b..518e47d7 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 @@ -1670,6 +1690,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 From 05d8be59359214f0cccab95457532aba84ee4e4d Mon Sep 17 00:00:00 2001 From: Andrey Koleshko Date: Tue, 22 Jun 2021 15:35:35 +0200 Subject: [PATCH 2/2] fix rubocop issues --- lib/grape_entity/entity.rb | 20 ++++++++++---------- lib/grape_entity/options.rb | 4 ++-- spec/grape_entity/entity_spec.rb | 4 +--- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/lib/grape_entity/entity.rb b/lib/grape_entity/entity.rb index edc73910..67ef9e8d 100644 --- a/lib/grape_entity/entity.rb +++ b/lib/grape_entity/entity.rb @@ -187,18 +187,10 @@ 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 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 - + 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 @@ -219,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 diff --git a/lib/grape_entity/options.rb b/lib/grape_entity/options.rb index 8d7c05df..861e6fc9 100644 --- a/lib/grape_entity/options.rb +++ b/lib/grape_entity/options.rb @@ -39,8 +39,8 @@ def with_expose(expose) 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 + 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 diff --git a/spec/grape_entity/entity_spec.rb b/spec/grape_entity/entity_spec.rb index 518e47d7..d7ed5723 100644 --- a/spec/grape_entity/entity_spec.rb +++ b/spec/grape_entity/entity_spec.rb @@ -50,9 +50,7 @@ it 'makes sure that :format_with as a proc cannot be used with a block' do # rubocop:disable Style/BlockDelimiters - # rubocop:disable Lint/EmptyBlock expect { subject.expose :name, format_with: proc {} do p 'hi' end }.to raise_error ArgumentError - # rubocop:enable Lint/EmptyBlock # rubocop:enable Style/BlockDelimiters end @@ -1601,7 +1599,7 @@ class NoPathCharacterEntity < Grape::Entity end fresh_class.class_eval do - expose :characteristics, using: EntitySpec::NoPathCharacterEntity, attr_path: proc { nil } + expose :characteristics, using: EntitySpec::NoPathCharacterEntity, attr_path: proc {} end expect(subject.serializable_hash).to eq(