roda-rest_api 1.4.5 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c384fef43088fdccb5d94c332d3d02d820ef1335
4
- data.tar.gz: a5162d2d027be2e5aa6441e36bd854658958bdf0
3
+ metadata.gz: 3d5c2fcc7e1accb98d420dab826d45d09e562f10
4
+ data.tar.gz: 3c0d41e7da9e9e2f9079a07298f38600217d8789
5
5
  SHA512:
6
- metadata.gz: 5c353bb7255e621679c71de400e7b7ef23752a33247d028b0ab9e35b0e373b5d078374a504bc0f9a43c932948c77275c9ae490ce8fedfa103a431bdb41a8ef80
7
- data.tar.gz: 05bc814b93814c1d2c5ec01b2fac41094f28e07e3fbfb0ef075adb9238cb48181c03e5a9a7cc1ae4d7ff10f6480c41ae70070f63d6d05c87a32752d64fbe2b5c
6
+ metadata.gz: 9f070e65992e1a696917d4f42c04904982d17cb2a2e05491cc9c3dc212d596858d1bbb1c8179415f4480b873983bea9a1dfd986dea1c9e3bb563e9f8188d636c
7
+ data.tar.gz: bf5acc47f30a35d9eea74137e1c2a3b9e81d5a2fd210c0c9fd8bd6476052bb8a257be370faad8d3c7f7854fe3a6d0a31b3e1a6569a588fff6331cbc2250c48fe
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'roda', '~> 2'
4
+
5
+ group :develpment do
6
+ gem 'rake', '~> 10'
7
+ gem 'minitest', '~> 5.5'
8
+ gem 'rack-test', '~> 0.6'
9
+ gem 'jeweler', '~> 2.0'
10
+ end
data/README.md ADDED
@@ -0,0 +1,302 @@
1
+ Roda plugin for RESTful APIs
2
+ =============
3
+
4
+ ### Quick start
5
+
6
+ Install gem with
7
+
8
+ gem 'roda-rest_api' #Gemfile
9
+
10
+ or
11
+
12
+ gem install roda-rest_api #Manual
13
+
14
+ Create rack app
15
+
16
+ ```ruby
17
+ #api.ru
18
+
19
+ require 'roda/rest_api'
20
+ require 'json'
21
+
22
+ class App < Roda
23
+
24
+ plugin :rest_api
25
+
26
+ route do |r|
27
+ r.api do
28
+ r.version 3 do
29
+ r.resource :things do |things|
30
+ things.list {|param| ['foo', 'bar']}
31
+ things.routes :index
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ run App
39
+ ```
40
+
41
+ And run with
42
+
43
+ bundle exec rackup api.ru
44
+
45
+ Try it out on:
46
+
47
+ curl http://127.0.0.1:9292/api/v3/things
48
+
49
+
50
+ ### Usage
51
+
52
+ ```ruby
53
+ route do |r|
54
+ r.api path:'', subdomain:'api' do # 'mount' on api.example.com/v1/...
55
+ r.version 1 do
56
+
57
+ #define all 7 routes:
58
+ # index - GET /v1/songs
59
+ # show - GET /v1/songs/:id
60
+ # create - POST /v1/songs
61
+ # update - PUT | PATCH /v1/songs/:id
62
+ # destroy - DELETE /v1/songs/:id
63
+ # edit - GET /v1/songs/:id/edit
64
+ # new - GET /v1/songs/new
65
+
66
+ # call permit to whitelist allowed parameters for save callback
67
+
68
+ r.resource :songs do |songs|
69
+ songs.list { |params| Song.where(params).all } #index
70
+ songs.one { |params| Song[params[:id]] } #show, edit, new
71
+ songs.delete { |params| Song[params[:id]].destroy } #destroy
72
+ songs.save { |atts| Song.create_or_update(atts) } #create, update
73
+ songs.permit :title, author: [:name, :address]
74
+ end
75
+
76
+ #define 2 routes and custom serializer, custom primary key:
77
+ # index - GET /v1/artists
78
+ # show - GET /v1/artists/:id
79
+
80
+ r.resource :artists, content_type: 'application/xml', primary_key: :artist_id do |artists|
81
+ artists.list { |params| Artist.where(params).all }
82
+ artists.one { |params| Artist[params[:artist_id]] }
83
+ artists.serialize { |result| ArtistSerializer.xml(result) }
84
+ artists.routes :index, :show
85
+ end
86
+
87
+ #define 6 singleton routes:
88
+ # show - GET /v1/profile
89
+ # create - POST /v1/profile
90
+ # update - PUT | PATCH /v1/profile
91
+ # destroy - DELETE /v1/profile
92
+ # edit - GET /v1/profile/edit
93
+ # new - GET /v1/profile/new
94
+
95
+ r.resource :profile, singleton: true do |profile|
96
+ profile.one { |params| current_user.profile } #show, edit, new
97
+ profile.save { |atts| current_user.profile.create_or_update(atts) } #create, update
98
+ profile.delete { |params| current_user.profile.destroy } #destroy
99
+ profile.permit :name, :address
100
+ end
101
+
102
+ #define nested routes
103
+ # index - GET /v1/albums/:parent_id/songs
104
+ # show - GET /v1/albums/:parent_id/songs/:id
105
+ # index - GET /v1/albums/:album_id/artwork
106
+ # index - GET /v1/albums/favorites
107
+ # show - GET /v1/albums/favorites/:id
108
+
109
+ r.resource :albums do |albums|
110
+ r.resource :songs do |songs|
111
+ songs.list { |params| Song.where({ :album_id => params[:parent_id] }) }
112
+ songs.one { |params| Song[params[:id]] }
113
+ songs.routes :index, :show
114
+ end
115
+ r.resource :artwork, parent_key: :album_id do |artwork|
116
+ artwork.list { |params| Artwork.where({ :album_id => params[:album_id] }).all }
117
+ artwork.routes :index
118
+ end
119
+ r.resource :favorites, bare: true do |favorites|
120
+ favorites.list { |params| Favorite.where(params).all }
121
+ favorites.one { |params| Favorite[params[:id]] ) }
122
+ favorites.routes :index, :show
123
+ end
124
+ end
125
+
126
+ #call block before route is called
127
+
128
+ r.resource :user, singleton: true do |user|
129
+ user.save {|atts| User.create_or_update(atts) }
130
+ user.routes :create # public
131
+ user.routes :update do # private
132
+ authenticate!
133
+ end
134
+ end
135
+
136
+ #define custom routes
137
+
138
+ r.resource :albums do
139
+ r.index do # GET /v1/albums
140
+ # list albums
141
+ end
142
+ r.create do # POST /v1/albums
143
+ # create album
144
+ end
145
+ r.show do |id| # GET /v1/albums/:id
146
+ # show album
147
+ end
148
+ r.update do |id| # PATCH | PUT /v1/albums/:id
149
+ # update album
150
+ end
151
+ r.destroy do |id| # DELETE /v1/albums/:id
152
+ # delete album
153
+ end
154
+ r.edit do |id| # GET /v1/albums/:id/edit
155
+ # edit album
156
+ end
157
+ r.new do # GET /v1/albums/new
158
+ # new album
159
+ end
160
+ end
161
+
162
+ end
163
+ end
164
+ end
165
+ ```
166
+
167
+ ###Options
168
+
169
+ The plugin supports several options serialize, content_type, wrapper and id_pattern to modify processing of the request. Besides these, any number of custom options can be passed, which can be handy for the wrapper option.
170
+ Each option can be specified and overridden at the api, version or resource level.
171
+
172
+ ####Serialization and content type
173
+
174
+ A serializer is an object that responds to :serialize and returns a string. Optionally it can provide the content_type, which may also be specified inline. Serialization can also be specified within a block inside the resource.
175
+
176
+ ```ruby
177
+ class XMLSerializer
178
+
179
+ def serialize(result) #required
180
+ result.to_xml
181
+ end
182
+
183
+ def content_type #optional
184
+ 'text/xml'
185
+ end
186
+
187
+ end
188
+
189
+ class CSVSerializer
190
+
191
+ def serialize(result)
192
+ result.to_csv
193
+ end
194
+
195
+ end
196
+
197
+
198
+ class App < Roda
199
+
200
+ plugin :rest_api
201
+
202
+ route do |r|
203
+ r.api serializer: XMLSerializer.new
204
+ r.resource :things do |things|
205
+ things.list {|param| ['foo', 'bar']}
206
+ things.routes :index
207
+ end
208
+ r.resource :objects, serializer: CSVSerializer.new, content_type: 'text/csv' do |objects|
209
+ objects.one {|param| Object.find(param) }
210
+ objects.routes :show
211
+ end
212
+ r.resource :items do |items|
213
+ items.list {|param| Item.where(param) }
214
+ items.routes :index
215
+ items.serialize content_type: 'text/plain' do |result| #inline specification
216
+ result.to_s
217
+ end
218
+ end
219
+
220
+ end
221
+ end
222
+ end
223
+ ```
224
+
225
+ ####Wrapper
226
+
227
+ A wrapper module can be specified, containing one or more 'around_*' methods. These methods should yield with the passed arguments. Wrappers can be used for cleaning up incoming parameters, database transactions, authorization checking or serialization. A resource can hold a generic :resource option, that can be used for providing extra info to the wrapper. It can be useful to set a custom option like model_class on a resource when using wrappers.
228
+
229
+ ```ruby
230
+ module Wrapper
231
+
232
+ def around_save(atts)
233
+ # actions before save
234
+ result = yield(atts) # call save action
235
+ #actions after save
236
+ result
237
+ end
238
+
239
+ # around_one, around_list, around_delete
240
+ end
241
+
242
+ module SpecialWrapper
243
+
244
+ def around_delete(atts)
245
+ model_class = opts[:model_class]
246
+ if current_user.can_delete(model_class[atts[:id]])
247
+ yield(atts)
248
+ else
249
+ #not allowed
250
+ end
251
+ end
252
+
253
+ def current_user
254
+ @request.current_user
255
+ end
256
+
257
+ end
258
+
259
+ class App < Roda
260
+
261
+ plugin :rest_api
262
+
263
+ route do |r|
264
+ r.api wrapper: Wrapper
265
+ r.resource :things do |things|
266
+ things.one {|params| Thing.find(params) } # will be called inside the 'Wrapper#around_one' method
267
+ things.list {|params| Thing.where(params) } # will be called inside the 'Wrapper#around_list' method
268
+ things.save {|atts| Thing.create_or_update(atts) } # will be called inside the 'Wrapper#around_save' method
269
+ things.delete {|params| Thing.destroy(params) } # will be called inside the 'Wrapper#around_delete' method
270
+ end
271
+ r.resource :items, wrapper: SpecialWrapper, resource: {model_class: Item} do |items|
272
+ items.delete {|params| Item.destroy(params) } # will be called inside the 'SpecialWrapper#around_delete' method
273
+ end
274
+
275
+ end
276
+ end
277
+ end
278
+ ```
279
+
280
+ ####ID pattern
281
+
282
+ To support various id formats, one of the symbol_matcher symbols or a regex can be specified to match custom id formats.
283
+ The plugin adds the :uuid symbol for 8-4-4-4-12 formatted UUIDs.
284
+
285
+ ```ruby
286
+ uuid_pat = /\w{8}-\w{4}-\w{4}-\w{4}-\w{12}/
287
+ r.api id_pattern: uuid_pat
288
+ r.resource :things do |things|
289
+ things.one {|params| Thing.find(params) }
290
+ r.resource :parts, id_pattern: /part(\d+)/ do |parts|
291
+ parts.one {|params| Part.find(params) }
292
+ end
293
+
294
+ end
295
+ end
296
+
297
+ # responds to /things/7e554915-210b-4dxe-a88b-3a09a5e790ge/parts/part123
298
+ ```
299
+
300
+ ### Caveat
301
+
302
+ This plugin catches StandardError when performing the data access methods (list, one, save, delete) and will return a 404 or 422 response code when an error is thrown. When ENV['RACK_ENV'] is set to 'development' the error will be raised, but in all other cases it will fail silently. Be aware that ENV['RACK_ENV'] may be blank, so you won't see any errors even in development. A better approach is to develop and test the data access methods in isolation.
data/Rakefile ADDED
@@ -0,0 +1,22 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new do |t|
4
+ t.libs << "test"
5
+ t.pattern = "test/*_test.rb"
6
+ end
7
+
8
+ require 'jeweler'
9
+ Jeweler::Tasks.new do |gem|
10
+ # gem is a Gem::Specification... see http://guides.rubygems.org/specification-reference/ for more options
11
+ gem.name = "roda-rest_api"
12
+ gem.homepage = "http://github.com/beno/roda-rest_api"
13
+ gem.license = "MIT"
14
+ gem.summary = %Q{Restful resources for Roda}
15
+ gem.description = %Q{Create restful API easily with the Roda framework}
16
+ gem.email = "michelbenevento@yahoo.com"
17
+ gem.authors = ["Michel Benevento"]
18
+ gem.version = "2.0.1"
19
+ # dependencies defined in Gemfile
20
+ end
21
+ Jeweler::RubygemsDotOrgTasks.new
22
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 2.0.1
@@ -1,267 +1,319 @@
1
1
  class Roda
