grape 0.1.5 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of grape might be problematic. Click here for more details.

Files changed (142) hide show
  1. data/.gitignore +13 -1
  2. data/.rspec +1 -1
  3. data/.travis.yml +1 -0
  4. data/Gemfile +10 -0
  5. data/Guardfile +15 -0
  6. data/README.markdown +472 -67
  7. data/grape.gemspec +4 -4
  8. data/lib/grape.rb +17 -5
  9. data/lib/grape/api.rb +227 -124
  10. data/lib/grape/cookies.rb +41 -0
  11. data/lib/grape/endpoint.rb +256 -27
  12. data/lib/grape/entity.rb +227 -0
  13. data/lib/grape/middleware/auth/oauth2.rb +1 -2
  14. data/lib/grape/middleware/base.rb +59 -6
  15. data/lib/grape/middleware/error.rb +15 -6
  16. data/lib/grape/middleware/filter.rb +17 -0
  17. data/lib/grape/middleware/formatter.rb +25 -31
  18. data/lib/grape/middleware/versioner.rb +20 -20
  19. data/lib/grape/middleware/versioner/header.rb +59 -0
  20. data/lib/grape/middleware/versioner/path.rb +42 -0
  21. data/lib/grape/route.rb +23 -0
  22. data/lib/grape/util/hash_stack.rb +100 -0
  23. data/lib/grape/version.rb +1 -1
  24. data/spec/grape/api_spec.rb +651 -162
  25. data/spec/grape/endpoint_spec.rb +216 -18
  26. data/spec/grape/entity_spec.rb +320 -0
  27. data/spec/grape/middleware/auth/basic_spec.rb +2 -2
  28. data/spec/grape/middleware/auth/digest_spec.rb +4 -6
  29. data/spec/grape/middleware/exception_spec.rb +1 -0
  30. data/spec/grape/middleware/formatter_spec.rb +81 -27
  31. data/spec/grape/middleware/versioner/header_spec.rb +148 -0
  32. data/spec/grape/middleware/versioner/path_spec.rb +40 -0
  33. data/spec/grape/middleware/versioner_spec.rb +6 -34
  34. data/spec/grape/util/hash_stack_spec.rb +133 -0
  35. data/spec/shared/versioning_examples.rb +77 -0
  36. data/spec/spec_helper.rb +11 -3
  37. data/spec/support/basic_auth_encode_helpers.rb +4 -0
  38. data/spec/support/rack_patch.rb +25 -0
  39. data/spec/support/versioned_helpers.rb +34 -0
  40. metadata +140 -241
  41. data/.yardoc/checksums +0 -13
  42. data/.yardoc/objects/Grape.dat +0 -0
  43. data/.yardoc/objects/Grape/API.dat +0 -0
  44. data/.yardoc/objects/Grape/API/auth_c.dat +0 -0
  45. data/.yardoc/objects/Grape/API/build_endpoint_c.dat +0 -0
  46. data/.yardoc/objects/Grape/API/call_c.dat +0 -0
  47. data/.yardoc/objects/Grape/API/compile_path_c.dat +0 -0
  48. data/.yardoc/objects/Grape/API/default_format_c.dat +0 -0
  49. data/.yardoc/objects/Grape/API/delete_c.dat +0 -0
  50. data/.yardoc/objects/Grape/API/get_c.dat +0 -0
  51. data/.yardoc/objects/Grape/API/group_c.dat +0 -0
  52. data/.yardoc/objects/Grape/API/head_c.dat +0 -0
  53. data/.yardoc/objects/Grape/API/helpers_c.dat +0 -0
  54. data/.yardoc/objects/Grape/API/http_basic_c.dat +0 -0
  55. data/.yardoc/objects/Grape/API/inherited_c.dat +0 -0
  56. data/.yardoc/objects/Grape/API/logger_c.dat +0 -0
  57. data/.yardoc/objects/Grape/API/namespace_c.dat +0 -0
  58. data/.yardoc/objects/Grape/API/nest_c.dat +0 -0
  59. data/.yardoc/objects/Grape/API/post_c.dat +0 -0
  60. data/.yardoc/objects/Grape/API/prefix_c.dat +0 -0
  61. data/.yardoc/objects/Grape/API/put_c.dat +0 -0
  62. data/.yardoc/objects/Grape/API/reset_21_c.dat +0 -0
  63. data/.yardoc/objects/Grape/API/resource_c.dat +0 -0
  64. data/.yardoc/objects/Grape/API/resources_c.dat +0 -0
  65. data/.yardoc/objects/Grape/API/route_c.dat +0 -0
  66. data/.yardoc/objects/Grape/API/route_set_c.dat +0 -0
  67. data/.yardoc/objects/Grape/API/scope_c.dat +0 -0
  68. data/.yardoc/objects/Grape/API/set_c.dat +0 -0
  69. data/.yardoc/objects/Grape/API/settings_c.dat +0 -0
  70. data/.yardoc/objects/Grape/API/settings_stack_c.dat +0 -0
  71. data/.yardoc/objects/Grape/API/version_c.dat +0 -0
  72. data/.yardoc/objects/Grape/Endpoint.dat +0 -0
  73. data/.yardoc/objects/Grape/Endpoint/block_3D_c.dat +0 -0
  74. data/.yardoc/objects/Grape/Endpoint/block_c.dat +0 -0
  75. data/.yardoc/objects/Grape/Endpoint/call_c.dat +0 -0
  76. data/.yardoc/objects/Grape/Endpoint/call_i.dat +0 -0
  77. data/.yardoc/objects/Grape/Endpoint/env_i.dat +0 -0
  78. data/.yardoc/objects/Grape/Endpoint/error_21_i.dat +0 -0
  79. data/.yardoc/objects/Grape/Endpoint/generate_c.dat +0 -0
  80. data/.yardoc/objects/Grape/Endpoint/header_i.dat +0 -0
  81. data/.yardoc/objects/Grape/Endpoint/params_i.dat +0 -0
  82. data/.yardoc/objects/Grape/Endpoint/request_i.dat +0 -0
  83. data/.yardoc/objects/Grape/Endpoint/status_i.dat +0 -0
  84. data/.yardoc/objects/Grape/Endpoint/version_i.dat +0 -0
  85. data/.yardoc/objects/Grape/Middleware.dat +0 -0
  86. data/.yardoc/objects/Grape/Middleware/Auth.dat +0 -0
  87. data/.yardoc/objects/Grape/Middleware/Auth/Basic.dat +0 -0
  88. data/.yardoc/objects/Grape/Middleware/Auth/Basic/authenticator_i.dat +0 -0
  89. data/.yardoc/objects/Grape/Middleware/Auth/Basic/basic_request_i.dat +0 -0
  90. data/.yardoc/objects/Grape/Middleware/Auth/Basic/before_i.dat +0 -0
  91. data/.yardoc/objects/Grape/Middleware/Auth/Basic/credentials_i.dat +0 -0
  92. data/.yardoc/objects/Grape/Middleware/Auth/Basic/initialize_i.dat +0 -0
  93. data/.yardoc/objects/Grape/Middleware/Auth/OAuth2.dat +0 -0
  94. data/.yardoc/objects/Grape/Middleware/Auth/OAuth2/before_i.dat +0 -0
  95. data/.yardoc/objects/Grape/Middleware/Auth/OAuth2/default_options_i.dat +0 -0
  96. data/.yardoc/objects/Grape/Middleware/Auth/OAuth2/error_out_i.dat +0 -0
  97. data/.yardoc/objects/Grape/Middleware/Auth/OAuth2/parse_authorization_header_i.dat +0 -0
  98. data/.yardoc/objects/Grape/Middleware/Auth/OAuth2/token_class_i.dat +0 -0
  99. data/.yardoc/objects/Grape/Middleware/Auth/OAuth2/verify_token_i.dat +0 -0
  100. data/.yardoc/objects/Grape/Middleware/Base.dat +0 -0
  101. data/.yardoc/objects/Grape/Middleware/Base/after_i.dat +0 -0
  102. data/.yardoc/objects/Grape/Middleware/Base/app_i.dat +0 -0
  103. data/.yardoc/objects/Grape/Middleware/Base/before_i.dat +0 -0
  104. data/.yardoc/objects/Grape/Middleware/Base/call_21_i.dat +0 -0
  105. data/.yardoc/objects/Grape/Middleware/Base/call_i.dat +0 -0
  106. data/.yardoc/objects/Grape/Middleware/Base/default_options_i.dat +0 -0
  107. data/.yardoc/objects/Grape/Middleware/Base/env_i.dat +0 -0
  108. data/.yardoc/objects/Grape/Middleware/Base/initialize_i.dat +0 -0
  109. data/.yardoc/objects/Grape/Middleware/Base/options_i.dat +0 -0
  110. data/.yardoc/objects/Grape/Middleware/Base/request_i.dat +0 -0
  111. data/.yardoc/objects/Grape/Middleware/Base/response_i.dat +0 -0
  112. data/.yardoc/objects/Grape/Middleware/Error.dat +0 -0
  113. data/.yardoc/objects/Grape/Middleware/Error/call_21_i.dat +0 -0
  114. data/.yardoc/objects/Grape/Middleware/Error/error_response_i.dat +0 -0
  115. data/.yardoc/objects/Grape/Middleware/Formatter.dat +0 -0
  116. data/.yardoc/objects/Grape/Middleware/Formatter/CONTENT_TYPES.dat +0 -0
  117. data/.yardoc/objects/Grape/Middleware/Formatter/after_i.dat +0 -0
  118. data/.yardoc/objects/Grape/Middleware/Formatter/before_i.dat +0 -0
  119. data/.yardoc/objects/Grape/Middleware/Formatter/content_types_i.dat +0 -0
  120. data/.yardoc/objects/Grape/Middleware/Formatter/default_options_i.dat +0 -0
  121. data/.yardoc/objects/Grape/Middleware/Formatter/encode_json_i.dat +0 -0
  122. data/.yardoc/objects/Grape/Middleware/Formatter/encode_txt_i.dat +0 -0
  123. data/.yardoc/objects/Grape/Middleware/Formatter/format_from_extension_i.dat +0 -0
  124. data/.yardoc/objects/Grape/Middleware/Formatter/format_from_header_i.dat +0 -0
  125. data/.yardoc/objects/Grape/Middleware/Formatter/headers_i.dat +0 -0
  126. data/.yardoc/objects/Grape/Middleware/Formatter/mime_array_i.dat +0 -0
  127. data/.yardoc/objects/Grape/Middleware/Formatter/mime_types_i.dat +0 -0
  128. data/.yardoc/objects/Grape/Middleware/Prefixer.dat +0 -0
  129. data/.yardoc/objects/Grape/Middleware/Prefixer/before_i.dat +0 -0
  130. data/.yardoc/objects/Grape/Middleware/Prefixer/prefix_i.dat +0 -0
  131. data/.yardoc/objects/Grape/Middleware/Versioner.dat +0 -0
  132. data/.yardoc/objects/Grape/Middleware/Versioner/before_i.dat +0 -0
  133. data/.yardoc/objects/Grape/Middleware/Versioner/default_options_i.dat +0 -0
  134. data/.yardoc/objects/Grape/MiddlewareStack.dat +0 -0
  135. data/.yardoc/objects/Grape/MiddlewareStack/initialize_i.dat +0 -0
  136. data/.yardoc/objects/Grape/MiddlewareStack/stack_i.dat +0 -0
  137. data/.yardoc/objects/Grape/MiddlewareStack/to_app_i.dat +0 -0
  138. data/.yardoc/objects/Grape/MiddlewareStack/use_i.dat +0 -0
  139. data/.yardoc/objects/root.dat +0 -0
  140. data/.yardoc/proxy_types +0 -2
  141. data/Gemfile.lock +0 -52
  142. data/autotest/discover.rb +0 -1
