standardapi 6.0.0.32 → 6.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 (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