roda-rest_api 1.4.5 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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