Skip to content

Commit 89bb09e

Browse files
author
Jerry Cheung
committed
refactors API#settings into Grape::Util::HashStack
- settings.add replaced with settings.imbue
1 parent f182aa9 commit 89bb09e

File tree

6 files changed

+281
-47
lines changed

6 files changed

+281
-47
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
## MAC OS
22
.DS_Store
3+
.com.apple.timemachine.supported
34

45
## TEXTMATE
56
*.tmproj

lib/grape.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ module Auth
2222
autoload :Digest, 'grape/middleware/auth/digest'
2323
end
2424
end
25+
26+
module Util
27+
autoload :HashStack, 'grape/util/hash_stack'
28+
end
2529
end
2630

2731
require 'grape/version'

lib/grape/api.rb

Lines changed: 34 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class << self
1212
attr_reader :route_set
1313
attr_reader :versions
1414
attr_reader :routes
15+
attr_reader :settings
1516

1617
def logger(logger = nil)
1718
if logger
@@ -22,7 +23,7 @@ def logger(logger = nil)
2223
end
2324

2425
def reset!
25-
@settings = [{}]
26+
@settings = Grape::Util::HashStack.new
2627
@route_set = Rack::Mount::RouteSet.new
2728
@prototype = nil
2829
end
@@ -31,40 +32,22 @@ def call(env)
3132
logger.info "#{env['REQUEST_METHOD']} #{env['PATH_INFO']}"
3233
route_set.freeze.call(env)
3334
end
34-
35-
# Settings are a stack, so when we
36-
# want to access them they are merged
37-
# in the order pushed.
38-
def settings
39-
@settings.inject({}){|f,h| f.merge!(h); f}
40-
end
41-
42-
def settings_stack
43-
@settings
44-
end
45-
35+
4636
# Set a configuration value for this namespace.
4737
#
4838
# @param key [Symbol] The key of the configuration variable.
4939
# @param value [Object] The value to which to set the configuration variable.
5040
def set(key, value)
51-
@settings.last[key.to_sym] = value
41+
settings[key.to_sym] = value
5242
end
53-
43+
5444
# Add to a configuration value for this
5545
# namespace.
5646
#
5747
# @param key [Symbol] The key of the configuration variable.
5848
# @param value [Object] The value to which to set the configuration variable.
59-
def add(key, value)
60-
current = @settings.last[key.to_sym]
61-
if current.is_a?(Array)
62-
current.concat(value)
63-
elsif current.is_a?(Hash)
64-
current.merge!(value)
65-
else
66-
@settings.last[key.to_sym] = value
67-
end
49+
def imbue(key, value)
50+
settings.imbue(key, value)
6851
end
6952

7053
# Define a root URL prefix for your entire
@@ -137,12 +120,12 @@ def default_error_status(new_status = nil)
137120
def rescue_from(*args, &block)
138121
if block_given?
139122
args.each do |arg|
140-
add(:rescue_handlers, { arg => block })
123+
imbue(:rescue_handlers, { arg => block })
141124
end
142125
end
143-
add(:rescue_options, args.pop) if args.last.is_a?(Hash)
126+
imbue(:rescue_options, args.pop) if args.last.is_a?(Hash)
144127
set(:rescue_all, true) and return if args.include?(:all)
145-
add(:rescued_errors, args)
128+
imbue(:rescued_errors, args)
146129
end
147130

148131
# Allows you to specify a default representation entity for a
@@ -167,12 +150,15 @@ def rescue_from(*args, &block)
167150
# @option options [Class] :with The entity class that will represent the model.
168151
def represent(model_class, options)
169152
raise ArgumentError, "You must specify an entity class in the :with option." unless options[:with] && options[:with].is_a?(Class)
170-
add(:representations, model_class => options[:with])
153+
imbue(:representations, model_class => options[:with])
171154
end
172155

