standardapi 6.0.0.32 → 6.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/lib/standard_api.rb +4 -0
  3. data/lib/standard_api/access_control_list.rb +114 -0
  4. data/lib/standard_api/controller.rb +32 -11
  5. data/lib/standard_api/helpers.rb +4 -3
  6. data/lib/standard_api/includes.rb +9 -0
  7. data/lib/standard_api/middleware/query_encoding.rb +3 -3
  8. data/lib/standard_api/railtie.rb +13 -2
  9. data/lib/standard_api/route_helpers.rb +5 -5
  10. data/lib/standard_api/test_case.rb +13 -3
  11. data/lib/standard_api/test_case/calculate_tests.rb +8 -2
  12. data/lib/standard_api/test_case/create_tests.rb +7 -9
  13. data/lib/standard_api/test_case/index_tests.rb +10 -0
  14. data/lib/standard_api/test_case/schema_tests.rb +7 -1
  15. data/lib/standard_api/test_case/show_tests.rb +1 -0
  16. data/lib/standard_api/test_case/update_tests.rb +8 -9
  17. data/lib/standard_api/version.rb +1 -1
  18. data/lib/standard_api/views/application/_record.json.jbuilder +14 -14
  19. data/lib/standard_api/views/application/_record.streamer +36 -34
  20. data/lib/standard_api/views/application/_schema.json.jbuilder +1 -1
  21. data/lib/standard_api/views/application/_schema.streamer +1 -1
  22. data/lib/standard_api/views/application/new.streamer +1 -1
  23. data/test/standard_api/caching_test.rb +14 -4
  24. data/test/standard_api/helpers_test.rb +25 -9
  25. data/test/standard_api/standard_api_test.rb +122 -14
  26. data/test/standard_api/test_app/app/controllers/acl/account_acl.rb +15 -0
  27. data/test/standard_api/test_app/app/controllers/acl/property_acl.rb +27 -0
  28. data/test/standard_api/test_app/app/controllers/acl/reference_acl.rb +7 -0
  29. data/test/standard_api/test_app/controllers.rb +13 -45
  30. data/test/standard_api/test_app/models.rb +17 -5
  31. data/test/standard_api/test_app/test/factories.rb +4 -3
  32. data/test/standard_api/test_app/views/photos/_photo.json.jbuilder +1 -0
  33. data/test/standard_api/test_app/views/photos/_photo.streamer +2 -1
  34. data/test/standard_api/test_helper.rb +12 -0
  35. metadata +19 -15
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6f8e9f6eee237614e9c303c8fe9e2e624b081272627f9a7aa3f680276872c1a9
4
- data.tar.gz: 73ede0c9b6229117b4781071441ac0581a06b16592f95f04959932c010ecb8f0
3
+ metadata.gz: 7134ec77418da381ff4cb9aa8717f797784ab1f4d45faefde89261e6e7a82ad9
4
+ data.tar.gz: 83e924e7fd2ded8c5d4239f9d775ef232fa985a4daa838aec374105eed65df2e
5
5
  SHA512:
6
- metadata.gz: 904192ff81007b628b672e094dfaad19f4b2c49a0e4119631dd46c320a85d0e2c06200b5570d29d3b70ded73046b843dfd28ce3e40358d3b2294bbf9bb7b50f6
7
- data.tar.gz: f9ed735aabd0830d6b2c7a390d4f2bca3bf8bcc1d8b806b615a76462404f58c9b65241c75e7b7e62abe84539705249ed10c82f9f5dcc4cc1ee4911e51513200f
6
+ metadata.gz: 6e477baa763d068d112a29a9539145b75edf47be726c21e679fc4842ae63157914c8b2753f0f7e338c43fe2e328cbe85c4cb3681958113b5533af478bcb43fc3
7
+ data.tar.gz: 551a57b70b62f0f6c27f97aba97930b220a40b80799058a7c1a35d0d4a0afa787dd760ac01d42110d050a4d0d0f1387d6b00d63629c523c96e0b49a6910b77c1
@@ -16,3 +16,7 @@ require 'standard_api/helpers'
16
16
  require 'standard_api/route_helpers'
17
17
  require 'standard_api/active_record/connection_adapters/postgresql/schema_statements'
18
18
  require 'standard_api/railtie'