@@ -0,0 +1,41 @@
1
+ module Grape
2
+ class Cookies
3
+
4
+ def initialize
5
+ @cookies = {}
6
+ @send_cookies = {}
7
+ end
8
+
9
+ def read(request)
10
+ request.cookies.each do |name, value|
11
+ @cookies[name.to_sym] = value
12
+ end
13
+ end
14
+
15
+ def write(header)
16
+ @cookies.select { |key, value|
17
+ @send_cookies[key.to_sym] == true
18
+ }.each { |name, value|
19
+ Rack::Utils.set_cookie_header!(
20
+ header, name, value.instance_of?(Hash) ? value : { :value => value })
21
+ }
22
+ end
23
+
24
+ def [](name)
25
+ @cookies[name]
26
+ end
27
+
28
+ def []=(name, value)
29
+ @cookies[name.to_sym] = value
30
+ @send_cookies[name.to_sym] = true
31
+ end
32
+
33
+ def each(&block)
34
+ @cookies.each(&block)
35
+ end
36
+
37
+ def delete(name)
38
+ self.[]=(name, { :value => 'deleted', :expires => Time.at(0) })
39
+ end
40
+ end
41
+ end
@@ -1,5 +1,6 @@
1
1
  require 'rack'
2
2
  require 'grape'
