restful_json 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +299 -0
- data/Rakefile +3 -0
- data/lib/application_permitter.rb +93 -0
- data/lib/restful_json/config.rb +27 -0
- data/lib/restful_json/controller.rb +346 -0
- data/lib/restful_json/model.rb +13 -0
- data/lib/restful_json/railtie.rb +19 -0
- data/lib/restful_json/version.rb +3 -0
- data/lib/restful_json.rb +7 -0
- data/lib/twinturbo/controller.rb +35 -0
- metadata +155 -0
data/README.md
ADDED
@@ -0,0 +1,299 @@
|
|
1
|
+
restful_json
|
2
|
+
=====
|
3
|
+
|
4
|
+
Develop declarative, featureful JSON RESTful-ish service controllers to use with modern Javascript MVC frameworks like AngularJS, Ember, etc. with much less code.
|
5
|
+
|
6
|
+
Should work with Rails 3.1+ and Rails 4. Feel free to submit issues and/or do a pull requests if you run into anything.
|
7
|
+
|
8
|
+
Uses Adam Hawkin's [permitter][permitter] code which uses [strong_parameters][strong_parameters] and encourages use of [active_model_serializers][active_model_serializers].
|
9
|
+
|
10
|
+
### Installation
|
11
|
+
|
12
|
+
In your Rails app's `Gemfile`, add:
|
13
|
+
|
14
|
+
gem 'restful_json'
|
15
|
+
|
16
|
+
and:
|
17
|
+
|
18
|
+
bundle install
|
19
|
+
|
20
|
+
You need to setup [cancan][cancan]. Here are the basics:
|
21
|
+
|
22
|
+
In your `app/controllers/application_controller.rb` or in your service controllers, make sure `current_user` is set:
|
23
|
+
|
24
|
+
class ApplicationController < ActionController::Base
|
25
|
+
protect_from_forgery
|
26
|
+
|
27
|
+
def current_user
|
28
|
+
User.new
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
You could do that better by setting up some real authentication with [Authlogic][authlogic], [Devise][devise], or whatever cancan will support.
|
33
|
+
|
34
|
+
In `app/models/ability.rb`, setup a basic cancan ability. Just for testing we'll allow everything:
|
35
|
+
|
36
|
+
class Ability
|
37
|
+
include CanCan::Ability
|
38
|
+
|
39
|
+
def initialize(user)
|
40
|
+
can :manage, :all
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
### Responders
|
45
|
+
|
46
|
+
If you plan to use responders outside of json with restful_json, you may want to formally install it, for flash messages, etc.:
|
47
|
+
|
48
|
+
gem install responders
|
49
|
+
|
50
|
+
Add this to your Gemfile and bundle install:
|
51
|
+
|
52
|
+
gem 'responders'
|
53
|
+
|
54
|
+
And do:
|
55
|
+
|
56
|
+
rails generate responders:install
|
57
|
+
|
58
|
+
### Strong Parameters
|
59
|
+
|
60
|
+
If you want to now disable the default whitelisting that occurs in later versions of Rails, change the config.active_record.whitelist_attributes property in your `config/application.rb`:
|
61
|
+
|
62
|
+
config.active_record.whitelist_attributes = false
|
63
|
+
|
64
|
+
This will allow you to remove / not have to use attr_accessible and do mass assignment inside your code and tests. Instead you will put this information into your permitters.
|
65
|
+
|
66
|
+
Note that restful_json automatically includes `ActiveModel::ForbiddenAttributesProtection` on all models as will be done in Rails 4.
|
67
|
+
|
68
|
+
### Configuration
|
69
|
+
|
70
|
+
At the bottom of `config/environment.rb`, you can set config items one at a time like:
|
71
|
+
|
72
|
+
RestfulJson.debug = true
|
73
|
+
|
74
|
+
or in bulk like:
|
75
|
+
|
76
|
+
RestfulJson.configure do
|
77
|
+
self.can_filter_by_default_using = [:eq] # default for :using in can_filter_by
|
78
|
+
self.debug = false # to output debugging info during request handling
|
79
|
+
self.filter_split = ',' # delimiter for values in request parameter values
|
80
|
+
self.formats = :json, :html # equivalent to specifying respond_to :json, :html in the controller, and can be overriden in the controller. Note that by default responders gem sets respond_to :html in application_controller.rb.
|
81
|
+
self.number_of_records_in_a_page = 15 # default number of records to return if using the page request function
|
82
|
+
self.predicate_prefix = '!' # delimiter for ARel predicate in the request parameter name
|
83
|
+
self.return_resource = false # if true, will render resource and HTTP 201 for post/create or resource and HTTP 200 for put/update
|
84
|
+
end
|
85
|
+
|
86
|
+
#### Advanced Configuration
|
87
|
+
|
88
|
+
In the controller, you can set a variety of class attributes with `self.something = ...` in the body of your controller to set model class, variable names, messages, etc. Take a look at the class_attribute definitions in `lib/restful_json/controller.rb`.
|
89
|
+
|
90
|
+
### Usage
|
91
|
+
|
92
|
+
By using `acts_as_restful_json` you have a configurable generic Rails 3.1+ controller that does the index, show, create, and update and other custom actions easily for you.
|
93
|
+
|
94
|
+
Everything is well-declared and concise:
|
95
|
+
|
96
|
+
class FoobarsController < ApplicationController
|
97
|
+
acts_as_restful_json
|
98
|
+
query_for :index, is: ->(t,q) {q.joins(:apples, :pears).where(apples: {color: 'green'}).where(pears: {color: 'green'})}
|
99
|
+
# args sent to can_filter_by are the request parameter name(s)
|
100
|
+
can_filter_by :foo_id # implies using: [:eq] because RestfulJson.can_filter_by_default_using = [:eq]
|
101
|
+
can_filter_by :foo_date, :bar_date, using: [:lt, :eq, :gt], with_default: Time.now # can specify multiple predicates and optionally a default value
|
102
|
+
can_filter_by :a_request_param_name, with_query: ->(t,q,param_value) {q.joins(:some_assoc).where(:some_assocs_table_name=>{some_attr: param_value})}
|
103
|
+
can_filter_by :and_another, through: [:some_attribute_on_this_model]
|
104
|
+
can_filter_by :one_more, through: [:some_association, :some_attribute_on_some_association_model]
|
105
|
+
can_filter_by :and_one_more, through: [:my_assoc, :my_assocs_assoc, :my_assocs_assocs_assoc, :an_attribute_on_my_assocs_assocs_assoc]
|
106
|
+
supports_functions :count, :uniq, :take, :skip, :page, :page_count
|
107
|
+
order_by {:foo_date => :asc}, :foo_color, {:bar_date => :desc} # an ordered array of hashes, assumes :asc if not a hash
|
108
|
+
respond_to :json, :html # specify if you want more than :json. It dynamically sets model variables with the right names, e.g. @foobar and @foobars.
|
109
|
+
end
|
110
|
+
|
111
|
+
`can_filter_by` without an option means you can send in that request param (via routing or directly, just like normal in Rails) and it will use that in the ARel query (safe from SQL injection and only letting you do what you tell it). `:using` means you can use those [ARel][arel] predicates for filtering. For a full list of available ones do:
|
112
|
+
|
113
|
+
rails c
|
114
|
+
Arel::Predications.public_instance_methods.sort
|
115
|
+
|
116
|
+
at time of writing these were:
|
117
|
+
|
118
|
+
[:does_not_match, :does_not_match_all, :does_not_match_any, :eq, :eq_all, :eq_any, :gt, :gt_all, :gt_any, :gteq, :gteq_all, :gteq_any, :in, :in_all, :in_any, :lt, :lt_all, :lt_any, :lteq, :lteq_all, :lteq_any, :matches, :matches_all, :matches_any, :not_eq, :not_eq_all, :not_eq_any, :not_in, :not_in_all, :not_in_any]
|
119
|
+
|
120
|
+
`supports_functions` lets you do other [ARel][arel] functions. `:uniq`, `:skip`, `:take`, and `:count`.
|
121
|
+
|
122
|
+
`can_filter_by` can also specify a `:with_query` to provide a lambda that takes the request parameter in when it is provided by the request.
|
123
|
+
|
124
|
+
And `can_filter_by` can also specify a `:through` to provide an easy way to inner join through a bunch models (using ActiveRecord relations) by specifying 0-to-many association names to go "through" to the final argument which is the attribute name on the last model, e.g. the two following are equivalent:
|
125
|
+
|
126
|
+
can_filter_by :a_request_param_name, with_query: ->(t,q,param_value) {q.joins(:some_assoc).where(:some_assocs_table_name=>{some_attr: param_value})}
|
127
|
+
can_filter_by :a_request_param_name, through: [:some_assoc, :some_attr] # much easier, but not as flexible as a lambda
|
128
|
+
|
129
|
+
#### Default Filter by Attribute(s)
|
130
|
+
|
131
|
+
First, declare in the controller:
|
132
|
+
|
133
|
+
can_filter_by :foo_id
|
134
|
+
|
135
|
+
If `RestfulJson.can_filter_by_default_using = [:eq]` as it is by default, then you can now get Foobars with a foo_id of '1':
|
136
|
+
|
137
|
+
http://localhost:3000/foobars?foo_id=1
|
138
|
+
|
139
|
+
#### Other Filters by Attribute(s)
|
140
|
+
|
141
|
+
First, declare in the controller:
|
142
|
+
|
143
|
+
can_filter_by :seen_on, using: [:gteq, :eq_any]
|
144
|
+
|
145
|
+
Get Foobars with seen_on of 2012-08-08 or later using the [ARel][arel] gteq predicate splitting the request param on `predicate_prefix` (configurable), you'd use:
|
146
|
+
|
147
|
+
http://localhost:3000/foobars?seen_on!gteq=2012-08-08
|
148
|
+
|
149
|
+
Multiple values are separated by `filter_split` (configurable):
|
150
|
+
|
151
|
+
http://localhost:3000/foobars?seen_on!eq_any=2012-08-08,2012-09-09
|
152
|
+
|
153
|
+
#### Unique (DISTINCT)
|
154
|
+
|
155
|
+
First, declare in the controller:
|
156
|
+
|
157
|
+
supports_functions :uniq
|
158
|
+
|
159
|
+
To return a simple unique view of a model, combine use of an active_model_serializer that returns just the attribute you want along with the uniq param, e.g. to return unique/distinct colors of foobars you'd have a serializer to just return the color and then use:
|
160
|
+
|
161
|
+
http://localhost:3000/foobars?uniq=
|
162
|
+
|
163
|
+
#### Count
|
164
|
+
|
165
|
+
First, declare in the controller:
|
166
|
+
|
167
|
+
supports_functions :count
|
168
|
+
|
169
|
+
This is another filter that can be used with the others, but instead of returning the json objects, it returns their count, which is useful for paging to determine how many results you can page through:
|
170
|
+
|
171
|
+
http://localhost:3000/foobars?count=
|
172
|
+
|
173
|
+
#### Paging
|
174
|
+
|
175
|
+
In controller make sure these are included:
|
176
|
+
|
177
|
+
supports_functions :page, :page_count
|
178
|
+
|
179
|
+
To get the first page of results:
|
180
|
+
|
181
|
+
http://localhost:3000/foobars?page=1
|
182
|
+
|
183
|
+
To get the second page of results:
|
184
|
+
|
185
|
+
http://localhost:3000/foobars?page=2
|
186
|
+
|
187
|
+
To get the total number of pages of results:
|
188
|
+
|
189
|
+
http://localhost:3000/foobars?page_count=
|
190
|
+
|
191
|
+
To set page size at application level:
|
192
|
+
|
193
|
+
RestfulJson.number_of_records_in_a_page = 15
|
194
|
+
|
195
|
+
To set page size at controller level:
|
196
|
+
|
197
|
+
self.number_of_records_in_a_page = 15
|
198
|
+
|
199
|
+
For a better idea of how this works on the backend, look at [ARel][arel]'s skip and take, or see Variable Paging.
|
200
|
+
|
201
|
+
##### Skip and Take (OFFSET and LIMIT)
|
202
|
+
|
203
|
+
In controller make sure these are included:
|
204
|
+
|
205
|
+
supports_functions :skip, :take
|
206
|
+
|
207
|
+
To skip rows returned, use 'skip'. It is called take, because skip is the [ARel][arel] equivalent of SQL OFFSET:
|
208
|
+
|
209
|
+
http://localhost:3000/foobars?skip=5
|
210
|
+
|
211
|
+
To limit the number of rows returned, use 'take'. It is called take, because take is the [ARel][arel] equivalent of SQL LIMIT:
|
212
|
+
|
213
|
+
http://localhost:3000/foobars.json?take=5
|
214
|
+
|
215
|
+
##### Variable Paging
|
216
|
+
|
217
|
+
Combine skip and take for manual completely customized paging.
|
218
|
+
|
219
|
+
Get the first page of 15 results:
|
220
|
+
|
221
|
+
http://localhost:3000/foobars?take=15
|
222
|
+
|
223
|
+
Second page of 15 results:
|
224
|
+
|
225
|
+
http://localhost:3000/foobars?skip=15&take=15
|
226
|
+
|
227
|
+
Third page of 15 results:
|
228
|
+
|
229
|
+
http://localhost:3000/foobars?skip=30&take=15
|
230
|
+
|
231
|
+
First page of 30 results:
|
232
|
+
|
233
|
+
http://localhost:3000/foobars?take=30
|
234
|
+
|
235
|
+
Second page of 30 results:
|
236
|
+
|
237
|
+
http://localhost:3000/foobars?skip=30&take=30
|
238
|
+
|
239
|
+
Third page of 30 results:
|
240
|
+
|
241
|
+
http://localhost:3000/foobars?skip=60&take=30
|
242
|
+
|
243
|
+
##### Custom Queries
|
244
|
+
|
245
|
+
To filter the list where the status_code attribute is 'green':
|
246
|
+
|
247
|
+
# t is self.model_class.arel_table and q is self.model_class.scoped
|
248
|
+
query_for :index, is: lambda {|t,q| q.where(:status_code => 'green')}
|
249
|
+
|
250
|
+
or use the `->` Ruby 1.9 lambda stab operator. You can also filter out items that have associations that don't have a certain attribute value (or anything else you can think up with [ARel][arel]/[ActiveRecord relations][ar]). To filter the list where the object's apples and pears associations are green:
|
251
|
+
|
252
|
+
# t is self.model_class.arel_table and q is self.model_class.scoped
|
253
|
+
# note: must be no space between -> and parenthesis
|
254
|
+
query_for :index, is: ->(t,q) {
|
255
|
+
q.joins(:apples, :pears)
|
256
|
+
.where(apples: {color: 'green'})
|
257
|
+
.where(pears: {color: 'green'})
|
258
|
+
}
|
259
|
+
|
260
|
+
##### Custom Actions
|
261
|
+
|
262
|
+
`query_for` also will `alias_method (some action), :index` anything other than `:index`, so you can easily create custom non-RESTful action methods:
|
263
|
+
|
264
|
+
# t is self.model_class.arel_table and q is self.model_class.scoped
|
265
|
+
# note: must be no space between -> and parenthesis in lambda syntax!
|
266
|
+
query_for :some_action, is: ->(t,q) {q.where(:status_code => 'green')}
|
267
|
+
|
268
|
+
Note that it is a proc so you can really do whatever you want with it and will have access to other things in the environment or can call another method, etc.
|
269
|
+
|
270
|
+
### Routing
|
271
|
+
|
272
|
+
Respects regular and nested Rails resourceful routing and controller namespacing, e.g. in `config/routes.rb`:
|
273
|
+
|
274
|
+
MyAwesomeApp::Application.routes.draw do
|
275
|
+
namespace :my_service_controller_module do
|
276
|
+
resources :foobars
|
277
|
+
# why use nested if you only want to provide ways of querying via path
|
278
|
+
match 'bars/:bar_id/foobars(.:format)' => 'foobars#index'
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
### Contributors
|
283
|
+
|
284
|
+
* Gary Weaver (https://github.com/garysweaver)
|
285
|
+
* Tommy Odom (https://github.com/tpodom)
|
286
|
+
|
287
|
+
### License
|
288
|
+
|
289
|
+
Copyright (c) 2012 Gary S. Weaver, released under the [MIT license][lic].
|
290
|
+
|
291
|
+
[permitter]: http://broadcastingadam.com/2012/07/parameter_authorization_in_rails_apis/
|
292
|
+
[cancan]: https://github.com/ryanb/cancan
|
293
|
+
[strong_parameters]: https://github.com/rails/strong_parameters
|
294
|
+
[active_model_serializers]: https://github.com/josevalim/active_model_serializers
|
295
|
+
[authlogic]: https://github.com/binarylogic/authlogic
|
296
|
+
[devise]: https://github.com/plataformatec/devise
|
297
|
+
[arel]: https://github.com/rails/arel
|
298
|
+
[ar]: http://api.rubyonrails.org/classes/ActiveRecord/Relation.html
|
299
|
+
[lic]: http://github.com/rubyservices/restful_json/blob/master/LICENSE
|
data/Rakefile
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
# from Adam Hawkins's gist:
|
2
|
+
# https://gist.github.com/3150306
|
3
|
+
# http://www.broadcastingadam.com/2012/07/parameter_authorization_in_rails_apis/
|
4
|
+
class ApplicationPermitter
|
5
|
+
class PermittedAttribute < Struct.new(:name, :options) ; end
|
6
|
+
|
7
|
+
delegate :authorize!, :to => :ability
|
8
|
+
class_attribute :permitted_attributes
|
9
|
+
self.permitted_attributes = []
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def permit(*args)
|
13
|
+
options = args.extract_options!
|
14
|
+
|
15
|
+
args.each do |name|
|
16
|
+
self.permitted_attributes += [PermittedAttribute.new(name, options)]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def scope(name)
|
21
|
+
with_options :scope => name do |nested|
|
22
|
+
yield nested
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(params, user, ability = nil)
|
28
|
+
@params, @user, @ability = params, user, ability
|
29
|
+
end
|
30
|
+
|
31
|
+
def permitted_params
|
32
|
+
authorize_params!
|
33
|
+
filtered_params
|
34
|
+
end
|
35
|
+
|
36
|
+
def resource_name
|
37
|
+
self.class.to_s.match(/(.+)Permitter/)[1].underscore.to_sym
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def authorize_params!
|
44
|
+
needing_authorization = permitted_attributes.select { |a| a.options[:authorize] }
|
45
|
+
|
46
|
+
needing_authorization.each do |attribute|
|
47
|
+
if attribute.options[:scope]
|
48
|
+
values = Array.wrap(filtered_params[attribute.options[:scope]]).collect do |hash|
|
49
|
+
hash[attribute.name]
|
50
|
+
end.compact
|
51
|
+
else
|
52
|
+
values = Array.wrap filtered_params[attribute.name]
|
53
|
+
end
|
54
|
+
|
55
|
+
klass = (attribute.options[:as].try(:to_s) || attribute.name.to_s.split(/(.+)_ids?/)[1]).classify.constantize
|
56
|
+
|
57
|
+
values.each do |record_id|
|
58
|
+
record = klass.find record_id
|
59
|
+
permission = attribute.options[:authorize].to_sym || :read
|
60
|
+
authorize! permission, record
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def filtered_params
|
66
|
+
scopes = {}
|
67
|
+
unscoped_attributes = []
|
68
|
+
|
69
|
+
permitted_attributes.each do |attribute|
|
70
|
+
if attribute.options[:scope]
|
71
|
+
key = attribute.options[:scope]
|
72
|
+
scopes[key] ||= []
|
73
|
+
scopes[key] << attribute.name
|
74
|
+
else
|
75
|
+
unscoped_attributes << attribute.name
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
@filtered_params ||= params.require(resource_name).permit(*unscoped_attributes, scopes)
|
80
|
+
end
|
81
|
+
|
82
|
+
def params
|
83
|
+
@params
|
84
|
+
end
|
85
|
+
|
86
|
+
def user
|
87
|
+
@user
|
88
|
+
end
|
89
|
+
|
90
|
+
def ability
|
91
|
+
@ability ||= Ability.new user
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module RestfulJson
|
2
|
+
CONTROLLER_OPTIONS = [
|
3
|
+
:can_filter_by_default_using,
|
4
|
+
:debug,
|
5
|
+
:filter_split,
|
6
|
+
:formats,
|
7
|
+
:number_of_records_in_a_page,
|
8
|
+
:predicate_prefix,
|
9
|
+
:return_resource
|
10
|
+
]
|
11
|
+
|
12
|
+
class << self
|
13
|
+
CONTROLLER_OPTIONS.each{|o|attr_accessor o}
|
14
|
+
alias_method :debug?, :debug
|
15
|
+
def configure(&blk); class_eval(&blk); end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
RestfulJson.configure do
|
20
|
+
self.can_filter_by_default_using = [:eq]
|
21
|
+
self.debug = false
|
22
|
+
self.filter_split = ','
|
23
|
+
self.formats = :json, :html
|
24
|
+
self.number_of_records_in_a_page = 15
|
25
|
+
self.predicate_prefix = '!'
|
26
|
+
self.return_resource = false
|
27
|
+
end
|
@@ -0,0 +1,346 @@
|
|
1
|
+
require 'restful_json/config'
|
2
|
+
require 'twinturbo/controller'
|
3
|
+
require 'active_model_serializers'
|
4
|
+
require 'strong_parameters'
|
5
|
+
require 'cancan'
|
6
|
+
|
7
|
+
module RestfulJson
|
8
|
+
module Controller
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def acts_as_restful_json(options = {})
|
13
|
+
include ::ActionController::Serialization
|
14
|
+
include ::ActionController::StrongParameters
|
15
|
+
include ::CanCan::ControllerAdditions
|
16
|
+
include ::TwinTurbo::Controller
|
17
|
+
include ActsAsRestfulJson
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module ActsAsRestfulJson
|
22
|
+
extend ActiveSupport::Concern
|
23
|
+
|
24
|
+
NILS = ['NULL','null','nil']
|
25
|
+
|
26
|
+
included do
|
27
|
+
# this can be overriden in the controller via defining respond_to
|
28
|
+
formats = RestfulJson.formats || Mime::EXTENSION_LOOKUP.keys.collect{|m|m.to_sym}
|
29
|
+
respond_to *formats
|
30
|
+
|
31
|
+
# create class attributes for each controller option and set the value to the value in the app configuration
|
32
|
+
class_attribute :model_class, instance_writer: true
|
33
|
+
class_attribute :model_singular_name, instance_writer: true
|
34
|
+
class_attribute :model_plural_name, instance_writer: true
|
35
|
+
class_attribute :param_to_attr_and_arel_predicate, instance_writer: true
|
36
|
+
class_attribute :supported_functions, instance_writer: true
|
37
|
+
class_attribute :ordered_by, instance_writer: true
|
38
|
+
class_attribute :action_to_query, instance_writer: true
|
39
|
+
class_attribute :param_to_query, instance_writer: true
|
40
|
+
class_attribute :param_to_through, instance_writer: true
|
41
|
+
|
42
|
+
# use values from config
|
43
|
+
# debug uses RestfulJson.debug? because until this is done no local debug class attribute exists to check
|
44
|
+
RestfulJson::CONTROLLER_OPTIONS.each do |key|
|
45
|
+
class_attribute key, instance_writer: true
|
46
|
+
self.send("#{key}=".to_sym, RestfulJson.send(key))
|
47
|
+
end
|
48
|
+
|
49
|
+
self.param_to_attr_and_arel_predicate ||= {}
|
50
|
+
self.supported_functions ||= []
|
51
|
+
self.ordered_by ||= []
|
52
|
+
self.action_to_query ||= {}
|
53
|
+
self.param_to_query ||= {}
|
54
|
+
self.param_to_through ||= {}
|
55
|
+
end
|
56
|
+
|
57
|
+
module ClassMethods
|
58
|
+
# A whitelist of filters and definition of filter options related to request parameters.
|
59
|
+
#
|
60
|
+
# If no options are provided or the :using option is provided, defines attributes that are queryable through the operation(s) already defined in can_filter_by_default_using, or can specify attributes:
|
61
|
+
# can_filter_by :attr_name_1, :attr_name_2 # implied using: [eq] if RestfulJson.can_filter_by_default_using = [:eq]
|
62
|
+
# can_filter_by :attr_name_1, :attr_name_2, using: [:eq, :not_eq]
|
63
|
+
#
|
64
|
+
# When :with_query is specified, it will call a supplied lambda. e.g. t is self.model_class.arel_table, q is self.model_class.scoped, and p is params[:my_param_name]:
|
65
|
+
# can_filter_by :my_param_name, with_query: ->(t,q,p) {...}
|
66
|
+
#
|
67
|
+
# When :through is specified, it will take the array supplied to through as 0 to many model names following by an attribute name. It will follow through
|
68
|
+
# each association until it gets to the attribute to filter by that via ARel joins, e.g. if the model Foobar has an association to :foo, and on the Foo model there is an assocation
|
69
|
+
# to :bar, and you want to filter by bar.name (foobar.foo.bar.name):
|
70
|
+
# can_filter_by :my_param_name, through: [:foo, :bar, :name]
|
71
|
+
def can_filter_by(*args)
|
72
|
+
options = args.extract_options!
|
73
|
+
|
74
|
+
# :using is the default action if no options are present
|
75
|
+
if options[:using] || options.size == 0
|
76
|
+
predicates = Array.wrap(options[:using] || self.can_filter_by_default_using)
|
77
|
+
predicates.each do |predicate|
|
78
|
+
predicate_sym = predicate.to_sym
|
79
|
+
args.each do |attr|
|
80
|
+
attr_sym = attr.to_sym
|
81
|
+
self.param_to_attr_and_arel_predicate[attr_sym] = [attr_sym, :eq, options] if predicate_sym == :eq
|
82
|
+
self.param_to_attr_and_arel_predicate["#{attr}#{self.predicate_prefix}#{predicate}".to_sym] = [attr_sym, predicate_sym, options]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
if options[:with_query]
|
88
|
+
args.each do |with_query_key|
|
89
|
+
self.param_to_query[with_query_key.to_sym] = options[:with_query]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
if options[:through]
|
94
|
+
args.each do |through_key|
|
95
|
+
self.param_to_through[through_key.to_sym] = options[:through]
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Can specify additional functions in the index, e.g.
|
101
|
+
# supports_functions :skip, :uniq, :take, :count
|
102
|
+
def supports_functions(*args)
|
103
|
+
options = args.extract_options! # overkill, sorry
|
104
|
+
self.supported_functions += args
|
105
|
+
end
|
106
|
+
|
107
|
+
# Specify a custom query. If action specified does not have a method, it will alias_method index to create a new action method with that query.
|
108
|
+
#
|
109
|
+
# t is self.model_class.arel_table and q is self.model_class.scoped, e.g.
|
110
|
+
# query_for :index, is: -> {|t,q| q.where(:status_code => 'green')}
|
111
|
+
def query_for(*args)
|
112
|
+
options = args.extract_options!
|
113
|
+
# TODO: support custom actions to be automaticaly defined
|
114
|
+
args.each do |an_action|
|
115
|
+
if options[:is]
|
116
|
+
self.action_to_query[an_action.to_s] = options[:is]
|
117
|
+
else
|
118
|
+
raise "#{self.class.name} must supply an :is option with query_for #{an_action.inspect}"
|
119
|
+
end
|
120
|
+
unless an_action.to_sym == :index
|
121
|
+
alias_method an_action.to_sym, :index
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Takes an string, symbol, array, hash to indicate order. If not a hash, assumes is ascending. Is cumulative and order defines order of sorting, e.g:
|
127
|
+
# #would order by foo_color attribute ascending
|
128
|
+
# order_by :foo_color
|
129
|
+
# or
|
130
|
+
# order_by {:foo_date => :asc}, :foo_color, 'foo_name', {:bar_date => :desc}
|
131
|
+
def order_by(args)
|
132
|
+
if args.is_a?(Array)
|
133
|
+
self.ordered_by += args
|
134
|
+
elsif args.is_a?(Hash)
|
135
|
+
self.ordered_by.merge!(args)
|
136
|
+
else
|
137
|
+
raise ArgumentError.new("order_by takes a hash or array of hashes")
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def initialize
|
143
|
+
super
|
144
|
+
|
145
|
+
# if not set, use controller classname
|
146
|
+
qualified_controller_name = self.class.name.chomp('Controller')
|
147
|
+
@model_class = self.model_class || qualified_controller_name.split('::').last.singularize.constantize
|
148
|
+
|
149
|
+
raise "#{self.class.name} failed to initialize. self.model_class was nil in #{self} which shouldn't happen!" if @model_class.nil?
|
150
|
+
raise "#{self.class.name} assumes that #{self.model_class} extends ActiveRecord::Base, but it didn't. Please fix, or remove this constraint." unless @model_class.ancestors.include?(ActiveRecord::Base)
|
151
|
+
|
152
|
+
@model_singular_name = self.model_singular_name || self.model_class.name.underscore
|
153
|
+
@model_plural_name = self.model_plural_name || @model_singular_name.pluralize
|
154
|
+
@model_at_plural_name_sym = "@#{@model_plural_name}".to_sym
|
155
|
+
@model_at_singular_name_sym = "@#{@model_singular_name}".to_sym
|
156
|
+
underscored_modules_and_underscored_plural_model_name = qualified_controller_name.gsub('::','_').underscore
|
157
|
+
|
158
|
+
# This workaround for models that are in a different module than the model only works if the controller's base part of the unqualified name in the plural model name.
|
159
|
+
# If the model name is different than the controller name, you will need to define methods to return the right urls.
|
160
|
+
class_eval "def #{@model_plural_name}_url;#{underscored_modules_and_underscored_plural_model_name}_url;end;def #{@model_singular_name}_url(record);#{underscored_modules_and_underscored_plural_model_name.singularize}_url(record);end"
|
161
|
+
end
|
162
|
+
|
163
|
+
def convert_request_param_value_for_filtering(attr_sym, value)
|
164
|
+
value && NILS.include?(value) ? nil : value
|
165
|
+
end
|
166
|
+
|
167
|
+
# The controller's index (list) method to list resources.
|
168
|
+
#
|
169
|
+
# Note: this method be alias_method'd by query_for, so it is more than just index.
|
170
|
+
def index
|
171
|
+
t = @model_class.arel_table
|
172
|
+
value = @model_class.scoped # returns ActiveRecord::Relation equivalent to select with no where clause
|
173
|
+
custom_query = self.action_to_query[params[:action].to_s]
|
174
|
+
if custom_query
|
175
|
+
value = custom_query.call(t, value)
|
176
|
+
end
|
177
|
+
|
178
|
+
self.param_to_query.each do |param_name, param_query|
|
179
|
+
if params[param_name]
|
180
|
+
# to_s as safety measure for vulnerabilities similar to CVE-2013-1854
|
181
|
+
value = param_query.call(t, value, params[param_name].to_s)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
self.param_to_through.each do |param_name, through_array|
|
186
|
+
if params[param_name]
|
187
|
+
# build query
|
188
|
+
# e.g. SomeModel.scoped.joins({:assoc_name => {:sub_assoc => {:sub_sub_assoc => :sub_sub_sub_assoc}}).where(sub_sub_sub_assoc_model_table_name: {column_name: value})
|
189
|
+
last_model_class = @model_class
|
190
|
+
joins = nil # {:assoc_name => {:sub_assoc => {:sub_sub_assoc => :sub_sub_sub_assoc}}
|
191
|
+
through_array.each do |association_or_attribute|
|
192
|
+
if association_or_attribute == through_array.last
|
193
|
+
# must convert param value to string before possibly using with ARel because of CVE-2013-1854, fixed in: 3.2.13 and 3.1.12
|
194
|
+
# https://groups.google.com/forum/?fromgroups=#!msg/rubyonrails-security/jgJ4cjjS8FE/BGbHRxnDRTIJ
|
195
|
+
value = value.joins(joins).where(last_model_class.table_name.to_sym => {association_or_attribute => params[param_name].to_s})
|
196
|
+
else
|
197
|
+
found_classes = last_model_class.reflections.collect {|association_name, reflection| reflection.class_name.constantize if association_name.to_sym == association_or_attribute}.compact
|
198
|
+
if found_classes.size > 0
|
199
|
+
last_model_class = found_classes[0]
|
200
|
+
else
|
201
|
+
# bad can_filter_by :through found at runtime
|
202
|
+
raise "Association #{association_or_attribute.inspect} not found on #{last_model_class}."
|
203
|
+
end
|
204
|
+
|
205
|
+
if joins.nil?
|
206
|
+
joins = association_or_attribute
|
207
|
+
else
|
208
|
+
joins = {association_or_attribute => joins}
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
self.param_to_attr_and_arel_predicate.keys.each do |param_name|
|
216
|
+
options = param_to_attr_and_arel_predicate[param_name][2]
|
217
|
+
param = params[param_name] || options[:with_default]
|
218
|
+
if param.present? && param_to_attr_and_arel_predicate[param_name]
|
219
|
+
attr_sym = param_to_attr_and_arel_predicate[param_name][0]
|
220
|
+
predicate_sym = param_to_attr_and_arel_predicate[param_name][1]
|
221
|
+
if predicate_sym == :eq
|
222
|
+
value = value.where(attr_sym => convert_request_param_value_for_filtering(attr_sym, param))
|
223
|
+
else
|
224
|
+
one_or_more_param = param.split(self.filter_split).collect{|v|convert_request_param_value_for_filtering(attr_sym, v)}
|
225
|
+
value = value.where(t[attr_sym].try(predicate_sym, one_or_more_param))
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
if params[:page] && self.supported_functions.include?(:page)
|
231
|
+
page = params[:page].to_i
|
232
|
+
page = 1 if page < 1 # to avoid people using this as a way to get all records unpaged, as that probably isn't the intent?
|
233
|
+
#TODO: to_s is hack to avoid it becoming an Arel::SelectManager for some reason which not sure what to do with
|
234
|
+
value = value.skip((self.number_of_records_in_a_page * (page - 1)).to_s)
|
235
|
+
value = value.take((self.number_of_records_in_a_page).to_s)
|
236
|
+
end
|
237
|
+
|
238
|
+
if params[:skip] && self.supported_functions.include?(:skip)
|
239
|
+
# to_s as safety measure for vulnerabilities similar to CVE-2013-1854
|
240
|
+
value = value.skip(params[:skip].to_s)
|
241
|
+
end
|
242
|
+
|
243
|
+
if params[:take] && self.supported_functions.include?(:take)
|
244
|
+
# to_s as safety measure for vulnerabilities similar to CVE-2013-1854
|
245
|
+
value = value.take(params[:take].to_s)
|
246
|
+
end
|
247
|
+
|
248
|
+
if params[:uniq] && self.supported_functions.include?(:uniq)
|
249
|
+
value = value.uniq
|
250
|
+
end
|
251
|
+
|
252
|
+
# these must happen at the end and are independent
|
253
|
+
if params[:count] && self.supported_functions.include?(:count)
|
254
|
+
value = value.count.to_i
|
255
|
+
elsif params[:page_count] && self.supported_functions.include?(:page_count)
|
256
|
+
count_value = value.count.to_i # this executes the query so nothing else can be done in AREL
|
257
|
+
value = (count_value / self.number_of_records_in_a_page) + (count_value % self.number_of_records_in_a_page ? 1 : 0)
|
258
|
+
else
|
259
|
+
self.ordered_by.each do |attr_to_direction|
|
260
|
+
# this looks nasty, but makes no sense to iterate keys if only single of each
|
261
|
+
value = value.order(t[attr_to_direction.keys[0]].call(attr_to_direction.values[0]))
|
262
|
+
end
|
263
|
+
value = value.to_a
|
264
|
+
end
|
265
|
+
|
266
|
+
@value = value
|
267
|
+
instance_variable_set(@model_at_plural_name_sym, @value)
|
268
|
+
respond_with @value
|
269
|
+
end
|
270
|
+
|
271
|
+
# The controller's show (get) method to return a resource.
|
272
|
+
def show
|
273
|
+
# to_s as safety measure for vulnerabilities similar to CVE-2013-1854
|
274
|
+
@value = @model_class.find(params[:id].to_s)
|
275
|
+
instance_variable_set(@model_at_singular_name_sym, @value)
|
276
|
+
respond_with @value
|
277
|
+
end
|
278
|
+
|
279
|
+
# The controller's new method (e.g. used for new record in html format).
|
280
|
+
def new
|
281
|
+
@value = @model_class.new
|
282
|
+
respond_with @value
|
283
|
+
end
|
284
|
+
|
285
|
+
# The controller's edit method (e.g. used for edit record in html format).
|
286
|
+
def edit
|
287
|
+
# to_s as safety measure for vulnerabilities similar to CVE-2013-1854
|
288
|
+
@value = @model_class.find(params[:id].to_s)
|
289
|
+
instance_variable_set(@model_at_singular_name_sym, @value)
|
290
|
+
end
|
291
|
+
|
292
|
+
# The controller's create (post) method to create a resource.
|
293
|
+
def create
|
294
|
+
authorize! :create, @model_class
|
295
|
+
@value = @model_class.new(permitted_params)
|
296
|
+
@value.save
|
297
|
+
instance_variable_set(@model_at_singular_name_sym, @value)
|
298
|
+
if RestfulJson.return_resource
|
299
|
+
respond_with(@value) do |format|
|
300
|
+
format.json do
|
301
|
+
if @value.errors.empty?
|
302
|
+
render json: @value, status: :created
|
303
|
+
else
|
304
|
+
render json: {errors: @value.errors}, status: :unprocessable_entity
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
else
|
309
|
+
respond_with @value
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
# The controller's update (put) method to update a resource.
|
314
|
+
def update
|
315
|
+
authorize! :update, @model_class
|
316
|
+
# to_s as safety measure for vulnerabilities similar to CVE-2013-1854
|
317
|
+
@value = @model_class.find(params[:id].to_s)
|
318
|
+
@value.update_attributes(permitted_params)
|
319
|
+
instance_variable_set(@model_at_singular_name_sym, @value)
|
320
|
+
if RestfulJson.return_resource
|
321
|
+
respond_with(@value) do |format|
|
322
|
+
format.json do
|
323
|
+
if @value.errors.empty?
|
324
|
+
render json: @value, status: :ok
|
325
|
+
else
|
326
|
+
render json: {errors: @value.errors}, status: :unprocessable_entity
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
330
|
+
else
|
331
|
+
respond_with @value
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
# The controller's destroy (delete) method to destroy a resource.
|
336
|
+
def destroy
|
337
|
+
# to_s as safety measure for vulnerabilities similar to CVE-2013-1854
|
338
|
+
@value = @model_class.find(params[:id].to_s)
|
339
|
+
@value.destroy
|
340
|
+
instance_variable_set(@model_at_singular_name_sym, @value)
|
341
|
+
respond_with @value
|
342
|
+
end
|
343
|
+
|
344
|
+
end
|
345
|
+
end
|
346
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'cancan'
|
2
|
+
|
3
|
+
module RestfulJson
|
4
|
+
module Model
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
included do
|
7
|
+
# strong parameters
|
8
|
+
include ::ActiveModel::ForbiddenAttributesProtection
|
9
|
+
# cancan, depended on by twinturbo's permitters
|
10
|
+
include ::CanCan::ModelAdditions
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'restful_json'
|
2
|
+
|
3
|
+
module RestfulJson
|
4
|
+
class Railtie < Rails::Railtie
|
5
|
+
initializer "restful_json.action_controller" do
|
6
|
+
ActiveSupport.on_load(:action_controller) do
|
7
|
+
puts "Extending #{self} with RestfulJson::Controller" if RestfulJson.debug?
|
8
|
+
include RestfulJson::Controller
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
initializer "restful_json.active_record" do
|
13
|
+
ActiveSupport.on_load(:active_record) do
|
14
|
+
puts "Extending #{self} with RestfulJson::Model" if RestfulJson.debug?
|
15
|
+
include RestfulJson::Model
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/restful_json.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
module TwinTurbo
|
2
|
+
module Controller
|
3
|
+
# Instance Methods:
|
4
|
+
|
5
|
+
# the following methods are from Adam Hawkins's post:
|
6
|
+
# http://www.broadcastingadam.com/2012/07/parameter_authorization_in_rails_apis/
|
7
|
+
# with modification to only try to call permitted params if is a permitter
|
8
|
+
|
9
|
+
def permitted_params
|
10
|
+
# if you send invalid content, it will return an HTTP 20x for a put and a 422 for a post, instead of a 500 for both.
|
11
|
+
@permitted_params ||= safe_permitted_params
|
12
|
+
end
|
13
|
+
|
14
|
+
def permitter
|
15
|
+
return unless permitter_class
|
16
|
+
|
17
|
+
@permitter ||= permitter_class.new params, current_user, current_ability
|
18
|
+
end
|
19
|
+
|
20
|
+
def permitter_class
|
21
|
+
begin
|
22
|
+
"#{self.class.to_s.match(/(.*?::)?(?<controller_name>.+)Controller/)[:controller_name].singularize}Permitter".constantize
|
23
|
+
rescue NameError
|
24
|
+
nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def safe_permitted_params
|
29
|
+
begin
|
30
|
+
permitter.send(:permitted_params)
|
31
|
+
rescue
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
metadata
ADDED
@@ -0,0 +1,155 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: restful_json
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 3.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Gary S. Weaver
|
9
|
+
- Tommy Odom
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2013-03-19 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rails-api
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ! '>='
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '0'
|
31
|
+
- !ruby/object:Gem::Dependency
|
32
|
+
name: active_model_serializers
|
33
|
+
requirement: !ruby/object:Gem::Requirement
|
34
|
+
none: false
|
35
|
+
requirements:
|
36
|
+
- - ! '>='
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '0'
|
39
|
+
type: :runtime
|
40
|
+
prerelease: false
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ! '>='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: actionpack
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :runtime
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ! '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
- !ruby/object:Gem::Dependency
|
64
|
+
name: activerecord
|
65
|
+
requirement: !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ! '>='
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
type: :runtime
|
72
|
+
prerelease: false
|
73
|
+
version_requirements: !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ! '>='
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
- !ruby/object:Gem::Dependency
|
80
|
+
name: cancan
|
81
|
+
requirement: !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ! '>='
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
type: :runtime
|
88
|
+
prerelease: false
|
89
|
+
version_requirements: !ruby/object:Gem::Requirement
|
90
|
+
none: false
|
91
|
+
requirements:
|
92
|
+
- - ! '>='
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
- !ruby/object:Gem::Dependency
|
96
|
+
name: strong_parameters
|
97
|
+
requirement: !ruby/object:Gem::Requirement
|
98
|
+
none: false
|
99
|
+
requirements:
|
100
|
+
- - ! '>='
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
type: :runtime
|
104
|
+
prerelease: false
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
none: false
|
107
|
+
requirements:
|
108
|
+
- - ! '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
description: Develop declarative, featureful JSON RESTful-ish service controllers
|
112
|
+
to use with modern Javascript MVC frameworks like AngularJS, Ember, etc. with much
|
113
|
+
less code.
|
114
|
+
email:
|
115
|
+
- garysweaver@gmail.com
|
116
|
+
executables: []
|
117
|
+
extensions: []
|
118
|
+
extra_rdoc_files: []
|
119
|
+
files:
|
120
|
+
- lib/application_permitter.rb
|
121
|
+
- lib/restful_json/config.rb
|
122
|
+
- lib/restful_json/controller.rb
|
123
|
+
- lib/restful_json/model.rb
|
124
|
+
- lib/restful_json/railtie.rb
|
125
|
+
- lib/restful_json/version.rb
|
126
|
+
- lib/restful_json.rb
|
127
|
+
- lib/twinturbo/controller.rb
|
128
|
+
- Rakefile
|
129
|
+
- README.md
|
130
|
+
homepage: https://github.com/rubyservices/restful_json
|
131
|
+
licenses:
|
132
|
+
- MIT
|
133
|
+
post_install_message:
|
134
|
+
rdoc_options: []
|
135
|
+
require_paths:
|
136
|
+
- lib
|
137
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
138
|
+
none: false
|
139
|
+
requirements:
|
140
|
+
- - ! '>='
|
141
|
+
- !ruby/object:Gem::Version
|
142
|
+
version: '0'
|
143
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
144
|
+
none: false
|
145
|
+
requirements:
|
146
|
+
- - ! '>='
|
147
|
+
- !ruby/object:Gem::Version
|
148
|
+
version: '0'
|
149
|
+
requirements: []
|
150
|
+
rubyforge_project:
|
151
|
+
rubygems_version: 1.8.25
|
152
|
+
signing_key:
|
153
|
+
specification_version: 3
|
154
|
+
summary: RESTful JSON controllers using Rails 3.1+, Rails 4+.
|
155
|
+
test_files: []
|