19
+
20
+ module StandardAPI
21
+ autoload :AccessControlList, 'standard_api/access_control_list'
22
+ end
@@ -0,0 +1,114 @@
1
+ module StandardAPI
2
+ module AccessControlList
3
+
4
+ def self.traverse(path, prefix: nil, &block)
5
+ path.children.each do |child|
6
+ if child.file? && child.basename('.rb').to_s.ends_with?('_acl')
7
+ block.call([prefix, child.basename('.rb').to_s].compact.join('/'))
8
+ elsif child.directory?
9
+ traverse(child, prefix: [prefix, child.basename.to_s].compact.join('/'), &block)
10
+ end
11
+ end
12
+ end
13
+
14
+ def self.included(application_controller)
15
+ acl_dir = Rails.application.root.join('app', 'controllers', 'acl')
16
+ return if !acl_dir.exist?
17
+
18
+ traverse(acl_dir) do |child|
19
+ mod = child.classify.constantize
20
+ prefix = child.delete_suffix('_acl').gsub('/', '_')
21
+
22
+ [:orders, :includes, :attributes].each do |m|
23
+ next if !mod.instance_methods.include?(m)
24
+ mod.send :alias_method, "#{prefix}_#{m}".to_sym, m
25
+ mod.send :remove_method, m
26
+ end
27
+
28
+ if mod.instance_methods.include?(:nested)
29
+ mod.send :alias_method, "nested_#{prefix}_attributes".to_sym, :nested
30
+ mod.send :remove_method, :nested
31
+ end
32
+
33
+ if mod.instance_methods.include?(:filter)
34
+ mod.send :alias_method, "filter_#{prefix}_params".to_sym, :filter
35
+ mod.send :remove_method, :filter
36
+ end
37
+
38
+ application_controller.include mod
39
+ end
40
+ end
41
+
42
+ def model_orders
43
+ if self.respond_to?("#{model.model_name.singular}_orders", true)
44
+ self.send("#{model.model_name.singular}_orders")
45
+ else
46
+ []
47
+ end
48
+ end
49
+
50
+ def model_params
51
+ if self.respond_to?("filter_#{model_name(model)}_params", true)
52
+ self.send("filter_#{model_name(model)}_params", params[model_name(model)], id: params[:id])
53
+ else
54
+ filter_model_params(params[model_name(model)], model.base_class)
55
+ end
56
+ end
57
+
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)
60
+ permits = self.send("#{model_name(model)}_attributes")
61
+
62
+ allow_id ? model_params.permit(permits, :id) : model_params.permit(permits)
63
+ else
64
+ ActionController::Parameters.new
65
+ end
66
+
67
+ if self.respond_to?("nested_#{model_name(model)}_attributes", true)
68
+ self.send("nested_#{model_name(model)}_attributes").each do |relation|
69
+ relation = model.reflect_on_association(relation)
70
+ attributes_key = "#{relation.name}_attributes"
71
+
72
+ if model_params.has_key?(attributes_key)
73
+ filter_method = "filter_#{relation.klass.base_class.model_name.singular}_params"
74
+ if model_params[attributes_key].nil?
75
+ permitted_params[attributes_key] = nil
76
+ elsif model_params[attributes_key].is_a?(Array) && model_params[attributes_key].all? { |a| a.keys.map(&:to_sym) == [:id] }
77
+ permitted_params["#{relation.name.to_s.singularize}_ids"] = model_params[attributes_key].map{|a| a['id']}
78
+ elsif self.respond_to?(filter_method, true)
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) }
81
+ else
82
+ self.send(filter_method, model_params[attributes_key], allow_id: true)
83
+ end
84
+ else
85
+ 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) }
87
+ else
88
+ filter_model_params(model_params[attributes_key], relation.klass.base_class, allow_id: true)
89
+ end
90
+ end
91
+ elsif relation.collection? && model_params.has_key?("#{relation.name.to_s.singularize}_ids")
92
+ permitted_params["#{relation.name.to_s.singularize}_ids"] = model_params["#{relation.name.to_s.singularize}_ids"]
93
+ elsif model_params.has_key?(relation.foreign_key)
94
+ permitted_params[relation.foreign_key] = model_params[relation.foreign_key]
95
+ permitted_params[relation.foreign_type] = model_params[relation.foreign_type] if relation.polymorphic?
96
+ end
97
+
98
+ permitted_params.permit!
99
+ end
100
+ end
101
+
102
+ permitted_params
103
+ end
104
+
105
+ def model_name(model)
106
+ if model.model_name.singular.starts_with?('habtm_')
107
+ model.reflect_on_all_associations.map { |a| a.klass.base_class.model_name.singular }.sort.join('_')
108
+ else
109
+ model.model_name.singular
110
+ end
111
+ end
112
+
113
+ end
114
+ end
@@ -107,12 +107,16 @@ module StandardAPI
107
107
 
108
108
  def remove_resource
109
109
  resource = resources.find(params[:id])
