standardapi 6.1.0 → 7.1.0

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