173156
# Add helper methods that will be accessible from any
174157
# endpoint within this namespace (and child namespaces).
175158
#
159+
# When called without a block, all known helpers within this scope
160+
# are included.
161+
#
176162
# @example Define some helpers.
177163
# class ExampleAPI < Grape::API
178164
# helpers do
@@ -183,12 +169,12 @@ def represent(model_class, options)
183169
# end
184170
def helpers(&block)
185171
if block_given?
186-
m = settings_stack.last[:helpers] || Module.new
172+
m = settings.peek[:helpers] || Module.new
187173
m.class_eval &block
188174
set(:helpers, m)
189175
else
190176
m = Module.new
191-
settings_stack.each do |s|
177+
settings.stack.each do |s|
192178
m.send :include, s[:helpers] if s[:helpers]
193179
end
194180
m
@@ -272,13 +258,11 @@ def route(methods, paths = ['/'], route_options = {}, &block)
272258
end
273259

274260
def before(&block)
275-
settings_stack.last[:befores] ||= []
276-
settings_stack.last[:befores] << block
261+
settings.imbue(:befores, [block])
277262
end
278263

279264
def after(&block)
280-
settings_stack.last[:afters] ||= []
281-
settings_stack.last[:afters] << block
265+
settings.imbue(:afters, [block])
282266
end
283267

284268
def get(paths = ['/'], options = {}, &block); route('GET', paths, options, &block) end
@@ -293,7 +277,7 @@ def namespace(space = nil, &block)
293277
set(:namespace, space.to_s) if space
294278
end
295279
else
296-
Rack::Mount::Utils.normalize_path(settings_stack.map{|s| s[:namespace]}.join('/'))
280+
Rack::Mount::Utils.normalize_path(settings.stack.map{|s| s[:namespace]}.join('/'))
297281
end
298282
end
299283

@@ -313,17 +297,17 @@ def scope(name = nil, &block)
313297
# to the current namespace and any children, but
314298
# not parents.
315299
#
316-
# @param middleware_class [Class] The class of the middleware you'd like to inject.
300+
# @param middleware_class [Class] The class of the middleware you'd like
301+
# to inject.
317302
def use(middleware_class, *args)
318-
settings_stack.last[:middleware] ||= []
319-
settings_stack.last[:middleware] << [middleware_class, *args]
303+
settings.imbue(:middleware, [[middleware_class, *args]])
320304
end
321305

322306
# Retrieve an array of the middleware classes
323307
# and arguments that are currently applied to the
324308
# application.
325309
def middleware
326-
settings_stack.inject([]){|a,s| a += s[:middleware] if s[:middleware]; a}
310+
settings.stack.inject([]){|a,s| a += s[:middleware] if s[:middleware]; a}
327311
end
328312

329313
# An array of API routes.
@@ -343,18 +327,18 @@ def versions
343327
def nest(*blocks, &block)
344328
blocks.reject!{|b| b.nil?}
345329
if blocks.any?
346-
settings_stack << {}
330+
settings.push # create a new context to eval the follow
347331
instance_eval &block if block_given?
348332
blocks.each{|b| instance_eval &b}
349-
settings_stack.pop
333+
settings.pop # when finished, we pop the context
350334
else
351335
instance_eval &block
352336
end
353337
end
354338

355339
def aggregate_setting(key)
356-
settings_stack.inject([]) do |befores, settings|
357-
befores += (settings[key] || [])
340+
settings.stack.inject([]) do |aggregate, frame|
341+
aggregate += (frame[key] || [])
358342
end
359343
end
360344

@@ -392,7 +376,13 @@ def build_endpoint(&block)
392376
def inherited(subclass)
393377
subclass.reset!
394378
end
395-
379+
380+
def inherit(other_stack)
381+
# settings stack should know how to merge aggregate keys / values
382+
# settings_stack.unshift *other_stack
383+
# raise settings_stack.inspect
384+
end
385+
396386
def route_set
397387
@route_set ||= Rack::Mount::RouteSet.new
398388
end