3
+ require 'hashie'
3
4
 
4
5
  module Grape
5
6
  # An Endpoint is the proxy scope in which all routing
@@ -7,37 +8,119 @@ module Grape
7
8
  # on the instance level of this class may be called
8
9
  # from inside a `get`, `post`, etc. block.
9
10
  class Endpoint
10
- def self.generate(&block)
11
- c = Class.new(Grape::Endpoint)
12
- c.class_eval do
13
- @block = block
11
+ attr_accessor :block, :options, :settings
12
+ attr_reader :env, :request
13
+
14
+ def initialize(settings, options = {}, &block)
15
+ @settings = settings
16
+ @block = block
17
+ @options = options
18
+
19
+ raise ArgumentError, "Must specify :path option." unless options.key?(:path)
20
+ options[:path] = Array(options[:path])
21
+ options[:path] = ['/'] if options[:path].empty?
22
+
23
+ raise ArgumentError, "Must specify :method option." unless options.key?(:method)
24
+ options[:method] = Array(options[:method])
25
+
26
+ options[:route_options] ||= {}
27
+ end
28
+
29
+ def routes
30
+ @routes ||= prepare_routes
31
+ end
32
+
33
+ def mount_in(route_set)
34
+ if options[:app] && options[:app].respond_to?(:endpoints)
35
+ options[:app].endpoints.each{|e| e.mount_in(route_set)}
36
+ else
37
+ routes.each do |route|
38
+ route_set.add_route(self, {
39
+ :path_info => route.route_compiled,
40
+ :request_method => route.route_method,
41
+ }, { :route_info => route })
42
+ end
14
43
  end