110
- subresource_class = resource.association(params[:relationship]).klass
111
- subresource = subresource_class.find_by_id(params[:resource_id])
110
+ association = resource.association(params[:relationship])
111
+ subresource = association.klass.find_by_id(params[:resource_id])
112
112
 
113
113
  if(subresource)
114
- result = resource.send(params[:relationship]).delete(subresource)
115
- head result ? :no_content : :bad_request
114
+ if association.is_a? ActiveRecord::Associations::HasManyAssociation
115
+ resource.send(params[:relationship]).delete(subresource)
116
+ else
117
+ resource.send("#{params[:relationship]}=", nil)
118
+ end
119
+ head :no_content
116
120
  else
117
121
  head :not_found
118
122
  end
@@ -120,11 +124,15 @@ module StandardAPI
120
124
 
121
125
  def add_resource
122
126
  resource = resources.find(params[:id])
127
+ association = resource.association(params[:relationship])
128
+ subresource = association.klass.find_by_id(params[:resource_id])
123
129
 
124
- subresource_class = resource.association(params[:relationship]).klass
125
- subresource = subresource_class.find_by_id(params[:resource_id])
126
130
  if(subresource)
127
- result = resource.send(params[:relationship]) << 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
128
136
  head result ? :created : :bad_request
129
137
  else
130
138
  head :not_found
@@ -189,7 +197,7 @@ module StandardAPI
189
197
  if self.respond_to?("#{model.model_name.singular}_params", true)
190
198
  params.require(model.model_name.singular).permit(self.send("#{model.model_name.singular}_params"))
191
199
  else
192
- []
200
+ ActionController::Parameters.new
193
201
  end
194
202
  end
195
203
 
@@ -207,7 +215,8 @@ module StandardAPI
207
215
  end
208
216
 
209
217
  def resources
210
- query = model.filter(params['where']).filter(current_mask[model.table_name])
218
+ mask = current_mask[model.table_name] || current_mask[model.table_name.to_sym]
219
+ query = model.filter(params['where']).filter(mask)
211
220
 
212
221
  if params[:distinct_on]
213
222
  query = query.distinct_on(params[:distinct_on])
@@ -309,7 +318,19 @@ module StandardAPI
309
318
  @selects = []
310
319
  @selects << params[:group_by] if params[:group_by]
311
320
  Array(params[:select]).each do |select|
312
- select.each do |func, column|
321
+ select.each do |func, value|
322
+ distinct = false
323
+
324
+ column = case value
325
+ when ActionController::Parameters
326
+ # TODO: Add support for other aggregate expressions
327
+ # https://www.postgresql.org/docs/current/sql-expressions.html#SYNTAX-AGGREGATES
328
+ distinct = !value[:distinct].nil?
329
+ value[:distinct]
330
+ else
331
+ value
332
+ end
333
+
313
334
  if (parts = column.split(".")).length > 1
314
335
  @model = parts[0].singularize.camelize.constantize
315
336
  column = parts[1]
@@ -318,7 +339,7 @@ module StandardAPI
318
339
  column = column == '*' ? Arel.star : column.to_sym
319
340
  if functions.include?(func.to_s.downcase)
320
341
  node = (defined?(@model) ? @model : model).arel_table[column].send(func)
321
- node.distinct = true if params[:distinct]
342
+ node.distinct = distinct
322
343
  @selects << node
323
344
  end
324
345
  end
@@ -80,9 +80,10 @@ module StandardAPI
80
80
  end
81
81
  end
82
82
 
83
- def can_cache_relation?(klass, relation, subincludes)
83
+ def can_cache_relation?(record, relation, subincludes)
84
+ return false if record.new_record?
84
85
  cache_columns = ["#{relation}_cached_at"] + cached_at_columns_for_includes(subincludes).map {|c| "#{relation}_#{c}"}
85
- if (cache_columns - klass.column_names).empty?
86
+ if (cache_columns - record.class.column_names).empty?
86
87
  true
87
88
  else
88
89
  false
@@ -91,7 +92,7 @@ module StandardAPI
91
92
 
92
93
  def association_cache_key(record, relation, subincludes)
93
94
  timestamp = ["#{relation}_cached_at"] + cached_at_columns_for_includes(subincludes).map {|c| "#{relation}_#{c}"}
94
- timestamp.map! { |col| record.send(col) }
95
+ timestamp = (timestamp & record.class.column_names).map! { |col| record.send(col) }
95
96
  timestamp = timestamp.max
96
97
 
97
98
  case association = record.class.reflect_on_association(relation)
