bulk_api 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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
@@ -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,5 @@
1
+ module Bulk
2
+ require 'bulk/collection'
3
+ require 'bulk/resource'
4
+ require 'bulk/engine'
5
+ end
@@ -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,9 @@
1
+ class ApplicationResource < Bulk::Resource
2
+ # In production you should filter resources that you want to handle.
3
+ # To do so, just uncomment the next line and set resources that should be
4
+ # available in Bulk API:
5
+ #
6
+ # resources :tasks, :projects
7
+
8
+ end
9
+
@@ -0,0 +1,3 @@
1
+ # If you want to use something other than ApplicationResource for your main class you can change it here, just uncomment the next line and change the class name:
2
+ #
3
+ # Bulk::Resource.application_resource_class = :ApplicationResource
@@ -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
@@ -0,0 +1,2 @@
1
+ class <%= class_name %>Resource < ApplicationResource
2
+ 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
+