2
- module RodaPlugins
2
+ module RodaPlugins
3
3
 
4
- module RestApi
4
+ module RestApi
5
+
6
+ class DefaultSerializer
7
+ def serialize(result)
8
+ result.is_a?(String) ? result : result.to_json
9
+ end
10
+ end
5
11
 
6
- APPLICATION_JSON = 'application/json'.freeze
7
- SINGLETON_ROUTES = %i{ show create update destroy edit new }.freeze
12
+ APPLICATION_JSON = 'application/json'.freeze
13
+ SINGLETON_ROUTES = %i{ create new update show destroy edit }.freeze
14
+ OPTS = {}.freeze
15
+
16
+ def self.load_dependencies(app, _opts = OPTS)
17
+ app.plugin :all_verbs
18
+ app.plugin :symbol_matchers
19
+ app.plugin :header_matchers
20
+ app.plugin :drop_body
21
+ end
22
+
23
+ def self.configure(app, opts = OPTS)
24
+ all_keys = Resource::OPTIONS.keys << :serialize
25
+ if (opts.keys & all_keys).any?
26
+ raise "For version 2.0 all options [#{Resource::OPTIONS.keys.join(' ')}] must be set at the api, version or resource level."
27
+ end
28
+ end
29
+
30
+ class Resource
31
+
32
+ attr_reader :request, :path, :singleton, :parent, :id_pattern
33
+ attr_accessor :captures
34
+
35
+ OPTIONS = { singleton: false,
36
+ primary_key: "id",
37
+ parent_key: "parent_id",
38
+ id_pattern: /(\d+)/,
39
+ content_type: APPLICATION_JSON,
40
+ bare: false,
41
+ serializer: RestApi::DefaultSerializer.new,
42
+ wrapper: nil,
43
+ resource: nil }.freeze
44
+
45
+ def initialize(path, request, parent, option_chain=[])
46
+ @request = request
47
+ @path = path.to_s
48
+ traverse_options option_chain
49
+ if parent
50
+ @parent = parent
51
+ @path = ":id/#{@path}" unless @bare
52
+ end
53
+ end
54
+
55
+ [:list, :one, :save, :delete, :serialize].each do |meth|
56
+ define_method meth do |&block|
57
+ self.instance_variable_set("@#{meth}", block) if block
58
+ self.instance_variable_get("@#{meth}") || ->(_){raise NotImplementedError, meth.to_s}
59
+ end
60
+ end
61
+
62
+ def content_type
63
+ if @serializer && @serializer.respond_to?(:content_type)
64
+ @serializer.content_type
65
+ else
66
+ @content_type
67
+ end
68
+ end
69
+
70
+ def opts
71
+ @resource || {}
72
+ end
73
+
74
+ def routes(*routes)
75
+ routes! if @routes
76
+ yield if block_given?
77
+ @routes = routes
78
+ end
79
+
80
+ def permit(*permitted)
81
+ @permitted = permitted
82
+ end
83
+
84
+ def routes!
85
+ unless @routes
86
+ @routes = SINGLETON_ROUTES.dup
87
+ @routes << :index unless @singleton
88
+ end
89
+ @routes.each { |route| @request.send(route) }
90
+ end
91
+
92
+ POST_BODY = 'rack.input'.freeze
93
+ FORM_INPUT = 'rack.request.form_input'.freeze
94
+ FORM_HASH = 'rack.request.form_hash'.freeze
8
95
 
