Skip to content

Commit 11fdb3d

Browse files
ericproulxclaude
andcommitted
Extract Entity DSL and refactor :with to keyword argument
- Move entity-related methods (present, entity_class_for_obj, entity_representation_for) from InsideRoute into a new Grape::DSL::Entity module - Convert options[:with] hash pattern to explicit `with:` keyword argument in error_formatter, rescue_from, represent, and error_formatter/base#present Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3ad8ad9 commit 11fdb3d

8 files changed

Lines changed: 107 additions & 148 deletions

File tree

lib/grape/dsl/entity.rb

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# frozen_string_literal: true
2+
3+
module Grape
4+
module DSL
5+
module Entity
6+
# Allows you to make use of Grape Entities by setting
7+
# the response body to the serializable hash of the
8+
# entity provided in the `:with` option. This has the
9+
# added benefit of automatically passing along environment
10+
# and version information to the serialization, making it
11+
# very easy to do conditional exposures. See Entity docs
12+
# for more info.
13+
#
14+
# @param args [Array] either `(object)` or `(key, object)` where key is a Symbol
15+
# used to nest the representation under that key in the response body.
16+
# @param root [Symbol, String, nil] wraps the representation under this root key.
17+
# @param with [Class, nil] the entity class to use for representation.
18+
# If omitted, the entity class is inferred from the object via {#entity_class_for_obj}.
19+
# @param options [Hash] additional options forwarded to the entity's `represent` call.
20+
#
21+
# @example
22+
#
23+
# get '/users/:id' do
24+
# present User.find(params[:id]),
25+
# with: API::Entities::User,
26+
# admin: current_user.admin?
27+
# end
28+
def present(*args, root: nil, with: nil, **options)
29+
key, object = args.count == 2 && args.first.is_a?(Symbol) ? args : [nil, args.first]
30+
entity_class = with || entity_class_for_obj(object)
31+
representation = entity_class ? entity_representation_for(entity_class, object, options) : object
32+
representation = { root => representation } if root
33+
34+
if key
35+
representation = (body || {}).merge(key => representation)
36+
elsif entity_class.present? && body
37+
raise ArgumentError, "Representation of type #{representation.class} cannot be merged." unless representation.respond_to?(:merge)
38+
39+
representation = body.merge(representation)
40+
end
41+
42+
body representation
43+
end
44+
45+
# Attempt to locate the Entity class for a given object, if not given
46+
# explicitly. This is done by looking for the presence of Klass::Entity,
47+
# where Klass is the class of the `object` parameter, or one of its
48+
# ancestors.
49+
# @param object [Object] the object to locate the Entity class for
50+
# @return [Class] the located Entity class, or nil if none is found
51+
def entity_class_for_obj(object)
52+
object_class =
53+
if object.respond_to?(:klass)
54+
object.klass
55+
elsif object.respond_to?(:first)
56+
object.first.class
57+
else
58+
object.class
59+
end
60+
61+
representations = inheritable_setting.namespace_stackable_with_hash(:representations)
62+
if representations
63+
potential = object_class.ancestors.detect { |potential| representations.key?(potential) }
64+
return representations[potential] if potential && representations[potential]
65+
end
66+
67+
return unless object_class.const_defined?(:Entity)
68+
69+
entity = object_class.const_get(:Entity)
70+
entity if entity.respond_to?(:represent)
71+
end
72+
73+
private
74+
75+
# @param entity_class [Class] the entity class to use for representation.
76+
# @param object [Object] the object to represent.
77+
# @param options [Hash] additional options forwarded to the entity's `represent` call.
78+
# @return the representation of the given object as done through the given entity_class.
79+
def entity_representation_for(entity_class, object, options)
80+
embeds = env.key?(Grape::Env::API_VERSION) ? { env:, version: env[Grape::Env::API_VERSION] } : { env: }
81+
entity_class.represent(object, **embeds, **options)
82+
end
83+
end
84+
end
85+
end

lib/grape/dsl/inside_route.rb

Lines changed: 1 addition & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ module Grape
44
module DSL
55
module InsideRoute
66
include Declared
7+
include Entity
78

89
# Backward compatibility: alias exception class to previous location
910
MethodNotYetAvailable = Declared::MethodNotYetAvailable
@@ -184,50 +185,6 @@ def stream(value = nil)
184185
end
185186
end
186187