15
- c
16
44
  end
17
-
18
- class << self
19
- attr_accessor :block
45
+
46
+ def prepare_routes
47
+ routes = []
48
+ options[:method].each do |method|
49
+ options[:path].each do |path|
50
+ prepared_path = prepare_path(path)
51
+
52
+ anchor = options[:route_options][:anchor]
53
+ anchor = anchor.nil? ? true : anchor
54
+
55
+ path = compile_path(prepared_path, anchor && !options[:app])
56
+ regex = Rack::Mount::RegexpWithNamedGroups.new(path)
57
+ path_params = {}
58
+ # named parameters in the api path
59
+ named_params = regex.named_captures.map { |nc| nc[0] } - [ 'version', 'format' ]
60
+ named_params.each { |named_param| path_params[named_param] = "" }
61
+ # route parameters declared via desc or appended to the api declaration
62
+ route_params = (options[:route_options][:params] || {})
63
+ path_params.merge!(route_params)
64
+ request_method = (method.to_s.upcase unless method == :any)
65
+ routes << Route.new(options[:route_options].clone.merge({
66
+ :prefix => settings[:root_prefix],
67
+ :version => settings[:version] ? settings[:version].join('|') : nil,
68
+ :namespace => namespace,
69
+ :method => request_method,
70
+ :path => prepared_path,
71
+ :params => path_params,
72
+ :compiled => path,
73
+ })
74
+ )
75
+ end
76
+ end
77
+ routes
20
78
  end
21
-
22
- def self.call(env)
23
- new.call(env)
79
+
80
+ def prepare_path(path)
81
+ parts = []
82
+ parts << settings[:root_prefix] if settings[:root_prefix]
83
+ parts << ':version' if settings[:version] && settings[:version_options][:using] == :path
84
+ parts << namespace.to_s if namespace
85
+ parts << path.to_s if path && '/' != path
86
+ parts.last << '(.:format)'
87
+ Rack::Mount::Utils.normalize_path(parts.join('/'))
24
88
  end