lib/grape/util/hash_stack.rb

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
module Grape
2+
module Util
3+
# HashStack is a stack of hashes. When retrieving a value, keys of the top
4+
# hash on the stack take precendent over the lower keys.
5+
class HashStack
6+
# Unmerged array of hashes to represent the stack.
7+
# The top of the stack is the last element.
8+
attr_reader :stack
9+
10+
# TODO: handle aggregates
11+
def initialize
12+
@stack = [{}]
13+
end
14+
15+
# Returns the top hash on the stack
16+
def peek
17+
@stack.last
18+
end
19+
20+
# Add a new hash to the top of the stack.
21+
#
22+
# @param hash [Hash] optional hash to be pushed. Defaults to empty hash
23+
# @return [HashStack]
24+
def push(hash = {})
25+
@stack.push(hash)
26+
self
27+
end
28+
29+
def pop
30+
@stack.pop
31+
end
32+
33+
# Looks through the stack for the first frame that matches :key
34+
#
35+
# @param key [Symbol] key to look for in hash frames
36+
# @return value of given key after merging the stack
37+
def get(key)
38+
(@stack.length - 1).downto(0).each do |i|
39+
return @stack[i][key] if @stack[i].key? key
40+
end
41+
nil
42+
end
43+
alias_method :[], :get
44+
45+
# Replace a value on the top hash of the stack.
46+
#
47+
# @param key [Symbol] The key to set.
48+
# @param value [Object] The value to set.
49+
def set(key, value)
50+
peek[key.to_sym] = value
51+
end
52+
alias_method :[]=, :set
53+
54+
# Replace multiple values on the top hash of the stack.
55+
#
56+
# @param hash [Hash] Hash of values to be merged in.
57+
def update(hash)
58+
peek.merge!(hash)
59+
self
60+
end
61+
62+
# Adds addition value into the top hash of the stack
63+
def imbue(key, value)
64+
current = peek[key.to_sym]
65+
if current.is_a?(Array)
66+
current.concat(value)
67+
elsif current.is_a?(Hash)
68+
current.merge!(value)
69+
else
70+
set(key, value)
71+
end
72+
end
73+
74+
# Prepend another HashStack's to self
75+
def prepend(hash_stack)
76+
@stack.unshift *hash_stack.stack
77+
self
78+
end
79+
80+
# Concatenate another HashStack's to self
81+
def concat(hash_stack)
82+
@stack.concat hash_stack.stack
83+
self
84+
end
85+
end
86+
end
87+
end

spec/grape/api_spec.rb

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,32 @@ def app; subject end
308308
end
309309
end
310310

311+
describe 'filters' do
312+
it 'should add a before filter' do
313+
subject.before { @foo = 'first' }
314+
subject.before { @bar = 'second' }
315+
subject.get '/' do
316+
"#{@foo} #{@bar}"
317+
end
318+
319+
get '/'
320+
last_response.body.should eql 'first second'
321+
end
322+
323+
it 'should add a after filter' do
324+
m = double('after mock')
325+
subject.after { m.do_something! }
326+
subject.after { m.do_something! }
327+
subject.get '/' do
328+
@var ||= 'default'
329+
end
330+
331+
m.should_receive(:do_something!).exactly(2).times
332+
get '/'
333+
last_response.body.should eql 'default'
334+
end
335+
end
336+
311337
context 'format' do
312338
before do
313339
subject.get("/foo") { "bar" }
@@ -353,15 +379,19 @@ def call(env)
353379

354380
describe '.middleware' do
355381
it 'should include middleware arguments from settings' do
356-
subject.stub!(:settings_stack).and_return [{:middleware => [[PhonyMiddleware, 'abc', 123]]}]
382+
settings = Grape::Util::HashStack.new
383+
settings.stub!(:stack).and_return([{:middleware => [[PhonyMiddleware, 'abc', 123]]}])
384+
subject.stub!(:settings).and_return(settings)
357385
subject.middleware.should eql [[PhonyMiddleware, 'abc', 123]]
358386
end
359387

360388
it 'should include all middleware from stacked settings' do
361-
subject.stub!(:settings_stack).and_return [
389+
settings = Grape::Util::HashStack.new
390+
settings.stub!(:stack).and_return [
362391
{:middleware => [[PhonyMiddleware, 123],[PhonyMiddleware, 'abc']]},
363392
{:middleware => [[PhonyMiddleware, 'foo']]}
364393
]
394+
subject.stub!(:settings).and_return(settings)
365395

366396
subject.middleware.should eql [
367397
[PhonyMiddleware, 123],
@@ -788,5 +818,4 @@ class CommunicationError < RuntimeError; end
788818
lambda { get '/uncaught' }.should raise_error(CommunicationError)
789819
end
790820
end
791-
792821
end

0 commit comments

Comments
 (0)