187-
# Allows you to make use of Grape Entities by setting
188-
# the response body to the serializable hash of the
189-
# entity provided in the `:with` option. This has the
190-
# added benefit of automatically passing along environment
191-
# and version information to the serialization, making it
192-
# very easy to do conditional exposures. See Entity docs
193-
# for more info.
194-
#
195-
# @example
196-
#
197-
# get '/users/:id' do
198-
# present User.find(params[:id]),
199-
# with: API::Entities::User,
200-
# admin: current_user.admin?
201-
# end
202-
def present(*args, **options)
203-
key, object = if args.count == 2 && args.first.is_a?(Symbol)
204-
args
205-
else
206-
[nil, args.first]
207-
end
208-
entity_class = entity_class_for_obj(object, options)
209-
210-
root = options.delete(:root)
211-
212-
representation = if entity_class
213-
entity_representation_for(entity_class, object, options)
214-
else
215-
object
216-
end
217-
218-
representation = { root => representation } if root
219-
220-
if key
221-
representation = (body || {}).merge(key => representation)
222-
elsif entity_class.present? && body
223-
raise ArgumentError, "Representation of type #{representation.class} cannot be merged." unless representation.respond_to?(:merge)
224-
225-
representation = body.merge(representation)
226-
end
227-
228-
body representation
229-
end
230-
231188
# Returns route information for the current request.
232189
#
233190
# @example
@@ -240,43 +197,6 @@ def route
240197
env[Grape::Env::GRAPE_ROUTING_ARGS][:route_info]
241198
end
242199

243-
# Attempt to locate the Entity class for a given object, if not given
244-
# explicitly. This is done by looking for the presence of Klass::Entity,
245-
# where Klass is the class of the `object` parameter, or one of its
246-
# ancestors.
247-
# @param object [Object] the object to locate the Entity class for
248-
# @param options [Hash]
249-
# @option options :with [Class] the explicit entity class to use
250-
# @return [Class] the located Entity class, or nil if none is found
251-
def entity_class_for_obj(object, options)
252-
entity_class = options.delete(:with)
253-
return entity_class if entity_class
254-
255-
# entity class not explicitly defined, auto-detect from relation#klass or first object in the collection
256-
object_class = if object.respond_to?(:klass)
257-
object.klass
258-
else
259-
object.respond_to?(:first) ? object.first.class : object.class
260-
end
261-
262-
representations = inheritable_setting.namespace_stackable_with_hash(:representations)
263-
if representations
264-
potential = object_class.ancestors.detect { |potential| representations.key?(potential) }
265-
entity_class = representations[potential] if potential
266-
end
267-
268-
entity_class = object_class.const_get(:Entity) if !entity_class && object_class.const_defined?(:Entity) && object_class.const_get(:Entity).respond_to?(:represent)
269-
entity_class
270-
end
271-
272-
# @return the representation of the given object as done through
273-
# the given entity_class.
274-
def entity_representation_for(entity_class, object, options)
275-
embeds = { env: }
276-
embeds[:version] = env[Grape::Env::API_VERSION] if env.key?(Grape::Env::API_VERSION)
277-
entity_class.represent(object, **embeds, **options)
278-
end
279-
280200
def http_version
281201
env.fetch('HTTP_VERSION') { env[Rack::SERVER_PROTOCOL] }
282202
end

lib/grape/dsl/request_response.rb

Lines changed: 14 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,8 @@ def default_error_formatter(new_formatter_name = nil)
4444
inheritable_setting.namespace_inheritable[:default_error_formatter] = new_formatter
4545
end
4646

47-
def error_formatter(format, options)
48-
formatter = if options.is_a?(Hash) && options.key?(:with)
49-
options[:with]
50-
else
51-
options
52-
end
53-
47+
def error_formatter(format, options = nil, with: nil)
48+
formatter = with || options
5449
inheritable_setting.namespace_stackable[:error_formatters] = { format.to_sym => formatter }
5550
end
5651

@@ -93,16 +88,8 @@ def default_error_status(new_status = nil)
9388
# @option options [Boolean] :rescue_subclasses Also rescue subclasses of exception classes
9489
# @param [Proc] handler Execution proc to handle the given exception as an
9590
# alternative to passing a block.
96-
def rescue_from(*args, **options, &block)
97-
if args.last.is_a?(Proc)
98-
handler = args.pop
99-
elsif block
100-
handler = block
101-
end
102-
103-
raise ArgumentError, 'both :with option and block cannot be passed' if block && options.key?(:with)
104-
105-
handler ||= extract_with(options)
91+
def rescue_from(*args, with: nil, **options, &block)
92+
handler = extract_handler(args, with:, block:)
10693

10794
if args.include?(:all)
10895
inheritable_setting.namespace_inheritable[:rescue_all] = true
@@ -146,22 +133,23 @@ def rescue_from(*args, **options, &block)
146133
#
147134
# @param model_class [Class] The model class that will be represented.
148135
# @option options [Class] :with The entity class that will represent the model.
149-
def represent(model_class, options)
150-
raise Grape::Exceptions::InvalidWithOptionForRepresent.new unless options[:with].is_a?(Class)
136+
def represent(model_class, with:)
137+
raise Grape::Exceptions::InvalidWithOptionForRepresent.new unless with.is_a?(Class)
151138

152-
inheritable_setting.namespace_stackable[:representations] = { model_class => options[:with] }
139+
inheritable_setting.namespace_stackable[:representations] = { model_class => with }
153140
end
154141