25
-
26
- attr_reader :env, :request
27
-
89
+
90
+ def namespace
91
+ Rack::Mount::Utils.normalize_path(settings.stack.map{|s| s[:namespace]}.join('/'))
92
+ end
93
+
94
+ def compile_path(prepared_path, anchor = true)
95
+ endpoint_options = {}
96
+ endpoint_options[:version] = /#{settings[:version].join('|')}/ if settings[:version]
97
+ Rack::Mount::Strexp.compile(prepared_path, endpoint_options, %w( / . ? ), anchor)
98
+ end
99
+
100
+ def call(env)
101
+ dup.call!(env)
102
+ end
103
+
104
+ def call!(env)
105
+ env['api.endpoint'] = self
106
+ if options[:app]
107
+ options[:app].call(env)
108
+ else
109
+ builder = build_middleware
110
+ builder.run options[:app] || lambda{|env| self.run(env) }
111
+ builder.call(env)
112
+ end
113
+ end
114
+
28
115
  # The parameters passed into the request as
29
116
  # well as parsed from URL segments.
30
117
  def params
31
- @params ||= request.params.merge(env['rack.routing_args'] || {}).inject({}) do |h,(k,v)|
32
- h[k.to_s] = v
33
- h[k.to_sym] = v
34
- h
35
- end
118
+ @params ||= Hashie::Mash.new.deep_merge(request.params).deep_merge(env['rack.routing_args'] || {})
36
119
  end
37
-
120
+
38
121
  # The API version as specified in the URL.
39
122
  def version; env['api.version'] end
40
-
123
+
41
124
  # End the request and display an error to the
42
125
  # end user with the specified message.
43
126
  #
@@ -46,7 +129,7 @@ module Grape
46
129
  def error!(message, status=403)
47
130
  throw :error, :message => message, :status => status
48
131
  end
49
-
132
+
50
133
  # Set or retrieve the HTTP status code.
51
134
  #
52
135
  # @param status [Integer] The HTTP Status Code to return for this request.
@@ -61,9 +144,9 @@ module Grape
61
144
  else
62
145
  200
63
146
  end
64
- end
147
+ end
65
148
  end
66
-
149
+
67
150
  # Set an individual header or retrieve
68
151
  # all headers that have been set.
69
152
  def header(key = nil, val = nil)
@@ -73,15 +156,161 @@ module Grape
73
156
  @header
74
157
  end
75
158
  end
159
+
160
+ # Set or get a cookie
161
+ #
162
+ # @example
163
+ # cookies[:mycookie] = 'mycookie val'
164
+ # cookies['mycookie-string'] = 'mycookie string val'
165
+ # cookies[:more] = { :value => '123', :expires => Time.at(0) }
166
+ # cookies.delete :more
167
+ #
168
+ def cookies
169
+ @cookies ||= Cookies.new
170
+ end
171
+
172
+ # Allows you to define the response body as something other than the
173
+ # return value.
174
+ #
175
+ # @example
176
+ # get '/body' do
177
+ # body "Body"
178
+ # "Not the Body"
179
+ # end
180
+ #
181
+ # GET /body # => "Body"
182
+ def body(value = nil)
183
+ if value
184
+ @body = value
185
+ else
186
+ @body
187
+ end
188
+ end
189
+
190
+ # Allows you to make use of Grape Entities by setting
191
+ # the response body to the serializable hash of the
192
+ # entity provided in the `:with` option. This has the
193
+ # added benefit of automatically passing along environment
194
+ # and version information to the serialization, making it
195
+ # very easy to do conditional exposures. See Entity docs
196
+ # for more info.
197
+ #
198
+ # @example
199
+ #
200
+ # get '/users/:id' do
201
+ # present User.find(params[:id]),
202
+ # :with => API::Entities::User,
203
+ # :admin => current_user.admin?
204
+ # end
205
+ def present(object, options = {})
206
+ entity_class = options.delete(:with)
207
+
208
+ object.class.ancestors.each do |potential|
209
+ entity_class ||= (settings[:representations] || {})[potential]
210
+ end
211
+
212
+ root = options.delete(:root)
213
+
214
+ representation = if entity_class
215
+ embeds = {:env => env}
216
+ embeds[:version] = env['api.version'] if env['api.version']
217
+ entity_class.represent(object, embeds.merge(options))
218
+ else
219
+ object
220
+ end
221
+
222
+ representation = { root => representation } if root
223
+ body representation
224
+ end
76
225
 
