standardapi 6.0.0.26 → 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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +15 -6
  3. data/lib/standard_api.rb +5 -0
  4. data/lib/standard_api/access_control_list.rb +114 -0
  5. data/lib/standard_api/active_record/connection_adapters/postgresql/schema_statements.rb +21 -0
  6. data/lib/standard_api/controller.rb +75 -78
  7. data/lib/standard_api/errors.rb +9 -0
  8. data/lib/standard_api/helpers.rb +66 -13
  9. data/lib/standard_api/includes.rb +9 -0
  10. data/lib/standard_api/middleware/query_encoding.rb +3 -3
  11. data/lib/standard_api/railtie.rb +13 -2
  12. data/lib/standard_api/route_helpers.rb +5 -5
  13. data/lib/standard_api/test_case.rb +24 -14
  14. data/lib/standard_api/test_case/calculate_tests.rb +10 -4
  15. data/lib/standard_api/test_case/create_tests.rb +13 -15
  16. data/lib/standard_api/test_case/index_tests.rb +14 -4
  17. data/lib/standard_api/test_case/schema_tests.rb +25 -3
  18. data/lib/standard_api/test_case/show_tests.rb +1 -0
  19. data/lib/standard_api/test_case/update_tests.rb +8 -9
  20. data/lib/standard_api/version.rb +1 -1
  21. data/lib/standard_api/views/application/_record.json.jbuilder +33 -30
  22. data/lib/standard_api/views/application/_record.streamer +36 -34
  23. data/lib/standard_api/views/application/_schema.json.jbuilder +68 -0
  24. data/lib/standard_api/views/application/_schema.streamer +78 -0
  25. data/lib/standard_api/views/application/new.streamer +1 -1
  26. data/lib/standard_api/views/application/schema.json.jbuilder +1 -12
  27. data/lib/standard_api/views/application/schema.streamer +1 -16
  28. data/test/standard_api/caching_test.rb +43 -0
  29. data/test/standard_api/helpers_test.rb +172 -0
  30. data/test/standard_api/performance.rb +39 -0
  31. data/test/standard_api/route_helpers_test.rb +33 -0
  32. data/test/standard_api/standard_api_test.rb +699 -0
  33. data/test/standard_api/test_app.rb +1 -0
  34. data/test/standard_api/test_app/app/controllers/acl/account_acl.rb +15 -0
  35. data/test/standard_api/test_app/app/controllers/acl/property_acl.rb +27 -0
  36. data/test/standard_api/test_app/app/controllers/acl/reference_acl.rb +7 -0
  37. data/test/standard_api/test_app/controllers.rb +13 -45
  38. data/test/standard_api/test_app/models.rb +38 -4
  39. data/test/standard_api/test_app/test/factories.rb +4 -3
  40. data/test/standard_api/test_app/views/photos/_photo.json.jbuilder +1 -0
  41. data/test/standard_api/test_app/views/photos/_photo.streamer +18 -0
  42. data/test/standard_api/test_app/views/photos/_schema.json.jbuilder +1 -0
  43. data/test/standard_api/test_app/views/photos/_schema.streamer +3 -0
  44. data/test/standard_api/test_app/views/photos/schema.json.jbuilder +1 -1
  45. data/test/standard_api/test_app/views/photos/schema.streamer +1 -0
  46. data/test/standard_api/test_helper.rb +238 -0
  47. metadata +33 -17
  48. data/test/standard_api/test_app/log/test.log +0 -129516
@@ -2,6 +2,15 @@ module StandardAPI
2
2
  class StandardAPIError < StandardError
3
3
  end
4
4
 
5
+ class ParameterMissing < StandardAPIError
6
+ attr_reader :param
7
+
8
+ def initialize(param)
9
+ @param = param
10
+ super("param is missing or the value is empty: #{param}")
11
+ end
12
+ end
13
+
5
14
  class UnpermittedParameters < StandardAPIError
6
15
  attr_reader :params
7
16
 
@@ -1,6 +1,58 @@
1
1
  module StandardAPI
2
2
  module Helpers