9
- def self.load_dependencies(app, _opts = {})
10
- app.plugin :all_verbs
11
- app.plugin :symbol_matchers
12
- app.plugin :header_matchers
13
- app.plugin :drop_body
14
- end
15
-
16
- class Resource
17
-
18
- attr_reader :request, :path, :singleton, :content_type, :parent
19
- attr_accessor :captures
20
-
21
- def initialize(path, request, parent, options={})
22
- @request = request
23
- @path = path.to_s
24
- bare = options.delete(:bare) || false
25
- @singleton = options.delete(:singleton) || false
26
- @primary_key = options.delete(:primary_key) || "id"
27
- @parent_key = options.delete(:parent_key) || "parent_id"
28
- @content_type = options.delete(:content_type) || APPLICATION_JSON
29
- if parent
30
- @parent = parent
31
- @path = [':d', @path].join('/') unless bare
32
- end
33
- end
34
-
35
- def list(&block)
36
- @list = block if block
37
- @list || ->(_){raise NotImplementedError, "list"}
38
- end
39
-
40
- def one(&block)
41
- @one = block if block
42
- @one || ->(_){raise NotImplementedError, "one"}
43
- end
44
-
45
- def save(&block)
46
- @save = block if block
47
- @save || ->(_){raise NotImplementedError, "save"}
48
- end
49
-
50
- def delete(&block)
51
- @delete = block if block
52
- @content_type = nil
53
- @delete || ->(_){raise NotImplementedError, "delete"}
54
- end
55
-
56
- def serialize(&block)
57
- @serialize = block if block
58
- @serialize || ->(obj){obj.is_a?(String) ? obj : obj.send(:to_json)}
59
- end
60
-
61
- def routes(*routes)
62
- routes! if @routes
63
- yield if block_given?
64
- @routes = routes
65
- end
66
-
67
- def permit(*permitted)
68
- @permitted = permitted
69
- end
70
-
71
- def routes!
72
- unless @routes
73
- @routes = SINGLETON_ROUTES.dup
74
- @routes << :index unless @singleton
75
- end
76
- @routes.each { |route| @request.send(route) }
77
- end
78
-
79
- POST_BODY = 'rack.input'.freeze
80
- FORM_INPUT = 'rack.request.form_input'.freeze
81
- FORM_HASH = 'rack.request.form_hash'.freeze
96
+ def perform_wrapped(method, args, &blk)
97
+ if respond_to? :"around_#{method}"
98
+ send :"around_#{method}", args, &blk
99
+ else
100
+ blk.call(args)
101
+ end
102
+ end
103
+
104
+ def perform(method, id = nil)
105
+ begin
106
+ args = self.arguments(method, id)
107
+ perform_wrapped(method, args) do |args|
108
+ r = self.send(method).call(args)
109
+ end
110
+ rescue StandardError => e
111
+ raise if ENV['RACK_ENV'] == 'development'
112
+ @request.response.status = method === :save ? 422 : 404
113
+ @request.response.write e
114
+ end
115
+ end
116
+
117
+ protected
118
+
119
+ def arguments(method, id)
120
+ args = if method === :save
121
+ form = Rack::Request::FORM_DATA_MEDIA_TYPES.include?(@request.media_type)
122
+ permitted_args(form ? @request.POST : JSON.parse(@request.body.read))
123
+ else
124
+ symbolize_keys @request.GET
125
+ end
126
+ args.merge!(@primary_key.to_sym => id) if id
127
+ args.merge!(@parent_key.to_sym => @captures[0]) if @captures
128
+ args
129
+ end
82
130
 
