bulk_api 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +14 -0
- data/MIT-LICENSE +20 -0
- data/README.markdown +319 -0
- data/Rakefile +25 -0
- data/app/controllers/bulk/api_controller.rb +25 -0
- data/lib/bulk/abstract_collection.rb +55 -0
- data/lib/bulk/collection.rb +50 -0
- data/lib/bulk/engine.rb +13 -0
- data/lib/bulk/resource.rb +277 -0
- data/lib/bulk/routes.rb +14 -0
- data/lib/bulk/sproutcore.rb +19 -0
- data/lib/bulk_api.rb +5 -0
- data/lib/generators/bulk/install/install_generator.rb +28 -0
- data/lib/generators/bulk/install/templates/app/bulk/application_resource.rb +9 -0
- data/lib/generators/bulk/install/templates/config/initializers/bulk_api.rb +3 -0
- data/lib/generators/bulk/resource/resource_generator.rb +13 -0
- data/lib/generators/bulk/resource/templates/resource.rb +2 -0
- metadata +93 -0
data/Gemfile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
|
3
|
+
gemspec
|
4
|
+
|
5
|
+
gem "sqlite3"
|
6
|
+
|
7
|
+
gem "rspec-rails"
|
8
|
+
gem "rack-test"
|
9
|
+
|
10
|
+
gem "generator_spec"
|
11
|
+
|
12
|
+
# To use debugger (ruby-debug for Ruby 1.8.7+, ruby-debug19 for Ruby 1.9.2+)
|
13
|
+
# gem 'ruby-debug'
|
14
|
+
# gem 'ruby-debug19'
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2010 Strobe Inc.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,319 @@
|
|
1
|
+
# Bulk Rails API
|
2
|
+
|
3
|
+
Bulk Rails API plugin makes integrating Sproutcore applications with Rails applications dead simple. It handles all the communication and allows to take advantage of bulk operations, which can make your application much faster. To use that plugin you will also need BulkDataSource, which will handle Sproutcore side of communcation.
|
4
|
+
|
5
|
+
## Installing:
|
6
|
+
|
7
|
+
### Rails app:
|
8
|
+
|
9
|
+
Add this line to Gemfile and run bundle install:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'bulk_api'
|
13
|
+
```
|
14
|
+
|
15
|
+
To set up Bulk API in your Rails app:
|
16
|
+
|
17
|
+
```
|
18
|
+
rails generate bulk:install
|
19
|
+
```
|
20
|
+
|
21
|
+
Now you need to configure it in your application. First thing to do is to use it with your store:
|
22
|
+
|
23
|
+
```javascript
|
24
|
+
YourApp = SC.Application.create({
|
25
|
+
store: SC.Store.create().from('SC.BulkDataSource')
|
26
|
+
});
|
27
|
+
```
|
28
|
+
|
29
|
+
The last thing that you need to do is to set resource names for your models. BulkDataSource assumes that you have resourceName attribute set on your record. If you don't have such attributes, you can add it like that:
|
30
|
+
|
31
|
+
```javascript
|
32
|
+
Todos.Todo = SC.Record.extend({
|
33
|
+
resourceName: 'todo'
|
34
|
+
})
|
35
|
+
```
|
36
|
+
|
37
|
+
## Usage
|
38
|
+
|
39
|
+
By default Bulk Api plugin handles all of the models, in production you will probably want to filter it:
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
# app/bulk/abstract_resource.rb
|
43
|
+
class ApplicationResource < Bulk::Resource
|
44
|
+
resources :tasks, :projects
|
45
|
+
end
|
46
|
+
```
|
47
|
+
|
48
|
+
If you don't have any specific needs like authentication or authorization, you're good to go with such simple configuration. In other cases you will need to do a bit more to integrate your application.
|
49
|
+
|
50
|
+
Bulk API approach is a bit different than standard REST APIs that you're probably used to, thus it needs to be handled differently. The point of using bulk API is to cut the requests number - it can handle many records (and many types of records) with one request. If you want to read more about how the API looks from HTTP point of view, please scroll to "HTTP Api" section. For now let's focus on what you need to know to implement it in ruby.
|
51
|
+
|
52
|
+
When using bulk api you can handle things on 3 levels:
|
53
|
+
1) All records level
|
54
|
+
2) Particular record type level
|
55
|
+
3) Individual record level
|
56
|
+
|
57
|
+
Let's see how to handle things on all of the 3 levels to add your own logic (like authentication or authorization).
|
58
|
+
|
59
|
+
If some of your logic is common for all the record types, you can use ApplicationResource that lives in app/bulk/abstract_resource.rb. This is base class for all of the resources, just like ApplicationController is a base class for all of your controllers (this may not be true for some applications, but let's agree that's the most common scenario). To allow easy integration with application, you have access to several application objects in ApplicationResource and its subclasses:
|
60
|
+
|
61
|
+
* session
|
62
|
+
* controller
|
63
|
+
* params
|
64
|
+
|
65
|
+
The methods used for records manipulation are:
|
66
|
+
|
67
|
+
* get
|
68
|
+
* create
|
69
|
+
* update
|
70
|
+
* delete
|
71
|
+
|
72
|
+
### Authentication callbacks
|
73
|
+
|
74
|
+
There are 3 kind of authorization callbacks that you can use. Each of them represents differnet level of handling records:
|
75
|
+
|
76
|
+
* authenticate(action) - that callback is executed before handling the request, if it returns false, the entire response gets 401 status
|
77
|
+
* authenticate_records(action, klass) - this callback is run before handling each type of resource, if it returns false, `not_authenticated` error is added to all of the records from given resource type. The class of the resource is passed as an argument.
|
78
|
+
* authenticate_record(action, record) - this callback is run for each of the records, if it returns false, `not_authenticated` error is added to the given record
|
79
|
+
|
80
|
+
Let's see example usage of each of this callbacks types:
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
class ApplicationResource < Bulk::Resource
|
84
|
+
# delegate all the things that we need from controller
|
85
|
+
delegate current_user, :can?, :to => :controller
|
86
|
+
|
87
|
+
def authenticate(action)
|
88
|
+
current_user.logged_in?
|
89
|
+
end
|
90
|
+
|
91
|
+
def authenticate_records(action, klass)
|
92
|
+
end
|
93
|
+
|
94
|
+
def authenticate_record(action, record)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
```
|
98
|
+
|
99
|
+
### Authorization callbacks
|
100
|
+
|
101
|
+
Authorization callbacks are very similar to authentication
|
102
|
+
callbacks. Notice that authorization callbacks will only be run when
|
103
|
+
authentication callback succeeds.
|
104
|
+
|
105
|
+
* authorize(action) - that callback is executed before handling the request, if it returns false, the entire response gets 403 status
|
106
|
+
* authorize_records(action, klass) - this callback is run before handling each type of resource, if it returns false, 403 error is added to all of the records from given resource type. The class of the resource is passed as an argument.
|
107
|
+
* authorize_record(action, record) - this callback is run for each of the records, if it returns false, 403 error is added to the given record
|
108
|
+
|
109
|
+
Let's see example usage of each of this callbacks types.
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
class ApplicationResource < Bulk::Resource
|
113
|
+
# delegate all the things that we need from controller
|
114
|
+
delegate current_user, :can?, :to => :controller
|
115
|
+
|
116
|
+
def authorize(action)
|
117
|
+
current_user.is_admin?
|
118
|
+
end
|
119
|
+
|
120
|
+
def authorize_records(action, klass)
|
121
|
+
# klass can be for example Project
|
122
|
+
if action == :update
|
123
|
+
can? :update, klass
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def authorize_record(action, record)
|
128
|
+
can? action, record # action returns one of the 4 actions (get, create, update, delete),
|
129
|
+
# so this will check if user can perform given type of action on
|
130
|
+
# the record
|
131
|
+
end
|
132
|
+
end
|
133
|
+
```
|
134
|
+
|
135
|
+
### Params filtering
|
136
|
+
|
137
|
+
While preparing your API, you will probably need to filter parameters
|
138
|
+
that user can set on your models. The easiest way to do it is to set
|
139
|
+
params_accessible or params_protected callbacks:
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
class ApplicationResource < Bulk::Resource
|
143
|
+
def params_accessible(klass)
|
144
|
+
{ :tasks => [:title, :done],
|
145
|
+
:projects => [:name] }
|
146
|
+
end
|
147
|
+
|
148
|
+
# or:
|
149
|
+
|
150
|
+
def params_protected(klass)
|
151
|
+
{ :tasks => [:created_at, :updated_at] }
|
152
|
+
end
|
153
|
+
end
|
154
|
+
```
|
155
|
+
|
156
|
+
You can also set it for individual resource classes. In such case this
|
157
|
+
will overwrite the one that's set in ApplicationResource.
|
158
|
+
|
159
|
+
### Attributes filtering
|
160
|
+
|
161
|
+
If you want to filter the attributes that are sent in a response, the
|
162
|
+
easiest way to do it is to use standard Rails mechanism for that -
|
163
|
+
override as_json method in your model:
|
164
|
+
|
165
|
+
```ruby
|
166
|
+
class MyModel < ActiveRecord::Base
|
167
|
+
def as_json(options={})
|
168
|
+
super(:only => [:email, :avatar], :include =>[:addresses])
|
169
|
+
end
|
170
|
+
end
|
171
|
+
```
|
172
|
+
|
173
|
+
With some applications that's not enough, though. If you have several
|
174
|
+
user roles, the chances are that you will need to differentiate
|
175
|
+
responses based on user rights. In that case you can use as_json
|
176
|
+
callback. Value returned from that callback will be passed to the
|
177
|
+
record's as_json method:
|
178
|
+
|
179
|
+
```ruby
|
180
|
+
class ApplicationResource < Bulk::Resource
|
181
|
+
def as_json(record)
|
182
|
+
# return hash that will be passed to record's as_json
|
183
|
+
{ :only => [:email] }
|
184
|
+
end
|
185
|
+
end
|
186
|
+
```
|
187
|
+
|
188
|
+
You can also override that method in individual resource classes.
|
189
|
+
|
190
|
+
### Specific resource classes
|
191
|
+
|
192
|
+
Sometimes you may want to implement specific application logic to one of the resources. Or you don't want to end up with Switch Driven Development in one of you authenticate callbacs. In such cases, the easiest way to handle resource specific code is to create an ApplicationResouce subclass that you can use to override standard behavior. There is a generator to make things easy for you:
|
193
|
+
|
194
|
+
```
|
195
|
+
rails g bulk:resource task
|
196
|
+
```
|
197
|
+
|
198
|
+
This will create the following file:
|
199
|
+
|
200
|
+
```ruby
|
201
|
+
# app/bulk/task_resource.rb
|
202
|
+
class TaskResource < ApplicationResource
|
203
|
+
end
|
204
|
+
```
|
205
|
+
|
206
|
+
If you do nothing else, Rails will automatically return records by retrieving them using ActiveRecord. You can customize any of the default behavior by overriding methods on the resource class. As you already now the main methods that are used to fetch and modify records are: get, create, update, delete. Let's see how you can override those methods:
|
207
|
+
|
208
|
+
```ruby
|
209
|
+
# app/resources/task_resource.rb
|
210
|
+
class TaskResource < Sproutcore::Resource
|
211
|
+
|
212
|
+
def get(ids)
|
213
|
+
# ids is an array with records that we need to fetch
|
214
|
+
|
215
|
+
collection = super(ids)
|
216
|
+
|
217
|
+
# collection is an instance of Bulk::Collection class that keeps
|
218
|
+
# fetched records, please check the rest of the README and the docs
|
219
|
+
# to see how you can manipulate records in collection
|
220
|
+
collection
|
221
|
+
end
|
222
|
+
|
223
|
+
def create(records)
|
224
|
+
# records is an array of hashes with data that will be used
|
225
|
+
# to create new records e.g.:
|
226
|
+
# [{:title => "First", :done => false, :_local_id => 1},
|
227
|
+
# {:title => "", :done => true, :_local_id => 3}]
|
228
|
+
# _local_id is needed to identify the records in sproutcore
|
229
|
+
# application, since they do not have an id yet
|
230
|
+
|
231
|
+
collection = super(records)
|
232
|
+
|
233
|
+
collection
|
234
|
+
end
|
235
|
+
|
236
|
+
def update(records)
|
237
|
+
# records array is very similar to the array from create method,
|
238
|
+
# but this time we should get data with id, like:
|
239
|
+
# [{:id => 1, :title => "First (changed!)", :done => false},
|
240
|
+
# {:id => 2, :title => ""}]
|
241
|
+
|
242
|
+
collection = super(records)
|
243
|
+
|
244
|
+
collection
|
245
|
+
end
|
246
|
+
|
247
|
+
def delete(ids)
|
248
|
+
# similarly to get method, we get array of ids to delete
|
249
|
+
|
250
|
+
collection = super(ids)
|
251
|
+
|
252
|
+
collection
|
253
|
+
end
|
254
|
+
end
|
255
|
+
```
|
256
|
+
|
257
|
+
While overriding records you can use super to handle the actions with default behavior or reimplement them yourself. In latter case you just need to make sure that you properly construct collection.
|
258
|
+
|
259
|
+
### Bulk::Collection
|
260
|
+
|
261
|
+
Bulk::Collection is a container for records and is used to construct response from. It has a few handy methods to easily modify collection, for more please refer to documantation.
|
262
|
+
|
263
|
+
```ruby
|
264
|
+
collection = Bulk::Colection.new
|
265
|
+
collection.set(1, record) # add record with identifier '1', identifier is then used while constructing response
|
266
|
+
# most of the time it's id or _local_id (the latter one is mainly for create)
|
267
|
+
|
268
|
+
collection.errors.set(1, :access_denied) # add error that will be passed to DataSource
|
269
|
+
# notice that these errors are not the same as validation errors,
|
270
|
+
# this is more general way to tell DataSource what's going on
|
271
|
+
collection.delete(1, record) # remove the record
|
272
|
+
```
|
273
|
+
|
274
|
+
### Advanced usage
|
275
|
+
|
276
|
+
If you want to change class or resource name that will be send, you can use resource_class and resource_name methods:
|
277
|
+
|
278
|
+
```ruby
|
279
|
+
class TaskResource < Sproutcore::Resource
|
280
|
+
resource_class Todo
|
281
|
+
resource_name 'todo'
|
282
|
+
end
|
283
|
+
```
|
284
|
+
|
285
|
+
## Http Bulk API
|
286
|
+
|
287
|
+
The point of using bulk API is to cut the requests number. Because of its nature it can't be handled efficiently using standard REST API. The bulk API is designed to handle many records and record types in one request. Let's look how does GET request can look like with bulk API;
|
288
|
+
|
289
|
+
```
|
290
|
+
POST /bulk/api
|
291
|
+
{
|
292
|
+
'todos': [
|
293
|
+
{'title': "First todo", 'done': false, '_storeKey': '3'},
|
294
|
+
{'title': "Second todo", 'done': true, '_storeKey': '10'}
|
295
|
+
],
|
296
|
+
'projects': [
|
297
|
+
{'name': "Sproutcore todolist", '_storeKey': '12'}
|
298
|
+
]
|
299
|
+
}
|
300
|
+
```
|
301
|
+
|
302
|
+
As you can see we POST some new items to our application. Rails application will then respond with list of created records:
|
303
|
+
|
304
|
+
```
|
305
|
+
{
|
306
|
+
'todos': [
|
307
|
+
{'id': 1, 'title': "First todo", 'done': false, '_storeKey': '3'}
|
308
|
+
],
|
309
|
+
'projects': [
|
310
|
+
{'id': 1, 'name': "Sproutcore todolist", '_storeKey': '12'}
|
311
|
+
]
|
312
|
+
'errors': {
|
313
|
+
'todos': {
|
314
|
+
}
|
315
|
+
}
|
316
|
+
}
|
317
|
+
```
|
318
|
+
|
319
|
+
As you can see, all the records that were created have id attached now and there is additional attribute 'errors' that tells us what went wrong during validation.
|
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require 'rubygems'
|
3
|
+
begin
|
4
|
+
require 'bundler/setup'
|
5
|
+
rescue LoadError
|
6
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'rake'
|
10
|
+
require 'rake/rdoctask'
|
11
|
+
|
12
|
+
require 'rspec/core'
|
13
|
+
require 'rspec/core/rake_task'
|
14
|
+
|
15
|
+
RSpec::Core::RakeTask.new(:spec)
|
16
|
+
|
17
|
+
task :default => :spec
|
18
|
+
|
19
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
20
|
+
rdoc.rdoc_dir = 'rdoc'
|
21
|
+
rdoc.title = 'BulkApi'
|
22
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
23
|
+
rdoc.rdoc_files.include('README.rdoc')
|
24
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class Bulk::ApiController < ActionController::Base
|
2
|
+
def get
|
3
|
+
options = Bulk::Resource.get(self)
|
4
|
+
yield options if block_given?
|
5
|
+
render options
|
6
|
+
end
|
7
|
+
|
8
|
+
def create
|
9
|
+
options = Bulk::Resource.create(self)
|
10
|
+
yield options if block_given?
|
11
|
+
render options
|
12
|
+
end
|
13
|
+
|
14
|
+
def update
|
15
|
+
options = Bulk::Resource.update(self)
|
16
|
+
yield options if block_given?
|
17
|
+
render options
|
18
|
+
end
|
19
|
+
|
20
|
+
def delete
|
21
|
+
options = Bulk::Resource.delete(self)
|
22
|
+
yield options if block_given?
|
23
|
+
render options
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Bulk
|
2
|
+
class AbstractCollection
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@items = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
# Clear items
|
10
|
+
def clear
|
11
|
+
items.clear
|
12
|
+
end
|
13
|
+
|
14
|
+
# Get record with given id
|
15
|
+
def get(id)
|
16
|
+
items[id.to_s]
|
17
|
+
end
|
18
|
+
alias_method :exists?, :get
|
19
|
+
|
20
|
+
# Set record for a given id
|
21
|
+
def set(id, item)
|
22
|
+
items[id.to_s] = item
|
23
|
+
end
|
24
|
+
|
25
|
+
# Remove record from collection
|
26
|
+
def delete(id)
|
27
|
+
items.delete(id.to_s)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Get the collection length
|
31
|
+
def length
|
32
|
+
items.length
|
33
|
+
end
|
34
|
+
|
35
|
+
# Clear records on collection
|
36
|
+
def clear
|
37
|
+
items.clear
|
38
|
+
end
|
39
|
+
|
40
|
+
# Checks if collection is empty
|
41
|
+
def empty?
|
42
|
+
items.empty?
|
43
|
+
end
|
44
|
+
|
45
|
+
# Return items ids
|
46
|
+
def ids
|
47
|
+
items.keys
|
48
|
+
end
|
49
|
+
|
50
|
+
delegate :each, :to => :items
|
51
|
+
|
52
|
+
private
|
53
|
+
attr_reader :items
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'active_support/core_ext/module/delegation'
|
2
|
+
require 'bulk/abstract_collection'
|
3
|
+
|
4
|
+
module Bulk
|
5
|
+
class Collection < AbstractCollection
|
6
|
+
class Error < Struct.new(:type, :data)
|
7
|
+
def to_hash
|
8
|
+
h = {:type => type}
|
9
|
+
h[:data] = data if data
|
10
|
+
h
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class Errors < AbstractCollection
|
15
|
+
attr_reader :collection
|
16
|
+
|
17
|
+
def initialize(collection)
|
18
|
+
@collection = collection
|
19
|
+
super()
|
20
|
+
end
|
21
|
+
|
22
|
+
def set(id, error, data = nil)
|
23
|
+
super(id, Error.new(error, data))
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns errors for the records
|
28
|
+
def errors
|
29
|
+
@errors ||= Errors.new(self)
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_hash(name, options = {})
|
33
|
+
only_ids = options[:only_ids]
|
34
|
+
response = {}
|
35
|
+
|
36
|
+
each do |id, record|
|
37
|
+
next if errors.get(id)
|
38
|
+
response[name] ||= []
|
39
|
+
response[name] << (only_ids ? record.id : record.as_json(options[:as_json]) )
|
40
|
+
end
|
41
|
+
|
42
|
+
errors.each do |id, error|
|
43
|
+
response[:errors] ||= {name => {}}
|
44
|
+
response[:errors][name][id] = error.to_hash
|
45
|
+
end
|
46
|
+
|
47
|
+
response
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/bulk/engine.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'bulk/routes'
|
2
|
+
|
3
|
+
module Bulk
|
4
|
+
class Engine < Rails::Engine
|
5
|
+
initializer "do not include root in json" do
|
6
|
+
# TODO: handle that nicely
|
7
|
+
ActiveRecord::Base.include_root_in_json = false
|
8
|
+
end
|
9
|
+
|
10
|
+
config.paths.add "app/bulk", :eager_load => true
|
11
|
+
config.paths.add "app/sproutcore"
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,277 @@
|
|
1
|
+
module Bulk
|
2
|
+
class AuthenticationError < StandardError; end
|
3
|
+
class AuthorizationError < StandardError; end
|
4
|
+
|
5
|
+
class Resource
|
6
|
+
|
7
|
+
attr_reader :controller
|
8
|
+
delegate :session, :params, :to => :controller
|
9
|
+
delegate :resource_class, :to => "self.class"
|
10
|
+
@@resources = []
|
11
|
+
|
12
|
+
class << self
|
13
|
+
attr_writer :application_resource_class
|
14
|
+
attr_reader :abstract
|
15
|
+
alias_method :abstract?, :abstract
|
16
|
+
|
17
|
+
def resource_class(klass = nil)
|
18
|
+
@resource_class = klass if klass
|
19
|
+
@resource_class
|
20
|
+
end
|
21
|
+
|
22
|
+
def resource_name(name = nil)
|
23
|
+
@resource_name = name if name
|
24
|
+
@resource_name
|
25
|
+
end
|
26
|
+
|
27
|
+
def resources(*resources)
|
28
|
+
@@resources = resources unless resources.blank?
|
29
|
+
@@resources
|
30
|
+
end
|
31
|
+
|
32
|
+
def application_resource_class
|
33
|
+
@application_resource_class ||= ApplicationResource
|
34
|
+
@application_resource_class.is_a?(Class) ? @application_resource_class : Object.const_get(@application_resource_class.to_sym)
|
35
|
+
end
|
36
|
+
|
37
|
+
def inherited(base)
|
38
|
+
if base.name == application_resource_class.to_s
|
39
|
+
base.abstract!
|
40
|
+
elsif base.name =~ /(.*)Resource$/
|
41
|
+
base.resource_name($1.underscore.singularize)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
%w/get create update delete/.each do |method|
|
46
|
+
define_method(method) do |controller|
|
47
|
+
handle_response(method, controller)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def abstract!
|
52
|
+
@abstract = true
|
53
|
+
@@resources = []
|
54
|
+
end
|
55
|
+
protected :abstract!
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
# TODO: refactor this to some kind of Response class
|
60
|
+
def handle_response(method, controller)
|
61
|
+
response = {}
|
62
|
+
application_resource = application_resource_class.new(controller, :abstract => true)
|
63
|
+
|
64
|
+
if application_resource.respond_to?(:authenticate)
|
65
|
+
raise AuthenticationError unless application_resource.authenticate(method)
|
66
|
+
end
|
67
|
+
|
68
|
+
if application_resource.respond_to?(:authorize)
|
69
|
+
raise AuthorizationError unless application_resource.authorize(method)
|
70
|
+
end
|
71
|
+
|
72
|
+
controller.params.each do |resource, hash|
|
73
|
+
next unless resources.blank? || resources.include?(resource.to_sym)
|
74
|
+
resource_object = instantiate_resource_class(controller, resource)
|
75
|
+
next unless resource_object
|
76
|
+
collection = resource_object.send(method, hash)
|
77
|
+
as_json = resource_object.send(:as_json, resource_object.send(:klass))
|
78
|
+
options = {:only_ids => (method == 'delete'), :as_json => as_json}
|
79
|
+
response.deep_merge! collection.to_hash(resource_object.plural_resource_name.to_sym, options)
|
80
|
+
end
|
81
|
+
|
82
|
+
{ :json => response }
|
83
|
+
rescue AuthenticationError
|
84
|
+
{ :status => 401 }
|
85
|
+
rescue AuthorizationError
|
86
|
+
{ :status => 403 }
|
87
|
+
end
|
88
|
+
|
89
|
+
def instantiate_resource_class(controller, resource)
|
90
|
+
begin
|
91
|
+
"#{resource.to_s.singularize}_resource".classify.constantize.new(controller)
|
92
|
+
rescue NameError
|
93
|
+
begin
|
94
|
+
application_resource_class.new(controller, :resource_name => resource)
|
95
|
+
rescue NameError
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def initialize(controller, options = {})
|
102
|
+
@controller = controller
|
103
|
+
@resource_name = options[:resource_name].to_s if options[:resource_name]
|
104
|
+
|
105
|
+
# try to get klass to raise error early if something is not ok
|
106
|
+
klass unless options[:abstract]
|
107
|
+
end
|
108
|
+
|
109
|
+
def get(ids = 'all')
|
110
|
+
all = ids.to_s == 'all'
|
111
|
+
collection = Collection.new
|
112
|
+
with_records_auth :get, collection, (all ? nil : ids) do
|
113
|
+
records = all ? klass.all : klass.where(:id => ids)
|
114
|
+
records.each do |r|
|
115
|
+
with_record_auth :get, collection, r.id, r do
|
116
|
+
collection.set(r.id, r)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
collection
|
121
|
+
end
|
122
|
+
|
123
|
+
def create(hashes)
|
124
|
+
collection = Collection.new
|
125
|
+
ids = hashes.map { |r| r[:_local_id] }
|
126
|
+
with_records_auth :create, collection, ids do
|
127
|
+
hashes.each do |attrs|
|
128
|
+
local_id = attrs.delete(:_local_id)
|
129
|
+
record = klass.new(filter_params(attrs))
|
130
|
+
record[:_local_id] = local_id
|
131
|
+
with_record_auth :create, collection, local_id, record do
|
132
|
+
record.save
|
133
|
+
set_with_validity_check(collection, local_id, record)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
collection
|
138
|
+
end
|
139
|
+
|
140
|
+
def update(hashes)
|
141
|
+
collection = Collection.new
|
142
|
+
ids = hashes.map { |r| r[:id] }
|
143
|
+
with_records_auth :update, collection, ids do
|
144
|
+
hashes.each do |attrs|
|
145
|
+
attrs.delete(:_local_id)
|
146
|
+
record = klass.where(:id => attrs[:id]).first
|
147
|
+
with_record_auth :update, collection, record.id, record do
|
148
|
+
record.update_attributes(filter_params(attrs))
|
149
|
+
set_with_validity_check(collection, record.id, record)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
collection
|
154
|
+
end
|
155
|
+
|
156
|
+
def delete(ids)
|
157
|
+
collection = Collection.new
|
158
|
+
with_records_auth :delete, collection, ids do
|
159
|
+
ids.each do |id|
|
160
|
+
record = klass.where(:id => id).first
|
161
|
+
with_record_auth :delete, collection, record.id, record do
|
162
|
+
record.destroy
|
163
|
+
set_with_validity_check(collection, record.id, record)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
collection
|
168
|
+
end
|
169
|
+
|
170
|
+
def plural_resource_name
|
171
|
+
resource_name.to_s.pluralize
|
172
|
+
end
|
173
|
+
|
174
|
+
def resource_name
|
175
|
+
@resource_name || self.class.resource_name
|
176
|
+
end
|
177
|
+
|
178
|
+
def as_json(klass)
|
179
|
+
{}
|
180
|
+
end
|
181
|
+
|
182
|
+
private
|
183
|
+
delegate :abstract?, :to => "self.class"
|
184
|
+
|
185
|
+
def with_record_auth(action, collection, id, record, &block)
|
186
|
+
with_record_authentication(action, collection, id, record) do
|
187
|
+
with_record_authorization(action, collection, id, record, &block)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def with_records_auth(action, collection, ids, &block)
|
192
|
+
with_records_authentication(action, collection, ids) do
|
193
|
+
with_records_authorization(action, collection, ids, &block)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def with_record_authentication(action, collection, id, record)
|
198
|
+
authenticated = self.respond_to?(:authenticate_record) ? authenticate_record(action, record) : true
|
199
|
+
if authenticated
|
200
|
+
yield
|
201
|
+
else
|
202
|
+
collection.errors.set(id, 'not_authenticated')
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def with_record_authorization(action, collection, id, record)
|
207
|
+
authorized = self.respond_to?(:authorize_record) ? authorize_record(action, record) : true
|
208
|
+
if authorized
|
209
|
+
yield
|
210
|
+
else
|
211
|
+
collection.errors.set(id, 'forbidden')
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def with_records_authentication(action, collection, ids)
|
216
|
+
authenticated = self.respond_to?(:authenticate_records) ? authenticate_records(action, klass) : true
|
217
|
+
if authenticated
|
218
|
+
yield
|
219
|
+
else
|
220
|
+
ids.each do |id|
|
221
|
+
collection.errors.set(id, 'not_authenticated')
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
def with_records_authorization(action, collection, ids)
|
227
|
+
authorized = self.respond_to?(:authorize_records) ? authorize_records(action, klass) : true
|
228
|
+
if authorized
|
229
|
+
yield
|
230
|
+
else
|
231
|
+
ids.each do |id|
|
232
|
+
collection.errors.set(id, 'forbidden')
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
def set_with_validity_check(collection, id, record)
|
238
|
+
collection.set(id, record)
|
239
|
+
unless record.errors.empty?
|
240
|
+
collection.errors.set(id, :invalid, record.errors.to_hash)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
def filter_params(attributes)
|
245
|
+
if self.respond_to?(:params_accessible)
|
246
|
+
filter_params_for(:accessible, attributes)
|
247
|
+
elsif self.respond_to?(:params_protected)
|
248
|
+
filter_params_for(:protected, attributes)
|
249
|
+
else
|
250
|
+
attributes
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
def filter_params_for(type, attributes)
|
255
|
+
filter = send("params_#{type}", klass)
|
256
|
+
filter = filter ? filter[resource_name.to_sym] : nil
|
257
|
+
|
258
|
+
if filter
|
259
|
+
attributes.delete_if do |k, v|
|
260
|
+
delete_if = filter.include?(k)
|
261
|
+
type == :accessible ? !delete_if : delete_if
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
attributes
|
266
|
+
end
|
267
|
+
|
268
|
+
def klass
|
269
|
+
@_klass ||= begin
|
270
|
+
resource_class || (resource_name ? resource_name.to_s.singularize.classify.constantize : nil) ||
|
271
|
+
raise("Could not get resource class, please either set resource_class or resource_name that matches model that you want to use")
|
272
|
+
rescue NameError
|
273
|
+
raise NameError.new("Could not find class matching your resource_name (#{resource_name} - we were looking for #{resource_name.classify})")
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
data/lib/bulk/routes.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
module ActionDispatch::Routing
|
2
|
+
module BulkHelpers
|
3
|
+
def bulk_routes(path)
|
4
|
+
get path => "bulk/api#get"
|
5
|
+
post path => "bulk/api#create"
|
6
|
+
put path => "bulk/api#update"
|
7
|
+
delete path => "bulk/api#delete"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class Mapper
|
12
|
+
include BulkHelpers
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'sproutcore'
|
2
|
+
require 'sproutcore/rack/service'
|
3
|
+
require 'sproutcore/models/project'
|
4
|
+
|
5
|
+
module Bulk
|
6
|
+
class Sproutcore
|
7
|
+
def sproutcore
|
8
|
+
@sproutcore ||= begin
|
9
|
+
project = SC::Project.load Rails.application.paths["app/sproutcore"].first, :parent => SC.builtin_project
|
10
|
+
SC::Rack::Service.new(project)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(env)
|
15
|
+
env["PATH_INFO"] = "/" if env["PATH_INFO"].blank?
|
16
|
+
sproutcore.call(env)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/bulk_api.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
module Bulk
|
2
|
+
module Generators
|
3
|
+
class InstallGenerator < Rails::Generators::Base
|
4
|
+
|
5
|
+
desc <<DESC
|
6
|
+
Description:
|
7
|
+
Creates initializer with configuration and adds required routes
|
8
|
+
DESC
|
9
|
+
|
10
|
+
def self.source_root
|
11
|
+
@source_root ||= File.expand_path(File.join(File.dirname(__FILE__), 'templates'))
|
12
|
+
end
|
13
|
+
|
14
|
+
def routes_entry
|
15
|
+
route 'bulk_routes "/api/bulk"'
|
16
|
+
route 'mount Bulk::Sproutcore.new => "/_sproutcore"'
|
17
|
+
end
|
18
|
+
|
19
|
+
def copy_app_bulk_application_resource
|
20
|
+
template 'app/bulk/application_resource.rb'
|
21
|
+
end
|
22
|
+
|
23
|
+
def copy_initializers_bulk_api
|
24
|
+
template "config/initializers/bulk_api.rb"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Bulk
|
2
|
+
module Generators
|
3
|
+
class ResourceGenerator < Rails::Generators::NamedBase
|
4
|
+
source_root File.expand_path("../templates", __FILE__)
|
5
|
+
|
6
|
+
check_class_collision :suffix => "Resource"
|
7
|
+
|
8
|
+
def generate_part_class
|
9
|
+
template "resource.rb", "app/bulk/#{file_name}_resource.rb"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
metadata
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bulk_api
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.1
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Piotr Sarnacki
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-05-08 00:00:00 +02:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: rails
|
18
|
+
prerelease: false
|
19
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
20
|
+
none: false
|
21
|
+
requirements:
|
22
|
+
- - ~>
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: "3.0"
|
25
|
+
type: :runtime
|
26
|
+
version_requirements: *id001
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: sproutcore
|
29
|
+
prerelease: false
|
30
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
31
|
+
none: false
|
32
|
+
requirements:
|
33
|
+
- - ~>
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: "1.5"
|
36
|
+
type: :runtime
|
37
|
+
version_requirements: *id002
|
38
|
+
description: Easy integration of rails apps with sproutcore.
|
39
|
+
email:
|
40
|
+
executables: []
|
41
|
+
|
42
|
+
extensions: []
|
43
|
+
|
44
|
+
extra_rdoc_files: []
|
45
|
+
|
46
|
+
files:
|
47
|
+
- app/controllers/bulk/api_controller.rb
|
48
|
+
- lib/bulk/abstract_collection.rb
|
49
|
+
- lib/bulk/collection.rb
|
50
|
+
- lib/bulk/engine.rb
|
51
|
+
- lib/bulk/resource.rb
|
52
|
+
- lib/bulk/routes.rb
|
53
|
+
- lib/bulk/sproutcore.rb
|
54
|
+
- lib/bulk_api.rb
|
55
|
+
- lib/generators/bulk/install/install_generator.rb
|
56
|
+
- lib/generators/bulk/install/templates/app/bulk/application_resource.rb
|
57
|
+
- lib/generators/bulk/install/templates/config/initializers/bulk_api.rb
|
58
|
+
- lib/generators/bulk/resource/resource_generator.rb
|
59
|
+
- lib/generators/bulk/resource/templates/resource.rb
|
60
|
+
- MIT-LICENSE
|
61
|
+
- Rakefile
|
62
|
+
- Gemfile
|
63
|
+
- README.markdown
|
64
|
+
has_rdoc: true
|
65
|
+
homepage:
|
66
|
+
licenses: []
|
67
|
+
|
68
|
+
post_install_message:
|
69
|
+
rdoc_options: []
|
70
|
+
|
71
|
+
require_paths:
|
72
|
+
- lib
|
73
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: "0"
|
79
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
80
|
+
none: false
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: "0"
|
85
|
+
requirements: []
|
86
|
+
|
87
|
+
rubyforge_project:
|
88
|
+
rubygems_version: 1.5.2
|
89
|
+
signing_key:
|
90
|
+
specification_version: 3
|
91
|
+
summary: Easy integration of rails apps with sproutcore.
|
92
|
+
test_files: []
|
93
|
+
|