3
-
3
+
4
+ def preloadables(record, includes)
5
+ preloads = {}
6
+
7
+ includes.each do |key, value|
8
+ if reflection = record.klass.reflections[key]
9
+ case value
10
+ when true
11
+ preloads[key] = value
12
+ when Hash, ActiveSupport::HashWithIndifferentAccess
13
+ if !value.keys.any? { |x| ['when', 'where', 'limit', 'offset', 'order', 'distinct'].include?(x) }
14
+ if !reflection.polymorphic?
15
+ preloads[key] = preloadables_hash(reflection.klass, value)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ preloads.empty? ? record : record.preload(preloads)
23
+ end
24
+
25
+ def preloadables_hash(klass, iclds)
26
+ preloads = {}
27
+
28
+ iclds.each do |key, value|
29
+ if reflection = klass.reflections[key]
30
+ case value
31
+ when true
32
+ preloads[key] = value
33
+ when Hash, ActiveSupport::HashWithIndifferentAccess
34
+ if !value.keys.any? { |x| ['when', 'where', 'limit', 'offset', 'order', 'distinct'].include?(x) }
35
+ if !reflection.polymorphic?
36
+ preloads[key] = preloadables_hash(reflection.klass, value)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ preloads
44
+ end
45
+
46
+ def schema_partial(model)
47
+ path = model.model_name.plural
48
+
49
+ if lookup_context.exists?("schema", path, true)
50
+ [path, "schema"].join('/')
51
+ else
52
+ 'application/schema'
53
+ end
54
+ end
55
+
4
56
  def model_partial(record)
5
57
  if lookup_context.exists?(record.model_name.element, record.model_name.plural, true)
6
58
  [record.model_name.plural, record.model_name.element].join('/')
@@ -17,7 +69,7 @@ module StandardAPI
17
69
  false
18
70
  end
19
71
  end
20
-
72
+
21
73
  def cache_key(record, includes)
22
74
  timestamp_keys = ['cached_at'] + record.class.column_names.select{|x| x.ends_with? "_cached_at"}
23
75
  if includes.empty?
@@ -27,21 +79,22 @@ module StandardAPI
27
79
  "#{record.model_name.cache_key}/#{record.id}-#{digest_hash(sort_hash(includes))}-#{timestamp.utc.to_s(record.cache_timestamp_format)}"
28
80
  end
29
81
  end
30
-
31
- def can_cache_relation?(klass, relation, subincludes)
82
+
83
+ def can_cache_relation?(record, relation, subincludes)
84
+ return false if record.new_record?
32
85
  cache_columns = ["#{relation}_cached_at"] + cached_at_columns_for_includes(subincludes).map {|c| "#{relation}_#{c}"}
33
- if (cache_columns - klass.column_names).empty?
86
+ if (cache_columns - record.class.column_names).empty?
34
87
  true
35
88
  else
36
89
  false
37
90
  end
38
91
  end
39
-
92
+
40
93
  def association_cache_key(record, relation, subincludes)
41
94
  timestamp = ["#{relation}_cached_at"] + cached_at_columns_for_includes(subincludes).map {|c| "#{relation}_#{c}"}
42
- timestamp.map! { |col| record.send(col) }
95
+ timestamp = (timestamp & record.class.column_names).map! { |col| record.send(col) }
43
96
  timestamp = timestamp.max
44
-
97
+
45
98
  case association = record.class.reflect_on_association(relation)
46
99
  when ActiveRecord::Reflection::HasManyReflection, ActiveRecord::Reflection::HasAndBelongsToManyReflection, ActiveRecord::Reflection::HasOneReflection, ActiveRecord::Reflection::ThroughReflection
47
100
  "#{record.model_name.cache_key}/#{record.id}/#{includes_to_cache_key(relation, subincludes)}-#{timestamp.utc.to_s(record.cache_timestamp_format)}"
@@ -56,13 +109,13 @@ module StandardAPI
56
109
  raise ArgumentError, 'Unkown association type'
57
110
  end
58
111
  end
59
-
112
+
60
113
  def cached_at_columns_for_includes(includes)
61
114
  includes.select { |k,v| !['when', 'where', 'limit', 'order', 'distinct', 'distinct_on'].include?(k) }.map do |k, v|
62
115
  ["#{k}_cached_at"] + cached_at_columns_for_includes(v).map { |v2| "#{k}_#{v2}" }
63
116
  end.flatten
64
117
  end
65
-
118
+
66
119
  def includes_to_cache_key(relation, subincludes)
67
120
  if subincludes.empty?
68
121
  relation.to_s
@@ -70,7 +123,7 @@ module StandardAPI
70
123
  "#{relation}-#{digest_hash(sort_hash(subincludes))}"
71
124
  end
72
125
  end
73
-
126
+
74
127
  def sort_hash(hash)
75
128
  hash.keys.sort.reduce({}) do |seed, key|
76
129
  if seed[key].is_a?(Hash)
@@ -81,7 +134,7 @@ module StandardAPI
81
134
  seed
82
135
  end
83
136
  end
84
-
137
+
85
138
  def digest_hash(*hashes)
86
139
  hashes.compact!
87
140
  hashes.map! { |h| sort_hash(h) }
@@ -141,4 +194,4 @@ module StandardAPI
141
194
  end
142
195
 
143
196
  end
144
- end
197
+ end
@@ -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
@@ -18,15 +18,14 @@ module StandardAPI::TestCase
18
18
  assert_equal(expected, *args)