@@ -20,6 +20,15 @@ module StandardAPI
20
20
  normalized[k] = case k.to_s
21
21
  when 'when', 'where', 'order'
22
22
  case v
23
+ when Array
24
+ v.map do |x|
25
+ case x
26
+ when Hash then x.to_h
27
+ when ActionController::Parameters then x.to_unsafe_h
28
+ else
29
+ x
30
+ end
31
+ end
23
32
  when Hash then v.to_h
24
33
  when ActionController::Parameters then v.to_unsafe_h
25
34
  end
@@ -13,8 +13,8 @@ require 'msgpack'
13
13
  module StandardAPI
14
14
  module Middleware
15
15
  class QueryEncoding
16
- MSGPACK_MIME_TYPE = "application/msgpack".freeze
17
- HTTP_METHOD_OVERRIDE_HEADER = "HTTP_QUERY_ENCODING".freeze
16
+ MSGPACK_MIME_TYPE = "application/msgpack"
17
+ HTTP_METHOD_OVERRIDE_HEADER = "HTTP_QUERY_ENCODING"
18
18
 
19
19
  def initialize(app)
20
20
  @app = app
@@ -31,4 +31,4 @@ module StandardAPI
31
31
 
32
32
  end
33
33
  end
34
- end
34
+ end
@@ -1,10 +1,21 @@
1
1
  module StandardAPI
2
2
  class Railtie < ::Rails::Railtie
3
3
 
4
- initializer 'standardapi' do
4
+ initializer 'standardapi', :before => :set_autoload_paths do |app|
5
+ if app.root.join('app', 'controllers', 'acl').exist?
6
+ ActiveSupport::Inflector.inflections(:en) do |inflect|
7
+ inflect.acronym 'ACL'
8
+ end
9
+
10
+ app.config.autoload_paths << app.root.join('app', 'controllers', 'acl').to_s
11
+ end
12
+
13
+ ActiveSupport.on_load(:before_configuration) do
14
+ ::ActionDispatch::Routing::Mapper.send :include, StandardAPI::RouteHelpers
15
+ end
16
+
5
17
  ActiveSupport.on_load(:action_view) do
6
18
  ::ActionView::Base.send :include, StandardAPI::Helpers
7
- ::ActionDispatch::Routing::Mapper.send :include, StandardAPI::RouteHelpers
8
19
  end
9
20
  end
10
21
 
@@ -1,10 +1,10 @@
1
1
  module StandardAPI
2
2
  module RouteHelpers
3
-
3
+
4
4
  # StandardAPI wrapper for ActionDispatch::Routing::Mapper::Resources#resources
5
5
  #
6
6
  # Includes the following routes
7
- #
7
+ #
8
8
  # GET /schema
9
9
  # GET /calculate
10
10
  #
@@ -20,7 +20,7 @@ module StandardAPI
20
20
  # end
21
21
  def standard_resources(*resources, &block)
22
22
  options = resources.extract_options!.dup
23
-
23
+
24
24
  resources(*resources, options) do
25
25
  get :schema, on: :collection
26
26
  get :calculate, on: :collection
@@ -33,7 +33,7 @@ module StandardAPI
33
33
  # StandardAPI wrapper for ActionDispatch::Routing::Mapper::Resources#resource
34
34
  #
35
35
  # Includes the following routes
36
- #
36
+ #
37
37
  # GET /schema
38
38
  # GET /calculate
39
39
  #
@@ -49,7 +49,7 @@ module StandardAPI
49
49
  # end
50
50
  def standard_resource(*resource, &block)
51
51
  options = resource.extract_options!.dup
52
-
52
+
53
53
  resource(*resource, options) do
54
54
  get :schema, on: :collection
55
55
  get :calculate, on: :collection
@@ -25,8 +25,7 @@ module StandardAPI::TestCase
25
25
  end
26
26
 
27
27
  begin
28
- model_class_name = klass.name.gsub(/ControllerTest$/, '').singularize
29
- model_class = model_class_name.constantize
28
+ model_class = klass.name.gsub(/Test$/, '').constantize.model
30
29
 
31
30
  klass.send(:filters=, model_class.attribute_names)
32
31
  klass.send(:orders=, model_class.attribute_names)
@@ -138,8 +137,19 @@ module StandardAPI::TestCase
138
137
 
139
138
  def view_attributes(record)
140
139
  return [] if record.nil?