155142
private
156143

157-
def extract_with(options)
158-
return unless options.key?(:with)
144+
def extract_handler(args, with:, block:)
145+
raise ArgumentError, 'both :with option and block cannot be passed' if block && with
159146

160-
with_option = options.delete(:with)
161-
return with_option if with_option.instance_of?(Proc)
162-
return with_option.to_sym if with_option.instance_of?(Symbol) || with_option.instance_of?(String)
147+
return args.pop if args.last.is_a?(Proc)
148+
return block if block
149+
return with if with.instance_of?(Proc) || with.instance_of?(Symbol)
150+
return with.to_sym if with.instance_of?(String)
163151

164-
raise ArgumentError, "with: #{with_option.class}, expected Symbol, String or Proc"
152+
raise ArgumentError, "with: #{with.class}, expected Symbol, String or Proc" if with
165153
end
166154
end
167155
end

lib/grape/error_formatter/base.rb

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,35 +17,25 @@ def call(message, backtrace, options = {}, env = nil, original_exception = nil)
1717
format_structured_message(wrapped_message)
1818
end
1919

20-
def present(message, env)
21-
present_options = {}
22-
presented_message = message
23-
if presented_message.is_a?(Hash)
24-
presented_message = presented_message.dup
25-
present_options[:with] = presented_message.delete(:with)
26-
end
27-
28-
presenter = env[Grape::Env::API_ENDPOINT].entity_class_for_obj(presented_message, present_options)
20+
def present(message, env, with: nil)
21+
presenter = with || env[Grape::Env::API_ENDPOINT].entity_class_for_obj(message)
2922

3023
unless presenter || env[Grape::Env::GRAPE_ROUTING_ARGS].nil?
3124
# env['api.endpoint'].route does not work when the error occurs within a middleware
3225
# the Endpoint does not have a valid env at this moment
3326
http_codes = env[Grape::Env::GRAPE_ROUTING_ARGS][:route_info].http_codes || []
34-
3527
found_code = http_codes.find do |http_code|
3628
(http_code[0].to_i == env[Grape::Env::API_ENDPOINT].status) && http_code[2].respond_to?(:represent)
3729
end if env[Grape::Env::API_ENDPOINT].request
3830

3931
presenter = found_code[2] if found_code
4032
end
4133

42-
if presenter
43-
embeds = { env: }
44-
embeds[:version] = env[Grape::Env::API_VERSION] if env.key?(Grape::Env::API_VERSION)
45-
presented_message = presenter.represent(presented_message, embeds).serializable_hash
46-
end
34+
return message unless presenter
4735

48-
presented_message
36+
embeds = { env: }
37+
embeds[:version] = env[Grape::Env::API_VERSION] if env.key?(Grape::Env::API_VERSION)
38+
presenter.represent(message, embeds).serializable_hash
4939
end
5040

5141
def wrap_message(message)

lib/grape/validations/params_scope.rb

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -492,13 +492,6 @@ def validate_value_coercion(coerce_type, *values_list)
492492
end
493493
end
494494

495-
def extract_message_option(attrs)
496-
return nil unless attrs.is_a?(Array)
497-
498-
opts = attrs.last.is_a?(Hash) ? attrs.pop : {}
499-
opts.key?(:message) && !opts[:message].nil? ? opts.delete(:message) : nil
500-
end
501-
502495
def options_key?(type, key, validations)
503496
validations[type].respond_to?(:key?) && validations[type].key?(key) && !validations[type][key].nil?
504497
end

spec/grape/api_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@
112112

113113
describe '.represent' do
114114
it 'requires a :with option' do
115-
expect { subject.represent Object, {} }.to raise_error(Grape::Exceptions::InvalidWithOptionForRepresent)
115+
expect { subject.represent Object, with: 1 }.to raise_error(Grape::Exceptions::InvalidWithOptionForRepresent)
116116
end
117117

118118
it 'adds the association to the :representations setting' do

spec/grape/dsl/parameters_spec.rb

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,6 @@ def new_group_scope(group)
4949
yield
5050
@group = prev_group
5151
end
52-
53-
def extract_message_option(attrs)
54-
return nil unless attrs.is_a?(Array)
55-
56-
opts = attrs.last.is_a?(Hash) ? attrs.pop : {}
57-
opts.key?(:message) && !opts[:message].nil? ? opts.delete(:message) : nil
58-
end
5952
end
6053
end
6154

spec/grape/middleware/error_spec.rb

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,6 @@
11
# frozen_string_literal: true
22

33
describe Grape::Middleware::Error do
4-
let(:error_entity) do
5-
Class.new(Grape::Entity) do
6-
expose :code
7-
expose :static
8-
9-
def static
10-
'static text'
11-
end
12-
end
13-
end
144
let(:err_app) do
155
Class.new do
166
class << self

0 commit comments

Comments
 (0)