19
19
  end
20
20
  end
21
-
21
+
22
22
  def self.included(klass)
23
23
  [:filters, :orders, :includes].each do |attribute|
24
24
  klass.send(:class_attribute, attribute)
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)
@@ -53,14 +52,14 @@ module StandardAPI::TestCase
53
52
  end
54
53
  end
55
54
 
56
- def supports_format(format)
55
+ def supports_format(format, action=nil)
57
56
  count = controller_class.view_paths.count do |path|
58
- !Dir.glob("#{path.instance_variable_get(:@path)}/{#{model.name.underscore},application}/**/*.#{format}*").empty?
57
+ !Dir.glob("#{path.instance_variable_get(:@path)}/{#{model.name.underscore},application}/**/#{action || '*'}.#{format}*").empty?
59
58
  end
60
-
59
+
61
60
  count > 0
62
61
  end
63
-
62
+
64
63
  def default_orders
65
64
  controller_class.new.send(:default_orders)
66
65
  end
@@ -76,7 +75,7 @@ module StandardAPI::TestCase
76
75
  def model
77
76
  self.class.model
78
77
  end
79
-
78
+
80
79
  def mask
81
80
  {}
82
81
  end
@@ -98,11 +97,11 @@ module StandardAPI::TestCase
98
97
  def singular_name
99
98
  model.model_name.singular
100
99
  end
101
-
100
+
102
101
  def plural_name
103
102
  model.model_name.plural
104
103
  end
105
-
104
+
106
105
  def create_webmocks(attributes)
107
106
  attributes.each do |attribute, value|
108
107
  self.class.model.validators_on(attribute)
@@ -122,7 +121,7 @@ module StandardAPI::TestCase
122
121
  value
123
122
  end
124
123
  end
125
-
124
+
126
125
  def normalize_to_json(record, attribute, value)
127
126
  value = normalize_attribute(record, attribute, value)
128
127
  return nil if value.nil?
@@ -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
 
@@ -149,7 +159,7 @@ module StandardAPI::TestCase
149
159
 
150
160
  def controller_class
151
161
  controller_class_name = self.name.gsub(/Test$/, '')
152
- controller_class_name.constantize
162
+ controller_class_name.constantize
153
163
  rescue NameError => e
154
164
  raise e if e.message != "uninitialized constant #{controller_class_name}"
155
165
  end
@@ -166,7 +176,7 @@ module StandardAPI::TestCase
166
176
  return @model if defined?(@model) && @model
167
177
 
168
178
  klass_name = controller_class.name.gsub(/Controller$/, '').singularize
169
-
179
+
170
180
  begin
171
181
  @model = klass_name.constantize
172
182
  rescue NameError
@@ -12,7 +12,7 @@ module StandardAPI
12
12
  create_model
13
13
 
14
14
  math_column = model.columns.find { |x| CALCULATE_COLUMN_TYPES.include?(x.sql_type) }
15
-
15
+
16
16
  if math_column
17
17
  column = math_column
18
18
  selects = [{ count: column.name }, { maximum: column.name }, { minimum: column.name }, { average: column.name }]
@@ -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
@@ -35,7 +41,7 @@ module StandardAPI
35
41
  create_model
36
42
 
37
43
  math_column = model.columns.find { |x| CALCULATE_COLUMN_TYPES.include?(x.sql_type) }
38
-
44
+
39
45
  if math_column
40
46
  column = math_column
41
47
  selects = [{ count: column.name}, { maximum: column.name }, { minimum: column.name }, { average: column.name }]
@@ -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
 
@@ -85,15 +84,15 @@ module StandardAPI
85
84
  end
86
85
 
87
86
  test '#create.html' do
88
- return unless supports_format(:html)
87
+ return unless supports_format(:html, :create)
88
+
89
+ attrs = attributes_for(singular_name, :nested).select do |k,v|
90
+ !model.readonly_attributes.include?(k.to_s)
91
+ end
89
92
 
90
- attrs = attributes_for(singular_name, :nested).select{ |k,v| !model.readonly_attributes.include?(k.to_s) }
91
93
  mask.each { |k, v| attrs[k] = v }
92
94
  create_webmocks(attrs)
93
95
 
94
- file_upload = attrs.any? { |k, v| v.is_a?(Rack::Test::UploadedFile) }
95
- as = file_upload ? nil : :json
96
-
97
96
  assert_difference("#{model.name}.count") do
98
97
  post resource_path(:create), params: { singular_name => attrs }, as: :html
99
98
  assert_response :redirect
@@ -101,13 +100,12 @@ module StandardAPI
101
100
  end
102
101
 
103
102
  test '#create.html with invalid attributes renders edit action' do
104
- return unless supports_format(:html)
103
+ return unless supports_format(:html, :create)
105
104
 
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