77
- def call(env)
226
+ # Returns route information for the current request.
227
+ #
228
+ # @example
229
+ #
230
+ # desc "Returns the route description."
231
+ # get '/' do
232
+ # route.route_description
233
+ # end
234
+ def route
235
+ env["rack.routing_args"][:route_info]
236
+ end
237
+
238
+ protected
239
+
240
+ def run(env)
78
241
  @env = env
79
242
  @header = {}
80
243
  @request = Rack::Request.new(@env)
244
+
245
+ self.extend helpers
246
+ cookies.read(@request)
247
+ run_filters befores
248
+ response_text = instance_eval &self.block
249
+ run_filters afters
250
+ cookies.write(header)
81
251
 
82
- response_text = instance_eval &self.class.block
252
+ [status, header, [body || response_text]]
253
+ end
254
+
255
+ def build_middleware
256
+ b = Rack::Builder.new
257
+
258
+ b.use Grape::Middleware::Error,
259
+ :default_status => settings[:default_error_status] || 403,
260
+ :rescue_all => settings[:rescue_all],
261
+ :rescued_errors => settings[:rescued_errors],
262
+ :format => settings[:error_format] || :txt,
263
+ :rescue_options => settings[:rescue_options],
264
+ :rescue_handlers => settings[:rescue_handlers] || {}
265
+
266
+ b.use Rack::Auth::Basic, settings[:auth][:realm], &settings[:auth][:proc] if settings[:auth] && settings[:auth][:type] == :http_basic
267
+ b.use Rack::Auth::Digest::MD5, settings[:auth][:realm], settings[:auth][:opaque], &settings[:auth][:proc] if settings[:auth] && settings[:auth][:type] == :http_digest
268
+ b.use Grape::Middleware::Prefixer, :prefix => settings[:root_prefix] if settings[:root_prefix]
269
+
270
+ if settings[:version]
271
+ b.use Grape::Middleware::Versioner.using(settings[:version_options][:using]), {
272
+ :versions => settings[:version],
273
+ :version_options => settings[:version_options]
274
+ }
275
+ end
83
276
 
84
- [status, header, [response_text]]
277
+ b.use Grape::Middleware::Formatter,
278
+ :format => settings[:format],
279
+ :default_format => settings[:default_format] || :txt,
280
+ :content_types => settings[:content_types]
281
+
282
+ aggregate_setting(:middleware).each do |m|
283
+ m = m.dup
284
+ block = m.pop if m.last.is_a?(Proc)
285
+ if block
286
+ b.use *m, &block
287
+ else
288
+ b.use *m
289
+ end
290
+ end
291
+
292
+ b
293
+ end
294
+
295
+ def helpers
296
+ m = Module.new
297
+ settings.stack.each{|frame| m.send :include, frame[:helpers] if frame[:helpers]}
298
+ m
85
299
  end
300
+
301
+ def aggregate_setting(key)
302
+ settings.stack.inject([]) do |aggregate, frame|
303
+ aggregate += (frame[key] || [])
304
+ end
305
+ end
306
+
307
+ def run_filters(filters)
308
+ (filters || []).each do |filter|
309
+ instance_eval &filter
310
+ end
311
+ end
312
+
313
+ def befores; aggregate_setting(:befores) end
314
+ def afters; aggregate_setting(:afters) end
86
315
  end
87
316
  end
