standardapi 6.1.0 → 7.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +80 -58
  3. data/lib/standard_api/access_control_list.rb +40 -6
  4. data/lib/standard_api/controller.rb +96 -28
  5. data/lib/standard_api/helpers.rb +13 -7
  6. data/lib/standard_api/middleware.rb +5 -0
  7. data/lib/standard_api/railtie.rb +17 -0
  8. data/lib/standard_api/route_helpers.rb +59 -9
  9. data/lib/standard_api/test_case/calculate_tests.rb +7 -6
  10. data/lib/standard_api/test_case/destroy_tests.rb +19 -7
  11. data/lib/standard_api/test_case/index_tests.rb +7 -13
  12. data/lib/standard_api/test_case/show_tests.rb +7 -7
  13. data/lib/standard_api/test_case/update_tests.rb +7 -6
  14. data/lib/standard_api/version.rb +1 -1
  15. data/lib/standard_api/views/application/_record.json.jbuilder +4 -3
  16. data/lib/standard_api/views/application/_record.streamer +4 -3
  17. data/lib/standard_api/views/application/_schema.json.jbuilder +20 -8
  18. data/lib/standard_api/views/application/_schema.streamer +22 -8
  19. data/lib/standard_api.rb +1 -0
  20. data/test/standard_api/caching_test.rb +2 -2
  21. data/test/standard_api/controller/include_test.rb +107 -0
  22. data/test/standard_api/controller/subresource_test.rb +157 -0
  23. data/test/standard_api/helpers_test.rb +9 -8
  24. data/test/standard_api/nested_attributes/belongs_to_test.rb +71 -0
  25. data/test/standard_api/nested_attributes/has_and_belongs_to_many_test.rb +70 -0
  26. data/test/standard_api/nested_attributes/has_many_test.rb +85 -0
  27. data/test/standard_api/nested_attributes/has_one_test.rb +71 -0
  28. data/test/standard_api/route_helpers_test.rb +56 -0
  29. data/test/standard_api/standard_api_test.rb +80 -50
  30. data/test/standard_api/test_app/app/controllers/acl/camera_acl.rb +7 -0
  31. data/test/standard_api/test_app/app/controllers/acl/photo_acl.rb +13 -0
  32. data/test/standard_api/test_app/app/controllers/acl/property_acl.rb +7 -1
  33. data/test/standard_api/test_app/controllers.rb +17 -0
  34. data/test/standard_api/test_app/models.rb +59 -2
  35. data/test/standard_api/test_app/test/factories.rb +3 -0
  36. data/test/standard_api/test_app/views/sessions/create.json.jbuilder +1 -0
  37. data/test/standard_api/test_app/views/sessions/create.streamer +3 -0
  38. data/test/standard_api/test_app.rb +12 -1
  39. data/test/standard_api/test_helper.rb +9 -0
  40. metadata +52 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7134ec77418da381ff4cb9aa8717f797784ab1f4d45faefde89261e6e7a82ad9
4
- data.tar.gz: 83e924e7fd2ded8c5d4239f9d775ef232fa985a4daa838aec374105eed65df2e
3
+ metadata.gz: 88dd56c94d20d8649c5f755509be1aee880f7dff0133602a4fe46c0408c4e65b
4
+ data.tar.gz: 3f14301f672ac171f9e2a39620de9f1913300572164978857ea12fcb1bb008de
5
5
  SHA512:
6
- metadata.gz: 6e477baa763d068d112a29a9539145b75edf47be726c21e679fc4842ae63157914c8b2753f0f7e338c43fe2e328cbe85c4cb3681958113b5533af478bcb43fc3
7
- data.tar.gz: 551a57b70b62f0f6c27f97aba97930b220a40b80799058a7c1a35d0d4a0afa787dd760ac01d42110d050a4d0d0f1387d6b00d63629c523c96e0b49a6910b77c1
6
+ metadata.gz: e3c282a6b8898d1c1451e381dea4cf57c3cd58b65bccfba6a0b5f9d302242287bb233dd43b4a078c51f1711a32a449d47c157626a92cc752939d8040ef704534
7
+ data.tar.gz: 56d90b5dc3d5d04924b7bd5e5fe6a98cfe9cf4f6fa79d30c933abff3e5c6f6b69a6718a699bb5199183f05372f99d8038f99dc7e5ff1b011dca4da978ee4d5ae
data/README.md CHANGED
@@ -1,36 +1,27 @@
1
1
  # StandardAPI