141
- record.attributes.select { |x| !@controller.send(:excludes_for, record.class).include?(x.to_sym) }
140
+ record.attributes.select do |x|
141
+ !@controller.send(:excludes_for, record.class).include?(x.to_sym)
142
+ end
143
+ end
144
+
145
+ def update_attributes(record)
146
+ return [] if record.nil?
147
+ record.attributes.select do |x|
148
+ !record.class.readonly_attributes.include?(x.to_s) &&
149
+ !@controller.send(:excludes_for, record.class).include?(x.to_sym)
150
+ end
142
151
  end
152
+ alias_method :create_attributes, :update_attributes
143
153
 
144
154
  module ClassMethods
145
155
 
@@ -26,8 +26,14 @@ module StandardAPI
26
26
  calculations = @controller.instance_variable_get('@calculations')
27
27
  expectations = selects.map { |s| model.send(s.keys.first, column.name) }
28
28
  expectations = [expectations] if expectations.length > 1
29
- assert_equal expectations,
30
- calculations
29
+
30
+ if math_column
31
+ assert_equal expectations.map { |a| a.map { |b| b.round(9) } },
32
+ calculations.map { |a| a.map { |b| b.round(9) } }
33
+ else
34
+ assert_equal expectations.map { |b| b.round(9) },
35
+ calculations.map { |b| b.round(9) }
36
+ end
31
37
  end
32
38
 
33
39
  test '#calculate.json params[:where]' do
@@ -22,7 +22,7 @@ module StandardAPI
22
22
  json = JSON.parse(response.body)
23
23
  assert json.is_a?(Hash)
24
24
 
25
- view_attributes(m.reload).select { |x| attrs.keys.map(&:to_s).include?(x) }.each do |key, value|
25
+ create_attributes(m.reload).select { |x| attrs.keys.map(&:to_s).include?(x) }.each do |key, value|
26
26
  message = "Model / Attribute: #{m.class.name}##{key}"
27
27
  if value.is_a?(BigDecimal)
28
28
  assert_equal_or_nil normalize_to_json(m, key, attrs[key.to_sym]).to_s.to_f, json[key.to_s].to_s.to_f, message
@@ -53,9 +53,9 @@ module StandardAPI
53
53
  json = JSON.parse(response.body)
54
54
  assert json.is_a?(Hash)
55
55
  m.reload
56
- view_attributes(m).select { |x| attrs.keys.map(&:to_s).include?(x) }.each do |key, value|
56
+ create_attributes(m).select { |x| attrs.keys.map(&:to_s).include?(x) }.each do |key, value|
57
57
  message = "Model / Attribute: #{m.class.name}##{key}"
58
- assert_equal_or_nil normalize_attribute(m, key, attrs[key.to_sym]), value, message
58
+ assert_equal_or_nil normalize_attribute(m, key, attrs[key.to_sym]), normalize_attribute(m, key, value), message
59
59
  end
60
60
  end
61
61
  end
@@ -64,8 +64,7 @@ module StandardAPI
64
64
  trait = FactoryBot.factories[singular_name].definition.defined_traits.any? { |x| x.name.to_s == 'invalid' }
65
65
 
66
66
  if !trait
67
- Rails.logger.try(:warn, "No invalid trait for #{model.name}. Skipping invalid tests")
68
- warn("No invalid trait for #{model.name}. Skipping invalid tests")
67
+ skip("No invalid trait for #{model.name}. Skipping invalid tests")
69
68
  return
70
69
  end
71
70
 
@@ -106,8 +105,7 @@ module StandardAPI
106
105
  trait = FactoryBot.factories[singular_name].definition.defined_traits.any? { |x| x.name.to_s == 'invalid' }
107
106
 
108
107
  if !trait
109
- Rails.logger.try(:warn, "No invalid trait for #{model.name}. Skipping invalid tests")
110
- warn("No invalid trait for #{model.name}. Skipping invalid tests")
108
+ skip("No invalid trait for #{model.name}. Skipping invalid tests")
111
109
  return
112
110
  end
113
111
 
@@ -146,7 +144,7 @@ module StandardAPI
146
144
  next if !association
147
145
 
148
146
  if ['belongs_to', 'has_one'].include?(association.macro.to_s)
149
- view_attributes(m.send(included)) do |key, value|
147
+ create_attributes(m.send(included)) do |key, value|
150
148
  assert_equal json[included.to_s][key.to_s], normalize_to_json(m, key, value)
151
149
  end
152
150
  else
@@ -160,7 +158,7 @@ module StandardAPI
160
158
  nil
161
159
  end
162
160
 
163
- view_attributes(m2).each do |key, value|
161
+ create_attributes(m2).each do |key, value|
164
162
  message = "Model / Attribute: #{m2.class.name}##{key}"
165
163
  if m_json[key.to_s].nil?
166
164
  assert_nil normalize_to_json(m2, key, value), message