@@ -0,0 +1,227 @@
1
+ require 'hashie'
2
+
3
+ module Grape
4
+ # An Entity is a lightweight structure that allows you to easily
5
+ # represent data from your application in a consistent and abstracted
6
+ # way in your API.
7
+ #
8
+ # @example Entity Definition
9
+ #
10
+ # module API
11
+ # module Entities
12
+ # class User < Grape::Entity
13
+ # expose :first_name, :last_name, :screen_name, :location
14
+ # expose :latest_status, :using => API::Status, :as => :status, :unless => {:collection => true}
15
+ # expose :email, :if => {:type => :full}
16
+ # expose :new_attribute, :if => {:version => 'v2'}
17
+ # expose(:name){|model,options| [model.first_name, model.last_name].join(' ')}
18
+ # end
19
+ # end
20
+ # end
21
+ #
22
+ # Entities are not independent structures, rather, they create
23
+ # **representations** of other Ruby objects using a number of methods
24
+ # that are convenient for use in an API. Once you've defined an Entity,
25
+ # you can use it in your API like this:
26
+ #
27
+ # @example Usage in the API Layer
28
+ #
29
+ # module API
30
+ # class Users < Grape::API
31
+ # version 'v2'
32
+ #
33
+ # get '/users' do
34
+ # @users = User.all
35
+ # type = current_user.admin? ? :full : :default
36
+ # present @users, :with => API::Entities::User, :type => type
37
+ # end
38
+ # end
39
+ # end
40
+ class Entity
41
+ attr_reader :object, :options
42
+
43
+ # This method is the primary means by which you will declare what attributes
44
+ # should be exposed by the entity.
45
+ #
46
+ # @option options :as Declare an alias for the representation of this attribute.
47
+ # @option options :if When passed a Hash, the attribute will only be exposed if the
48
+ # runtime options match all the conditions passed in. When passed a lambda, the
49
+ # lambda will execute with two arguments: the object being represented and the
50
+ # options passed into the representation call. Return true if you want the attribute
51
+ # to be exposed.
52
+ # @option options :unless When passed a Hash, the attribute will be exposed if the
53
+ # runtime options fail to match any of the conditions passed in. If passed a lambda,
54
+ # it will yield the object being represented and the options passed to the
55
+ # representation call. Return true to prevent exposure, false to allow it.
56
+ # @option options :using This option allows you to map an attribute to another Grape
57
+ # Entity. Pass it a Grape::Entity class and the attribute in question will
58
+ # automatically be transformed into a representation that will receive the same
59
+ # options as the parent entity when called. Note that arrays are fine here and
60
+ # will automatically be detected and handled appropriately.
61
+ # @option options :proc If you pass a Proc into this option, it will
62
+ # be used directly to determine the value for that attribute. It
63
+ # will be called with the represented object as well as the
64
+ # runtime options that were passed in. You can also just supply a
65
+ # block to the expose call to achieve the same effect.
66
+ def self.expose(*args, &block)
67
+ options = args.last.is_a?(Hash) ? args.pop : {}
68
+
69
+ if args.size > 1
70
+ raise ArgumentError, "You may not use the :as option on multi-attribute exposures." if options[:as]
71
+ raise ArgumentError, "You may not use block-setting on multi-attribute exposures." if block_given?
72
+ end
73
+
74
+ options[:proc] = block if block_given?
75
+
76
+ args.each do |attribute|
77
+ exposures[attribute.to_sym] = options
78
+ end
79
+ end
80
+
81
+ # Returns a hash of exposures that have been declared for this Entity or ancestors. The keys
82
+ # are symbolized references to methods on the containing object, the values are
83
+ # the options that were passed into expose.
84
+ def self.exposures
85
+ @exposures ||= {}
86
+
87
+ if superclass.respond_to? :exposures
88
+ @exposures = superclass.exposures.merge(@exposures)
89
+ end
90
+
91
+ @exposures
92
+ end
93
+
94
+ # This allows you to set a root element name for your representation.
95
+ #
96
+ # @param plural [String] the root key to use when representing
97
+ # a collection of objects. If missing or nil, no root key will be used
98
+ # when representing collections of objects.
99
+ # @param singular [String] the root key to use when representing
100
+ # a single object. If missing or nil, no root key will be used when
101
+ # representing an individual object.
102
+ #
103
+ # @example Entity Definition
104
+ #
105
+ # module API
106
+ # module Entities
107
+ # class User < Grape::Entity
108
+ # root 'users', 'user'
109
+ # expose :id
110
+ # end
111
+ # end
112
+ # end
113
+ #
114
+ # @example Usage in the API Layer
115
+ #
116
+ # module API
117
+ # class Users < Grape::API
118
+ # version 'v2'
119
+ #
120
+ # # this will render { "users": [ {"id":"1"}, {"id":"2"} ] }
121
+ # get '/users' do
122
+ # @users = User.all
123
+ # present @users, :with => API::Entities::User
124
+ # end
125
+ #
126
+ # # this will render { "user": {"id":"1"} }
127
+ # get '/users/:id' do
128
+ # @user = User.find(params[:id])
129
+ # present @user, :with => API::Entities::User
130
+ # end
131
+ # end
132
+ # end
133
+ def self.root(plural, singular=nil)
134
+ @collection_root = plural
135
+ @root = singular
136
+ end
137
+
138
+ # This convenience method allows you to instantiate one or more entities by
139
+ # passing either a singular or collection of objects. Each object will be
140
+ # initialized with the same options. If an array of objects is passed in,
141
+ # an array of entities will be returned. If a single object is passed in,
142
+ # a single entity will be returned.
143
+ #
144
+ # @param objects [Object or Array] One or more objects to be represented.
145
+ # @param options [Hash] Options that will be passed through to each entity
146
+ # representation.
147
+ #
148
+ # @option options :root [String] override the default root name set for the
149
+ #  entity. Pass nil or false to represent the object or objects with no
150
+ # root name even if one is defined for the entity.
151
+ def self.represent(objects, options = {})
152
+ inner = if objects.respond_to?(:to_ary)
153
+ objects.to_ary().map{|o| self.new(o, {:collection => true}.merge(options))}
154
+ else
155
+ self.new(objects, options)
156
+ end
157
+
158
+ root_element = if options.has_key?(:root)
159
+ options[:root]
160
+ else
161
+ objects.respond_to?(:to_ary) ? @collection_root : @root
162
+ end
163
+ root_element ? { root_element => inner } : inner
164
+ end
165
+
166
+ def initialize(object, options = {})
167
+ @object, @options = object, options
168
+ end
169
+
170
+ def exposures
171
+ self.class.exposures
172
+ end
173
+
174
+ # The serializable hash is the Entity's primary output. It is the transformed
175
+ # hash for the given data model and is used as the basis for serialization to
176
+ # JSON and other formats.
177
+ #
178
+ # @param options [Hash] Any options you pass in here will be known to the entity
179
+ # representation, this is where you can trigger things from conditional options
180
+ # etc.
181
+ def serializable_hash(runtime_options = {})
182
+ return nil if object.nil?
183
+ opts = options.merge(runtime_options || {})
184
+ exposures.inject({}) do |output, (attribute, exposure_options)|
185
+ output[key_for(attribute)] = value_for(attribute, opts) if conditions_met?(exposure_options, opts)
186
+ output
187
+ end
188
+ end
189
+
190
+ alias :as_json :serializable_hash
191
+
192
+ protected
193
+
194
+ def key_for(attribute)
195
+ exposures[attribute.to_sym][:as] || attribute.to_sym
196
+ end
197
+
198
+ def value_for(attribute, options = {})
199
+ exposure_options = exposures[attribute.to_sym]
200
+
201
+ if exposure_options[:proc]
202
+ exposure_options[:proc].call(object, options)
203
+ elsif exposure_options[:using]
204
+ exposure_options[:using].represent(object.send(attribute), :root => nil)
205
+ else
206
+ object.send(attribute)
207
+ end
208
+ end
209
+
210
+ def conditions_met?(exposure_options, options)
211
+ if_condition = exposure_options[:if]
212
+ unless_condition = exposure_options[:unless]
213
+
214
+ case if_condition
215
+ when Hash; if_condition.each_pair{|k,v| return false if options[k.to_sym] != v }
216
+ when Proc; return false unless if_condition.call(object, options)
217
+ end
218
+
219
+ case unless_condition
220
+ when Hash; unless_condition.each_pair{|k,v| return false if options[k.to_sym] == v}
221
+ when Proc; return false if unless_condition.call(object, options)
222
+ end
223
+
224
+ true
225
+ end
226
+ end
227
+ end