83
-
84
- def perform(method, id = nil)
85
- begin
86
- args = self.arguments(method)
87
- args.merge!(@primary_key.to_sym => id) if id
88
- args.merge!(@parent_key.to_sym => @captures[0]) if @captures
89
- self.send(method).call(args)
90
- rescue StandardError => e
91
- raise if ENV['RACK_ENV'] == 'development'
92
- @request.response.status = method === :save ? 422 : 404
93
- @request.response.write e
94
- end
95
- end
96
-
97
- protected
98
-
99
- def arguments(method)
100
- if method === :save
101
- args = if Rack::Request::FORM_DATA_MEDIA_TYPES.include?(@request.media_type)
102
- @request.POST
103
- else
104
- JSON.parse(@request.body.read)
105
- end
106
- permitted_args args
107
- else
108
- symbolize_keys @request.GET
109
- end
110
- end
131
+ private
132
+
133
+ def traverse_options(option_chain)
134
+ reverse_chain = option_chain.reverse
135
+ OPTIONS.each_pair do |key, default|
136
+ ivar = "@#{key}"
137
+ options = reverse_chain.find do |opts|
138
+ opts[key]
139
+ end
140
+ self.instance_variable_set(ivar, (options && options[key]) || default)
141
+ end
142
+ if @wrapper
143
+ raise ":wrapper should be a module" unless @wrapper.is_a? Module
144
+ self.extend @wrapper
145
+ end
146
+ if @serializer
147
+ raise ":serializer should respond to :serialize" unless @serializer.respond_to?(:serialize)
148
+ @serialize = ->(res){@serializer.serialize(res)}
149
+ end
150
+ end
151
+
152
+ def symbolize_keys(args)
153
+ _args = {}
154
+ args.each do |k,v|
155
+ v = symbolize_keys(v) if v.is_a?(Hash)
156
+ _args[k.to_sym] = v
157
+ end
158
+ _args
159
+ end
160
+
161
+ def permitted_args(args, keypath = [])
162
+ permitted = nil
163
+ case args
164
+ when Hash
165
+ permitted = Hash.new
166
+ args.each_pair do |k,v|
167
+ keypath << k.to_sym
168
+ if permitted?(keypath)
169
+ value = permitted_args(v, keypath)
170
+ permitted[k.to_sym] = value if value
171
+ end
172
+ keypath.pop
173
+ end
174
+ else
175
+ permitted = args if permitted?(keypath)
176
+ end
177
+ permitted
178
+ end
179
+
180
+ def permitted?(keypath)
181
+ return false unless @permitted
182
+ permitted = @permitted
183
+ find_key = ->(items, key){
184
+ items.find do |item|
185
+ case item
186
+ when Hash
187
+ !!item.keys.index(key)
188
+ when Symbol
189
+ item === key
190
+ end
191
+ end
192
+ }
193
+ keypath.each do |key|
194
+ found = find_key.call(permitted, key)
195
+ permitted = found.is_a?(Hash) ? found.values.flatten : []
196
+ return false unless found
197
+ end
198
+ end
199
+
200
+ end
201
+
202
+ module RequestMethods
203
+
204
+ def api(options={}, &block)
205
+ extract_resource_options options
206
+ path = options.delete(:path) || 'api'
207
+ subdomain = options.delete(:subdomain)
208
+ options.merge!(host: /\A#{Regexp.escape(subdomain)}\./) if subdomain
209
+ path = true if path.nil? or path.empty?
210
+ on(path, options, &block)
211
+ end
111
212
 
112
- private
113
-
114
- def symbolize_keys(args)
115
- _args = {}
116
- args.each do |k,v|
117
- v = symbolize_keys(v) if v.is_a?(Hash)
118
- _args[k.to_sym] = v
119
- end
120
- _args
121
- end
122
-
123
- def permitted_args(args, keypath = [])
124
- permitted = nil
125
- case args
126
- when Hash
127
- permitted = Hash.new
128
- args.each_pair do |k,v|
129
- keypath << k.to_sym
130
- if permitted?(keypath)
131
- value = permitted_args(v, keypath)
132
- permitted[k.to_sym] = value if value
133
- end
134
- keypath.pop
135
- end
136
- else
137
- permitted = args if permitted?(keypath)
138
- end
139
- permitted
140
- end
141
-
142
- def permitted?(keypath)
143
- return false unless @permitted
144
- permitted = @permitted
145
- find_key = ->(items, key){
146
- items.find do |item|
147
- case item
148
- when Hash
149
- !!item.keys.index(key)
150
- when Symbol
151
- item === key
152
- end
153
- end
154
- }
155
- keypath.each do |key|
156
- found = find_key.call(permitted, key)
157
- permitted = found.is_a?(Hash) ? found.values.flatten : []
158
- return false unless found
159
- end
160
- end
161
-
162
- end
163
-
164
- module RequestMethods
165
-
166
- def api(options={}, &block)
167
- path = options.delete(:path) || 'api'
168
- subdomain = options.delete(:subdomain)
169
- options.merge!(host: /\A#{Regexp.escape(subdomain)}\./) if subdomain
170
- path = true if path.nil? or path.empty?
171
- on(path, options, &block)
172
- end
213
+ def version(version, options={}, &block)
214
+ extract_resource_options options
215
+ on("v#{version}", options, &block)
216
+ end
173
217
 
174
- def version(version, &block)
175
- on("v#{version}", &block)
176
- end
218
+ def resource(path, options={})
219
+ extract_resource_options options
220
+ @resource = Resource.new(path, self, @resource, @resource_options)
221
+ on(@resource.path, options) do
222
+ roda_class.symbol_matcher(:id, @resource.id_pattern)
223
+ @resource.captures = captures.dup unless captures.empty?
224
+ yield @resource
225
+ @resource.routes!
226
+ response.status = 404
227
+ end
228
+ @resource_options.pop
229
+ @resource = @resource.parent
230
+ end
177
231
 
178
- def resource(path, options={})
179
- @resource = Resource.new(path, self, @resource, options)
180
- on(@resource.path, options) do
181
- @resource.captures = captures.dup unless captures.empty?
182
- yield @resource
183
- @resource.routes!
184
- response.status = 404
185
- end
186
- @resource = @resource.parent
187
- end
232
+ def index(options={}, &block)
233
+ block ||= ->{ @resource.perform(:list) }
234
+ get(['', true], options, &block)
235
+ end
188
236
 
189
- def index(options={}, &block)
190
- block ||= ->{ @resource.perform(:list) }
191
- get(['', true], options, &block)
192
- end
237
+ def show(options={}, &block)
238
+ block ||= default_block(:one)
239
+ get(_path, options, &block)
240
+ end
193
241
 
194
- def show(options={}, &block)
195
- block ||= default_block(:one)
196
- get(_path, options, &block)
197
- end
242
+ def create(options={}, &block)
243
+ block ||= ->(){@resource.perform(:save)}
244
+ post(['', true], options) do
245
+ response.status = 201
246
+ block.call(*captures) if block
247
+ end
248
+ end
198
249
 
199
- def create(options={}, &block)
200
- block ||= ->{@resource.perform(:save)}
201
- post(["", true], options) do
202
- response.status = 201
203
- block.call(*captures) if block
204
- end
205
- end
250
+ def update(options={}, &block)
251
+ block ||= default_block(:save)
252
+ options.merge!(method: [:put, :patch])
253
+ is(_path, options, &block)
254
+ end
206
255
 
207
- def update(options={}, &block)
208
- block ||= default_block(:save)
209
- options.merge!(method: [:put, :patch])
210
- is(_path, options, &block)
211
- end
256
+ def destroy(options={}, &block)
257
+ block ||= default_block(:delete)
258
+ delete(_path, options) do
259
+ response.status = 204
260
+ block.call(*captures) if block
261
+ end
262
+ end
212
263
 
213
- def destroy(options={}, &block)
214
- block ||= default_block(:delete)
215
- delete(_path, options) do
216
- response.status = 204
217
- block.call(*captures) if block
218
- end
219
- end
264
+ def edit(options={}, &block)
265
+ block ||= default_block(:one)
266
+ get(_path('edit'), options, &block)
267
+ end
220
268
 
221
- def edit(options={}, &block)
222
- block ||= default_block(:one)
223
- get(_path("edit"), options, &block)
224
- end
269
+ def new(options={}, &block)
270
+ block ||= ->{@resource.perform(:one, "new")}
271
+ get('new', options, &block)
272
+ end
273
+
274
+ private
275
+
276
+ def extract_resource_options(options)
277
+ @resource_options ||= []
278
+ opts = {}
279
+ (Resource::OPTIONS.keys & options.keys).each do |key|
280
+ opts[key] = options.delete(key)
281
+ end
282
+ @resource_options << opts
283
+ end
284
+
285
+ def _path(path=nil)
286
+ if @resource and @resource.singleton
287
+ path = ['', true] unless path
288
+ else
289
+ path = [':id', path].compact.join("/")
290
+ end
291
+ path
292
+ end
293
+
294
+ def default_block(method)
295
+ if @resource.singleton
296
+ ->(){@resource.perform(method)}
297
+ else
298
+ ->(id){@resource.perform(method, id)}
299
+ end
300
+ end
301
+
302
+ CONTENT_TYPE = 'Content-Type'.freeze
225
303
 
226
- def new(options={}, &block)
227
- block ||= ->{@resource.perform(:one, "new")}
228
- get("new", options, &block)
229
- end
230
-
231
- private
232
-
233
- def _path(path=nil)
234
- if @resource and @resource.singleton
235
- path = ["", true] unless path
236
- else
237
- path = [":d", path].compact.join("/")
238
- end
239
- path
240
- end
241
-
242
- def default_block(method)
243
- if @resource.singleton
244
- ->(){@resource.perform(method)}
245
- else
246
- ->(id){@resource.perform(method, id)}
247
- end
248
- end
249
-
250
- CONTENT_TYPE = 'Content-Type'.freeze
304
+ def block_result_body(result)
305
+ if result && @resource
306
+ response[CONTENT_TYPE] = @resource.content_type
307
+ @resource.serialize.call(result)
308
+ else
309
+ super
310
+ end
311
+ end
312
+
313
+ end
314
+ end
315
+
316
+ register_plugin(:rest_api, RestApi)
251
317
 
252
- def block_result_body(result)
253
- if result && @resource
254
- response[CONTENT_TYPE] = @resource.content_type
255
- @resource.serialize.call(result)
256
- else
257
- super
258
- end
259
- end
260
-
261
- end
262
- end
263
-
264
- register_plugin(:rest_api, RestApi)
265
-
266
- end
318
+ end
267
319
  end