2
2
 
3
- StandardAPI makes it easy to expost a [REST](https://en.wikipedia.org/wiki/Representational_state_transfer)
3
+ StandardAPI makes it easy to expose a [REST](https://en.wikipedia.org/wiki/Representational_state_transfer)
4
4
  interface to your Rails models.
5
5
 
6
6
  # Installation
7
7
 
8
8
  gem install standardapi
9
9
 
10
- In your Gemfile:
10
+ In your `Gemfile`:
11
11
 
12
12
  gem 'standardapi', require: 'standard_api'
13
13
 
14
- In `config/application.rb:
14
+ Optionally in `config/application.rb`:
15
15
 
16
- require_relative 'boot'
17
-
18
- require 'rails/all'
19
- require 'standard_api/middleware/query_encoding'
20
-
21
- # Require the gems listed in Gemfile, including any gems
22
- # you've limited to :test, :development, or :production.
23
- Bundler.require(*Rails.groups)
24
-
25
- module Tester
16
+ module MyApplication
26
17
  class Application < Rails::Application
27
18
  # Initialize configuration defaults for originally generated Rails version.
28
- config.load_defaults 5.2
19
+ config.load_defaults 7.0
29
20
 
30
- # Settings in config/environments/* take precedence over those specified here.
31
- # Application configuration can go into files in config/initializers
32
- # -- all .rb files in that directory are automatically loaded after loading
33
- # the framework and any gems in your application.
21
+ # QueryEncoding middleware intercepts and parses the query string
22
+ # as MessagePack if the `Query-Encoding` header is set to `application/msgpack`
23
+ # which allows GET request with types as opposed to all values being interpeted
24
+ # as strings
34
25
  config.middleware.insert_after Rack::MethodOverride, StandardAPI::Middleware::QueryEncoding
35
26
  end
36
27
  end
@@ -41,66 +32,96 @@ StandardAPI is a module that can be included into any controller to expose a API
41
32
  for. Alternatly, it can be included into `ApplicationController`, giving all
42
33
  inherited controllers an exposed API.
43
34
 
44
- class ApplicationController < ActiveController::Base
45
- include StandardAPI::Controller
46
-
47
- end
48
-
49
- By default any paramaters passed to update and create are whitelisted with by
50
- the method named after the model the controller represents. For example, the
51
- following will only allow the `caption` attribute of the `Photo` model to be
52
- updated.
53
-
54
35
  class PhotosController < ApplicationController
55
36
  include StandardAPI
56
37
 
38
+ # Allowed paramaters
39
+ # By default any paramaters passed to update and create are whitelisted by
40
+ # the method named after the model the controller represents. For example,
41
+ # the following will only allow the `caption` attribute of the `Photo`
42
+ # model to be set on update or create.
57
43
  def photo_params
58
44
  [:caption]
59
45
  end
46
+
47
+ # Allowed orderings
48
+ # The ordering is whitelisted as well, you will mostly likely want to
49
+ # ensure indexes have been created on these columns. In this example the
50
+ # response can be ordered by any permutation of `id`, `created_at`, and
51
+ # `updated_at`.
52
+ def photo_orders
53
+ [:id, :created_at, :updated_at]
54
+ end
55
+
56
+ # Allowed includes
57
+ # Similarly, the includes (including of relationships in the reponse) are
58
+ # whitelisted. Note how includes can also support nested includes. In this
59
+ # case when including the author, the photos that the author took can also
60
+ # be included.
61
+ def photo_includes
62
+ { author: [:photos] }
63
+ end
60
64
  end
61
65
 
62
- If greater control of the allowed paramaters is required, the `model_params`
63
- method can be overridden. It simply returns a set of `StrongParameters`.
66
+ ##### Access Control List
64
67
 
65
- class PhotosController < ApplicationController
66
- include StandardAPI
67
-
68
- def model_params
69
- if @photo.author == current_user
70
- [:caption]
71
- else
72
- [:comment]
73
- end
74
- end
68
+ For greater control of the allowed paramaters and nesting of paramaters
69
+ `StandardAPI::AccessControlList` is available. To use it include it in your base
70
+ controller:
71
+
72
+ class ApplicationController
73
+ include StandardAPI::Control
74
+ include StandardAPI::AccessControlList
75
75
  end
76
76
 
77
- Similarly, the ordering and includes (including of relationships in the reponse)
78
- is whitelisted as well.
77
+ Then create an ACL file for each model you want in `app/controllers/acl`.
79
78
 
80
- Full Example:
79
+ Taking the above example we would remove the `photo_*` methods and create the
80
+ following files:
81
81
 
82
- class PhotosController < ApplicationController
83
- including StandardAPI
82
+ `app/controllers/acl/photo_acl.rb`:
84
83
 
85
- # Allowed paramaters
86
- def photo_params
87
- [:caption]
84
+ module PhotoACL
85
+ # Allowed attributes
86
+ def attributes
87
+ [ :caption ]
88
88
  end
89
-
90
- # Allowed orderings
91
- def photo_orders
92
- [:id, :created_at, :updated_at]
89
+
90
+ # Allowed saving / creating nested attributes
91
+ def nested
92
+ [ :camera ]
93
93
  end
94
-
94
+
95
+ # Allowed orders
96
+ def orders
97
+ [ :id, :created_at, :updated_at ]
98
+ end
99
+
95
100
  # Allowed includes
96
- def photo_includes
97
- { author: [:photos] }
101
+ def includes
102
+ [ :author ]
98
103
  end
104
+ end
99
105
 
106
+ `app/controllers/acl/author_acl.rb`:
107
+
108
+ module AuthorACL
109
+ def includes
110
+ [ :photos ]
111
+ end
100
112
  end
101
113
 
102
- Note how includes can also support nested includes. So in this case when
103
- including the author, the photos that the author took can also be included.
114
+ All of these methods are optional and will be included in ApplicationController
115
+ for StandardAPI to determine allowed attributes, nested attributes, orders and
116
+ includes.
117
+
118
+ `includes` now returns a shallow Array, StandardAPI can how determine including
119
+ an `author` and the author's `photos` is allowed by looking at what includes are
120
+ allowed on photo and author.
121
+
122
+ The `nested` function tells StandardAPI what relations on `Photo` are allowed to
123
+ be set with the API and will determine what attributes are allowed by looking
124
+ for a `camera_acl` file.
104
125
 
105
126
  # API Usage
106
127
  Resources can be queried via REST style end points
@@ -208,7 +229,7 @@ And example contoller and it's tests.
208
229
  # The mask is then applyed to all actions when querring ActiveRecord
209
230
  # Will only allow photos that have id one. For more on the syntax see
210
231
  # the activerecord-filter gem.
211
- def current_mask
232
+ def mask_for(table_name)
212
233
  { id: 1 }
213
234
  end
214
235
 
@@ -231,3 +252,4 @@ StandardAPI Resource Interface
231
252
  | `/models?where[id][]=1&where[id][]=2` | `{ where: { id: [1,2] } }` | `SELECT * FROM models WHERE id IN (1, 2)` | `[{ id: 1 }, { id: 2 }]` |
232
253
 
233
254
 
255
+
@@ -56,7 +56,7 @@ module StandardAPI
56
56
  end
57
57
 
58
58
  def filter_model_params(model_params, model, id: nil, allow_id: nil)
59
- permitted_params = if self.respond_to?("#{model_name(model)}_attributes", true)
59
+ permitted_params = if model_params && self.respond_to?("#{model_name(model)}_attributes", true)
60
60
  permits = self.send("#{model_name(model)}_attributes")
61
61
 
62
62
  allow_id ? model_params.permit(permits, :id) : model_params.permit(permits)
@@ -67,7 +67,7 @@ module StandardAPI
67
67
  if self.respond_to?("nested_#{model_name(model)}_attributes", true)
68
68
  self.send("nested_#{model_name(model)}_attributes").each do |relation|
69
69
  relation = model.reflect_on_association(relation)
70
- attributes_key = "#{relation.name}_attributes"
70
+ attributes_key = "#{relation.name}"
71
71
 
72
72
  if model_params.has_key?(attributes_key)
73
73
  filter_method = "filter_#{relation.klass.base_class.model_name.singular}_params"
@@ -77,15 +77,49 @@ module StandardAPI
77
77
  permitted_params["#{relation.name.to_s.singularize}_ids"] = model_params[attributes_key].map{|a| a['id']}
78
78
  elsif self.respond_to?(filter_method, true)
79
79
  permitted_params[attributes_key] = if model_params[attributes_key].is_a?(Array)
80
- model_params[attributes_key].map { |i| self.send(filter_method, i, allow_id: true) }
80
+ models = relation.klass.find(model_params[attributes_key].map { |i| i['id'] }.compact)
81
+ model_params[attributes_key].map { |i|
82
+ i_params = self.send(filter_method, i, allow_id: true)
83
+ if i_params['id']
84
+ r = models.find { |r| r.id == i_params['id'] }
85
+ r.assign_attributes(i_params)
86
+ r
87
+ else
88
+ relation.klass.new(i_params)
89
+ end
90
+ }
81
91
  else
82
- self.send(filter_method, model_params[attributes_key], allow_id: true)
92
+ i_params = self.send(filter_method, model_params[attributes_key], allow_id: true)
93
+ if i_params['id']
94
+ r = relation.klass.find(i_params['id'])
95
+ r.assign_attributes(i_params)
96
+ r
97
+ else
98
+ relation.klass.new(i_params)
99
+ end
83
100
  end
84
101
  else
85
102
  permitted_params[attributes_key] = if model_params[attributes_key].is_a?(Array)
86
- model_params[attributes_key].map { |i| filter_model_params(i, relation.klass.base_class, allow_id: true) }
103
+ models = relation.klass.find(model_params[attributes_key].map { |i| i['id'] }.compact)
104
+ model_params[attributes_key].map { |i|
105
+ i_params = filter_model_params(i, relation.klass.base_class, allow_id: true)
106
+ if i_params['id']
107
+ r = models.find { |r| r.id == i_params['id'] }
108
+ r.assign_attributes(i_params)
109
+ r
110
+ else
111
+ relation.klass.new(i_params)
112
+ end
113
+ }
87
114
  else
88
- filter_model_params(model_params[attributes_key], relation.klass.base_class, allow_id: true)
115
+ i_params = filter_model_params(model_params[attributes_key], relation.klass.base_class, allow_id: true)
116
+ if i_params['id']
117
+ r = relation.klass.find(i_params['id'])
118
+ r.assign_attributes(i_params)
119
+ r
120
+ else
121
+ relation.klass.new(i_params)
122
+ end
89
123
  end
90
124
  end
91
125
  elsif relation.collection? && model_params.has_key?("#{relation.name.to_s.singularize}_ids")
@@ -1,12 +1,13 @@
1
1
  module StandardAPI
2
2
  module Controller
3
3
 
4
- delegate :preloadables, to: :helpers
4
+ delegate :preloadables, :model_partial, to: :helpers
5
5
 
6
6
  def self.included(klass)
7
7
  klass.helper_method :includes, :orders, :model, :models, :resource_limit,
8
8
  :default_limit
9
9
  klass.before_action :set_standardapi_headers
10
+ klass.before_action :includes, except: [:destroy, :add_resource, :remove_resource]
10
11
  klass.rescue_from StandardAPI::ParameterMissing, with: :bad_request
11
12
  klass.rescue_from StandardAPI::UnpermittedParameters, with: :bad_request
12
13
  klass.append_view_path(File.join(File.dirname(__FILE__), 'views'))
@@ -73,7 +74,7 @@ module StandardAPI
73
74
  end
74
75
  else
75
76
  if request.format == :html
76
- render :edit, status: :bad_request
77
+ render :new, status: :bad_request
77
78
  else
78
79
  render :show, status: :bad_request
79
80
  end
@@ -96,53 +97,97 @@ module StandardAPI
96
97
  render :show, status: :ok
97
98
  end
98
99
  else
99
- render :show, status: :bad_request
100
+ if request.format == :html
101
+ render :edit, status: :bad_request
102
+ else
103
+ render :show, status: :bad_request
104
+ end
100
105
  end
101
106
  end
102
107
 
103
108
  def destroy
104
- resources.find(params[:id]).destroy!
109
+ records = resources.find(params[:id].split(','))
110
+ model.transaction { records.each(&:destroy!) }
111
+
105
112
  head :no_content
106
113
  end
107
114
 
108
115
  def remove_resource
109
116
  resource = resources.find(params[:id])
110
117
  association = resource.association(params[:relationship])
111
- subresource = association.klass.find_by_id(params[:resource_id])
112
118
 
113
- if(subresource)
114
- if association.is_a? ActiveRecord::Associations::HasManyAssociation
115
- resource.send(params[:relationship]).delete(subresource)
116
- else
117
- resource.send("#{params[:relationship]}=", nil)
119
+ result = case association
120
+ when ActiveRecord::Associations::CollectionAssociation
121
+ association.delete(association.klass.find(params[:resource_id]))
122
+ when ActiveRecord::Associations::SingularAssociation
123
+ if resource.send(params[:relationship])&.id&.to_s == params[:resource_id]
124
+ resource.update(params[:relationship] => nil)
118
125
  end
119
- head :no_content
120
- else
121
- head :not_found
122
126
  end
127
+ head result ? :no_content : :not_found
123
128
  end
124
129
 
125
130
  def add_resource
126
131
  resource = resources.find(params[:id])
127
132
  association = resource.association(params[:relationship])
128
- subresource = association.klass.find_by_id(params[:resource_id])
133
+ subresource = association.klass.find(params[:resource_id])
129
134
 
130
- if(subresource)
131
- if association.is_a? ActiveRecord::Associations::HasManyAssociation
132
- result = resource.send(params[:relationship]) << subresource
133
- else
134
- result = resource.send("#{params[:relationship]}=", subresource)
135
- end
136
- head result ? :created : :bad_request
135
+ result = case association
136
+ when ActiveRecord::Associations::CollectionAssociation
137
+ association.concat(subresource)
138
+ when ActiveRecord::Associations::SingularAssociation
139
+ resource.update(params[:relationship] => subresource)
140
+ end
141
+ head result ? :created : :bad_request
142
+ rescue ActiveRecord::RecordNotUnique
143
+ render json: {errors: [
144
+ "Relationship between #{resource.class.name} and #{subresource.class.name} violates unique constraints"
145
+ ]}, status: :bad_request
146
+ end
147
+
148
+ def create_resource
149
+ resource = resources.find(params[:id])
150
+ association = resource.association(params[:relationship])
151
+
152
+ subresource_params = if self.respond_to?("filter_#{model_name(association.klass)}_params", true)
153
+ self.send("filter_#{model_name(association.klass)}_params", params[model_name(association.klass)], id: params[:id])
154
+ elsif self.respond_to?("#{association.klass.model_name.singular}_params", true)
155
+ params.require(association.klass.model_name.singular).permit(self.send("#{association.klass.model_name.singular}_params"))
156
+ elsif self.respond_to?("filter_model_params", true)
157
+ filter_model_params(params[model_name(association.klass)], association.klass.base_class)
137
158
  else
138
- head :not_found
159
+ ActionController::Parameters.new
160
+ end
161
+
162
+ subresource = association.klass.new(subresource_params)
163
+
164
+ result = case association
165
+ when ActiveRecord::Associations::CollectionAssociation
166
+ association.concat(subresource)
167
+ when ActiveRecord::Associations::SingularAssociation
168
+ resource.update(params[:relationship] => subresource)
139
169
  end
140
170
 
171
+ partial = model_partial(subresource)
172
+ partial_record_name = partial.split('/').last.to_sym
173
+ if result
174
+ render partial: partial, locals: {partial_record_name => subresource}, status: :created
175
+ else
176
+ render partial: partial, locals: {partial_record_name => subresource}, status: :bad_request
177
+ end
141
178
  end
142
179
 
180
+ def mask
181
+ @mask ||= Hash.new do |hash, key|
182
+ hash[key] = mask_for(key)
183
+ end
184
+ end
185
+
143
186
  # Override if you want to support masking
144
- def current_mask
145
- @current_mask ||= {}
187
+ def mask_for(table_name)
188
+ # case table_name
189
+ # when 'accounts'
190
+ # end
146
191
  end
147
192
 
148
193
  module ClassMethods
@@ -165,7 +210,11 @@ module StandardAPI
165
210
  end
166
211
 
167
212
  def model
168
- self.class.model
213
+ if action_name&.end_with?('_resource')
214
+ self.class.model.reflect_on_association(params[:relationship]).klass
215
+ else
216
+ self.class.model
217
+ end
169
218
  end
170
219
 
171
220
  def models
@@ -215,8 +264,7 @@ module StandardAPI
215
264
  end
216
265
 
217
266
  def resources
218
- mask = current_mask[model.table_name] || current_mask[model.table_name.to_sym]
219
- query = model.filter(params['where']).filter(mask)
267
+ query = self.class.model.filter(params['where']).filter(mask[self.class.model.table_name.to_sym])
220
268
 
221
269
  if params[:distinct_on]
222
270
  query = query.distinct_on(params[:distinct_on])
@@ -234,9 +282,29 @@ module StandardAPI
234
282
 
235
283
  query
236
284
  end
285
+
286
+ def nested_includes(model, attributes)
287
+ includes = {}
288
+ attributes&.each do |key, value|
289
+ if association = model.reflect_on_association(key)
290
+ includes[key] = nested_includes(association.klass, value)
291
+ end
292
+ end
293
+ includes
294
+ end
237
295
 
238
296
  def includes
239
- @includes ||= StandardAPI::Includes.sanitize(params[:include], model_includes)
297
+ @includes ||= if params[:include]
298
+ StandardAPI::Includes.sanitize(params[:include], model_includes)
299
+ else
300
+ {}
301
+ end
302
+
303
+ if (action_name == 'create' || action_name == 'update') && model && params.has_key?(model.model_name.singular)
304
+ @includes.reverse_merge!(nested_includes(model, params[model.model_name.singular].to_unsafe_h))
305
+ end
306
+
307
+ @includes
240
308
  end
241
309
 
242
310
  def required_orders
@@ -1,5 +1,11 @@
1
1
  module StandardAPI
2
2
  module Helpers
3
+
4
+ def serialize_attribute(json, record, name, type)
5
+ value = record.send(name)
6
+
7
+ json.set! name, type == :binary ? value&.unpack1('H*') : value
8
+ end
3
9
 
4
10
  def preloadables(record, includes)
5
11
  preloads = {}
@@ -97,13 +103,13 @@ module StandardAPI
97
103
 
98
104
  case association = record.class.reflect_on_association(relation)
99
105
  when ActiveRecord::Reflection::HasManyReflection, ActiveRecord::Reflection::HasAndBelongsToManyReflection, ActiveRecord::Reflection::HasOneReflection, ActiveRecord::Reflection::ThroughReflection
100
- "#{record.model_name.cache_key}/#{record.id}/#{includes_to_cache_key(relation, subincludes)}-#{timestamp.utc.to_s(record.cache_timestamp_format)}"
106
+ "#{record.model_name.cache_key}/#{record.id}/#{includes_to_cache_key(relation, subincludes)}-#{timestamp.utc.to_fs(record.cache_timestamp_format)}"
101
107
  when ActiveRecord::Reflection::BelongsToReflection
102
108
  klass = association.options[:polymorphic] ? record.send(association.foreign_type).constantize : association.klass
103
109
  if subincludes.empty?
104
- "#{klass.model_name.cache_key}/#{record.send(association.foreign_key)}-#{timestamp.utc.to_s(klass.cache_timestamp_format)}"
110
+ "#{klass.model_name.cache_key}/#{record.send(association.foreign_key)}-#{timestamp.utc.to_fs(klass.cache_timestamp_format)}"
105
111
  else
106
- "#{klass.model_name.cache_key}/#{record.send(association.foreign_key)}/#{digest_hash(sort_hash(subincludes))}-#{timestamp.utc.to_s(klass.cache_timestamp_format)}"
112
+ "#{klass.model_name.cache_key}/#{record.send(association.foreign_key)}/#{digest_hash(sort_hash(subincludes))}-#{timestamp.utc.to_fs(klass.cache_timestamp_format)}"
107
113
  end
108
114
  else
109
115
  raise ArgumentError, 'Unkown association type'
@@ -156,7 +162,9 @@ module StandardAPI
156
162
 
157
163
  def json_column_type(sql_type)
158
164
  case sql_type
159
- when 'timestamp without time zone'
165
+ when 'binary', 'bytea'
166
+ 'binary'
167
+ when /timestamp(\(\d+\))? without time zone/
160
168
  'datetime'
161
169
  when 'time without time zone'
162
170
  'datetime'
@@ -164,9 +172,7 @@ module StandardAPI
164
172
  'string'
165
173
  when 'json'
166
174
  'hash'
167
- when 'bigint'
168
- 'integer'
169
- when 'integer'
175
+ when 'smallint', 'bigint', 'integer'
170
176
  'integer'
171
177
  when 'jsonb'
172
178
  'hash'
@@ -0,0 +1,5 @@
1
+ module StandardAPI
2
+ module Middleware
3
+ autoload :QueryEncoding, 'standard_api/middleware/query_encoding'
4
+ end
5
+ end
@@ -20,4 +20,21 @@ module StandardAPI
20
20
  end
21
21
 
22
22
  end
23
+
24
+ module AutosaveByDefault
25
+ def self.included base
26
+ base.class_eval do
27
+ class <<self
28
+ alias_method :standard_build, :build
29
+ end
30
+
31
+ def self.build(model, name, scope, options, &block)
32
+ options[:autosave] = true
33
+ standard_build(model, name, scope, options, &block)
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ ::ActiveRecord::Associations::Builder::Association.include(AutosaveByDefault)
23
40
  end
@@ -22,11 +22,38 @@ module StandardAPI
22
22
  options = resources.extract_options!.dup
23
23
 
24
24
  resources(*resources, options) do
25
- get :schema, on: :collection
26
- get :calculate, on: :collection
27
- delete ':relationship/:resource_id' => :remove_resource, on: :member
28
- post ':relationship/:resource_id' => :add_resource, on: :member
29
- block.call if block
25
+ block.call if block # custom routes take precedence over standardapi routes
26
+
27
+ available_actions = if only = parent_resource.instance_variable_get(:@only)
28
+ Array(only).map(&:to_sym)
29
+ else
30
+ if parent_resource.instance_variable_get(:@api_only)
31
+ [:index, :create, :show, :update, :destroy]
32
+ else
33
+ [:index, :create, :new, :show, :update, :destroy, :edit]
34
+ end + [ :schema, :calculate, :add_resource, :remove_resource, :create_resource ]
35
+ end
36
+
37
+ actions = if except = parent_resource.instance_variable_get(:@except)
38
+ available_actions - Array(except).map(&:to_sym)
39
+ else
40
+ available_actions
41
+ end
42
+
43
+ get :schema, on: :collection if actions.include?(:schema)
44
+ get :calculate, on: :collection if actions.include?(:calculate)
45
+
46
+ if actions.include?(:add_resource)
47
+ post ':relationship/:resource_id' => :add_resource, on: :member
48
+ end
49
+
50
+ if actions.include?(:create_resource)
51
+ post ':relationship' => :create_resource, on: :member
52
+ end
53
+
54
+ if actions.include?(:remove_resource)
55
+ delete ':relationship/:resource_id' => :remove_resource, on: :member
56
+ end
30
57
  end
31
58
  end
32
59
 
@@ -51,10 +78,33 @@ module StandardAPI
51
78
  options = resource.extract_options!.dup
52
79
 
53
80
  resource(*resource, options) do
54
- get :schema, on: :collection
55
- get :calculate, on: :collection
56
- delete ':relationship/:resource_id' => :remove_resource, on: :member
57
- post ':relationship/:resource_id' => :add_resource, on: :member
81
+ available_actions = if only = parent_resource.instance_variable_get(:@only)
82
+ Array(only).map(&:to_sym)
83
+ else
84
+ if parent_resource.instance_variable_get(:@api_only)
85
+ [:index, :create, :show, :update, :destroy]
86
+ else
87
+ [:index, :create, :new, :show, :update, :destroy, :edit]
88
+ end + [ :schema, :calculate, :add_resource, :remove_resource ]
89
+ end
90
+
91
+ actions = if except = parent_resource.instance_variable_get(:@except)
92
+ available_actions - Array(except).map(&:to_sym)
93
+ else
94
+ available_actions
95
+ end
96
+
97
+ get :schema, on: :collection if actions.include?(:schema)
98
+ get :calculate, on: :collection if actions.include?(:calculate)
99
+
100
+ if actions.include?(:add_resource)
101
+ post ':relationship/:resource_id' => :add_resource, on: :member
102
+ end
103
+
104
+ if actions.include?(:remove_resource)
105
+ delete ':relationship/:resource_id' => :remove_resource, on: :member
106
+ end
107
+
58
108
  block.call if block
59
109
  end
60
110
  end
@@ -59,22 +59,23 @@ module StandardAPI
59
59
  # calculations
60
60
  end
61
61
 
62
- test '#calculate.json mask' do
62
+ test '#calculate.json mask_for' do
63
63
  # This is just to instance @controller
64
64
  get resource_path(:calculate)
65
65
 
66
- # If #current_mask isn't defined by StandardAPI we don't know how to
67
- # test other's implementation of #current_mask. Return and don't test.
68
- return if @controller.method(:current_mask).owner != StandardAPI
66
+ # If #mask isn't defined by StandardAPI we don't know how to
67
+ # test other's implementation of #mask_for. Return and don't test.
68
+ return if @controller.method(:mask_for).owner != StandardAPI
69
69
 
70
70
  m = create_model
71
71
 
72
- @controller.current_mask[plural_name] = { id: m.id + 100 }
72
+ @controller.define_singleton_method(:mask_for) do |table_name|
73
+ { id: m.id + 100 }
74
+ end
73
75
  selects = [{ count: :id}, { maximum: :id }, { minimum: :id }, { average: :id }]
74
76
  get :calculate, select: selects, format: 'json'
75
77
  assert_response :ok
76
78
  assert_equal [[0, nil, nil, nil]], @controller.instance_variable_get('@calculations')
77
- @controller.current_mask.delete(plural_name)
78
79
  end
79
80
 
80
81
  end