standardapi 6.0.0.26 → 6.1.0

Sign up to get free protection for your applications and to get access to all the features.
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