standardapi 6.0.0.24 → 6.0.0.30

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +15 -6
  3. data/lib/standard_api.rb +1 -0
  4. data/lib/standard_api/active_record/connection_adapters/postgresql/schema_statements.rb +21 -0
  5. data/lib/standard_api/controller.rb +45 -69
  6. data/lib/standard_api/errors.rb +9 -0
  7. data/lib/standard_api/helpers.rb +62 -10
  8. data/lib/standard_api/test_case.rb +11 -11
  9. data/lib/standard_api/test_case/calculate_tests.rb +2 -2
  10. data/lib/standard_api/test_case/create_tests.rb +6 -6
  11. data/lib/standard_api/test_case/index_tests.rb +4 -4
  12. data/lib/standard_api/test_case/schema_tests.rb +19 -3
  13. data/lib/standard_api/version.rb +1 -1
  14. data/lib/standard_api/views/application/_schema.json.jbuilder +68 -0
  15. data/lib/standard_api/views/application/_schema.streamer +78 -0
  16. data/lib/standard_api/views/application/schema.json.jbuilder +1 -12
  17. data/lib/standard_api/views/application/schema.streamer +1 -16
  18. data/test/standard_api/test_app.rb +55 -0
  19. data/test/standard_api/test_app/config/database.yml +4 -0
  20. data/test/standard_api/test_app/controllers.rb +107 -0
  21. data/test/standard_api/test_app/models.rb +94 -0
  22. data/test/standard_api/test_app/test/factories.rb +50 -0
  23. data/test/standard_api/test_app/test/fixtures/photo.png +0 -0
  24. data/test/standard_api/test_app/views/photos/_photo.json.jbuilder +15 -0
  25. data/test/standard_api/test_app/views/photos/_photo.streamer +17 -0
  26. data/test/standard_api/test_app/views/photos/_schema.json.jbuilder +1 -0
  27. data/test/standard_api/test_app/views/photos/_schema.streamer +3 -0
  28. data/test/standard_api/test_app/views/photos/schema.json.jbuilder +1 -0
  29. data/test/standard_api/test_app/views/photos/schema.streamer +1 -0
  30. data/test/standard_api/test_app/views/properties/edit.html.erb +1 -0
  31. data/test/standard_api/test_app/views/sessions/new.html.erb +0 -0
  32. metadata +20 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b5c4c41da6b869e919e53b048048c8b3c69aae01c1ce04ec3ebddf03d7e6217a
4
- data.tar.gz: df1ab4c4111e74959f85cc0ba5a79cd580d419d97a90adcfb08dac70335fd74c
3
+ metadata.gz: bf7ed75a06eda9f024ebd87caccab44573802435d0494944137d92da6403fca2
4
+ data.tar.gz: f44ad9cdd0d8bb87bc971f0b11b8f50b5f5f1289911d7bf4764f6f867b59b407
5
5
  SHA512:
6
- metadata.gz: a035e59b5e886a620d9ccb95d316ca436a72c5b9ac4fb59b7cd85e4d484931d33c349a162ba0bd79bc6f09169ec8bb842902b984b356d83a9844f696dd643fde
7
- data.tar.gz: 5e0e1df8b36ab9bfa6ef7c2043b32c36cc6df9e4b1b772e8b848034a18cadafddaaa9b561e54aad4d2c2e46512c6a1f0cd0b0b8ff8a52e2b2b4c82acc2d51bb1
6
+ metadata.gz: 2f0f4ba53a851f87467a584b0a37708a1d2c6a52fc3360c31fbd50462af17918b7c43b435001410984291db24a451940e97487fc549f993d35ecc63d832d3b20
7
+ data.tar.gz: 26d70be42fd115039c04a4a0ba614225c05a549af0847d3473659c9d593002cd11addcfd330000163eb9d1597843c274593ba99ddd6f9a57ee39b83e8a05fcbb
data/README.md CHANGED
@@ -105,11 +105,12 @@ including the author, the photos that the author took can also be included.
105
105
  # API Usage
106
106
  Resources can be queried via REST style end points
107
107
  ```
108
- GET /records/:id fetch record
109
- PATCH /records/:id update record
110
- GET /records fetch records
111
- POST /records create record
112
- DELETE /records destroy record
108
+ GET /records/:id fetch record
109
+ PATCH /records/:id update record
110
+ GET /records/ fetch records
111
+ GET /records/calculate apply count and other functions on record(s)
112
+ POST /records create record
113
+ DELETE /records destroy record
113
114
  ```
114
115
 
115
116
  All resource end points can be filtered, ordered, limited, offset, and have includes. All options are passed via query string in a nested URI encoded format.
@@ -163,8 +164,16 @@ location: {within: 0106000020e6...} WHERE ST_Within("listings"."location
163
164
 
164
165
  // On Relationships
165
166
  property: {size: 10000} JOIN properties WHERE properties.size = 10000"
167
+ ```
168
+ ## Calculations
169
+
170
+ The only change on calculate routes is the `selects` paramater contains the functions to apply. Currently just `minimum`, `maximum`, `average`, `sum`, and `count`.
166
171
 
167
- //
172
+ ```
173
+ { count: '*' } SELECT COUNT(*)
174
+ [{ count: '*' }] SELECT COUNT(*)
175
+ [{ count: '*', maximum: :id, minimum: :id }] SELECT COUNT(*), MAXIMUM(id), MINIMUM(id)
176
+ [{ maximum: :id }, { maximum: :count }] SELECT MAXIMUM(id), MAXIMUM(count)
168
177
  ```
169
178
 
170
179
  # Testing
data/lib/standard_api.rb CHANGED
@@ -14,4 +14,5 @@ require 'standard_api/includes'
14
14
  require 'standard_api/controller'
15
15
  require 'standard_api/helpers'
16
16
  require 'standard_api/route_helpers'
17
+ require 'standard_api/active_record/connection_adapters/postgresql/schema_statements'
17
18
  require 'standard_api/railtie'
@@ -0,0 +1,21 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ class PostgreSQLAdapter < AbstractAdapter
4
+
5
+ # Returns a comment stored in database for given table
6
+ def database_comment(database_name=nil) # :nodoc:
7
+ database_name ||= current_database
8
+
9
+ scope = quoted_scope(database_name, type: "BASE TABLE")
10
+ if scope[:name]
11
+ query_value(<<~SQL, "SCHEMA")
12
+ SELECT pg_catalog.shobj_description(d.oid, 'pg_database')
13
+ FROM pg_catalog.pg_database d
14
+ WHERE datname = #{scope[:name]};
15
+ SQL
16
+ end
17
+ end
18
+
19
+ end
20
+ end
21
+ end
@@ -1,23 +1,32 @@
1
1
  module StandardAPI
2
2
  module Controller
3
3
 
4
+ delegate :preloadables, to: :helpers
5
+
4
6
  def self.included(klass)
5
- klass.helper_method :includes, :orders, :model, :resource_limit,
6
- :default_limit, :preloadables
7
+ klass.helper_method :includes, :orders, :model, :models, :resource_limit,
8
+ :default_limit
7
9
  klass.before_action :set_standardapi_headers
10
+ klass.rescue_from StandardAPI::ParameterMissing, with: :bad_request
8
11
  klass.rescue_from StandardAPI::UnpermittedParameters, with: :bad_request
9
12
  klass.append_view_path(File.join(File.dirname(__FILE__), 'views'))
10
13
  klass.extend(ClassMethods)
11
14
  end
12
15
 
13
16
  def tables
14
- Rails.application.eager_load! if Rails.env == 'development'.freeze
15
-
16
- controllers = ApplicationController.descendants
17
- controllers.select! { |c| c.ancestors.include?(self.class) && c != self.class }
18
- controllers.map!(&:model).compact!
19
- controllers.map!(&:table_name)
20
- render json: controllers
17
+ Rails.application.eager_load! if !Rails.application.config.eager_load
18
+
19
+ tables = ApplicationController.descendants
20
+ tables.select! { |c| c.ancestors.include?(self.class) && c != self.class }
21
+ tables.map!(&:model).compact!
22
+ tables.map!(&:table_name)
23
+ render json: tables
24
+ end
25
+
26
+ if Rails.env == 'development'
27
+ def schema
28
+ Rails.application.eager_load! if !Rails.application.config.eager_load
29
+ end
21
30
  end
22
31
 
23
32
  def index
@@ -34,7 +43,7 @@ module StandardAPI
34
43
  end
35
44
  end
36
45
  @calculations = Hash[@calculations] if @calculations[0].is_a?(Array) && params[:group_by]
37
-
46
+
38
47
  render json: @calculations
39
48
  end
40
49
 
@@ -95,12 +104,12 @@ module StandardAPI
95
104
  resources.find(params[:id]).destroy!
96
105
  head :no_content
97
106
  end
98
-
107
+
99
108
  def remove_resource
100
109
  resource = resources.find(params[:id])
101
110
  subresource_class = resource.association(params[:relationship]).klass
102
111
  subresource = subresource_class.find_by_id(params[:resource_id])
103
-
112
+
104
113
  if(subresource)
105
114
  result = resource.send(params[:relationship]).delete(subresource)
106
115
  head result ? :no_content : :bad_request
@@ -108,10 +117,10 @@ module StandardAPI
108
117
  head :not_found
109
118
  end
110
119
  end
111
-
120
+
112
121
  def add_resource
113
122
  resource = resources.find(params[:id])
114
-
123
+
115
124
  subresource_class = resource.association(params[:relationship]).klass
116
125
  subresource = subresource_class.find_by_id(params[:resource_id])
117
126
  if(subresource)
@@ -120,7 +129,7 @@ module StandardAPI
120
129
  else
121
130
  head :not_found
122
131
  end
123
-
132
+
124
133
  end
125
134
 
126
135
  # Override if you want to support masking
@@ -129,7 +138,7 @@ module StandardAPI
129
138
  end
130
139
 
131
140
  module ClassMethods
132
-
141
+
133
142
  def model
134
143
  return @model if defined?(@model)
135
144
  @model = name.sub(/Controller\z/, '').singularize.camelize.safe_constantize
@@ -151,6 +160,15 @@ module StandardAPI
151
160
  self.class.model
152
161
  end
153
162
 
163
+ def models
164
+ return @models if defined?(@models)
165
+ Rails.application.eager_load! if !Rails.application.config.eager_load
166
+
167
+ @models = ApplicationController.descendants
168
+ @models.select! { |c| c.ancestors.include?(self.class) && c != self.class }
169
+ @models.map!(&:model).compact!
170
+ end
171
+
154
172
  def model_includes
155
173
  if self.respond_to?("#{model.model_name.singular}_includes", true)
156
174
  self.send("#{model.model_name.singular}_includes")
@@ -190,81 +208,39 @@ module StandardAPI
190
208
 
191
209
  def resources
192
210
  query = model.filter(params['where']).filter(current_mask[model.table_name])
193
-
211
+
194
212
  if params[:distinct_on]
195
213
  query = query.distinct_on(params[:distinct_on])
196
214
  elsif params[:distinct]
197
215
  query = query.distinct
198
216
  end
199
-
217
+
200
218
  if params[:join]
201
219
  query = query.joins(params[:join].to_sym)
202
220
  end
203
-
221
+
204
222
  if params[:group_by]
205
223
  query = query.group(params[:group_by])
206
224
  end
207
-
225
+
208
226
  query
209
227
  end
210
228
 
211
229
  def includes
212
230
  @includes ||= StandardAPI::Includes.sanitize(params[:include], model_includes)
213
231
  end
214
-
215
- def preloadables(record, includes)
216
- preloads = {}
217
-
218
- includes.each do |key, value|
219
- if reflection = record.klass.reflections[key]
220
- case value
221
- when true
222
- preloads[key] = value
223
- when Hash, ActiveSupport::HashWithIndifferentAccess
224
- if !value.keys.any? { |x| ['when', 'where', 'limit', 'offset', 'order', 'distinct'].include?(x) }
225
- if !reflection.polymorphic?
226
- preloads[key] = preloadables_hash(reflection.klass, value)
227
- end
228
- end
229
- end
230
- end
231
- end
232
-
233
- preloads.empty? ? record : record.preload(preloads)
234
- end
235
-
236
- def preloadables_hash(klass, iclds)
237
- preloads = {}
238
-
239
- iclds.each do |key, value|
240
- if reflection = klass.reflections[key]
241
- case value
242
- when true
243
- preloads[key] = value
244
- when Hash, ActiveSupport::HashWithIndifferentAccess
245
- if !value.keys.any? { |x| ['when', 'where', 'limit', 'offset', 'order', 'distinct'].include?(x) }
246
- if !reflection.polymorphic?
247
- preloads[key] = preloadables_hash(reflection.klass, value)
248
- end
249
- end
250
- end
251
- end
252
- end
253
-
254
- preloads
255
- end
256
-
232
+
257
233
  def required_orders
258
234
  []
259
235
  end
260
-
236
+
261
237
  def default_orders
262
238
  nil
263
239
  end
264
240
 
265
241
  def orders
266
242
  exluded_required_orders = required_orders.map(&:to_s)
267
-
243
+
268
244
  case params[:order]
269
245
  when Hash, ActionController::Parameters
270
246
  exluded_required_orders -= params[:order].keys.map(&:to_s)
@@ -280,7 +256,7 @@ module StandardAPI
280
256
  when String
281
257
  exluded_required_orders.delete(params[:order])
282
258
  end
283
-
259
+
284
260
  if !exluded_required_orders.empty?
285
261
  params[:order] = exluded_required_orders.unshift(params[:order])
286
262
  end
@@ -309,9 +285,9 @@ module StandardAPI
309
285
  limit = params.permit(:limit)[:limit]&.to_i || default_limit
310
286
 
311
287
  if !limit
312
- raise ActionController::ParameterMissing.new(:limit)
288
+ raise StandardAPI::ParameterMissing.new(:limit)
313
289
  elsif limit > resource_limit
314
- raise ActionController::UnpermittedParameters.new([:limit, limit])
290
+ raise StandardAPI::UnpermittedParameters.new([:limit, limit])
315
291
  end
316
292
 
317
293
  limit
@@ -338,7 +314,7 @@ module StandardAPI
338
314
  @model = parts[0].singularize.camelize.constantize
339
315
  column = parts[1]
340
316
  end
341
-
317
+
342
318
  column = column == '*' ? Arel.star : column.to_sym
343
319
  if functions.include?(func.to_s.downcase)
344
320
  node = (defined?(@model) ? @model : model).arel_table[column].send(func)
@@ -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,7 +79,7 @@ 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
-
82
+
31
83
  def can_cache_relation?(klass, relation, subincludes)
32
84
  cache_columns = ["#{relation}_cached_at"] + cached_at_columns_for_includes(subincludes).map {|c| "#{relation}_#{c}"}
33
85
  if (cache_columns - klass.column_names).empty?
@@ -36,12 +88,12 @@ module StandardAPI
36
88
  false
37
89
  end
38
90
  end
39
-
91
+
40
92
  def association_cache_key(record, relation, subincludes)
41
93
  timestamp = ["#{relation}_cached_at"] + cached_at_columns_for_includes(subincludes).map {|c| "#{relation}_#{c}"}
42
94
  timestamp.map! { |col| record.send(col) }
43
95
  timestamp = timestamp.max
44
-
96
+
45
97
  case association = record.class.reflect_on_association(relation)
46
98
  when ActiveRecord::Reflection::HasManyReflection, ActiveRecord::Reflection::HasAndBelongsToManyReflection, ActiveRecord::Reflection::HasOneReflection, ActiveRecord::Reflection::ThroughReflection
47
99
  "#{record.model_name.cache_key}/#{record.id}/#{includes_to_cache_key(relation, subincludes)}-#{timestamp.utc.to_s(record.cache_timestamp_format)}"
@@ -56,13 +108,13 @@ module StandardAPI
56
108
  raise ArgumentError, 'Unkown association type'
57
109
  end
58
110
  end
59
-
111
+
60
112
  def cached_at_columns_for_includes(includes)
61
113
  includes.select { |k,v| !['when', 'where', 'limit', 'order', 'distinct', 'distinct_on'].include?(k) }.map do |k, v|
62
114
  ["#{k}_cached_at"] + cached_at_columns_for_includes(v).map { |v2| "#{k}_#{v2}" }
63
115
  end.flatten
64
116
  end
65
-
117
+
66
118
  def includes_to_cache_key(relation, subincludes)
67
119
  if subincludes.empty?
68
120
  relation.to_s
@@ -70,7 +122,7 @@ module StandardAPI
70
122
  "#{relation}-#{digest_hash(sort_hash(subincludes))}"
71
123
  end
72
124
  end
73
-
125
+
74
126
  def sort_hash(hash)
75
127
  hash.keys.sort.reduce({}) do |seed, key|
76
128
  if seed[key].is_a?(Hash)
@@ -81,7 +133,7 @@ module StandardAPI
81
133
  seed
82
134
  end
83
135
  end
84
-
136
+
85
137
  def digest_hash(*hashes)
86
138
  hashes.compact!
87
139
  hashes.map! { |h| sort_hash(h) }
@@ -141,4 +193,4 @@ module StandardAPI
141
193
  end
142
194
 
143
195
  end
144
- end
196
+ end
@@ -18,7 +18,7 @@ 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)
@@ -53,14 +53,14 @@ module StandardAPI::TestCase
53
53
  end
54
54
  end
55
55
 
56
- def supports_format(format)
56
+ def supports_format(format, action=nil)
57
57
  count = controller_class.view_paths.count do |path|
58
- !Dir.glob("#{path.instance_variable_get(:@path)}/{#{model.name.underscore},application}/**/*.#{format}*").empty?
58
+ !Dir.glob("#{path.instance_variable_get(:@path)}/{#{model.name.underscore},application}/**/#{action || '*'}.#{format}*").empty?
59
59
  end
60
-
60
+
61
61
  count > 0
62
62
  end
63
-
63
+
64
64
  def default_orders
65
65
  controller_class.new.send(:default_orders)
66
66
  end
@@ -76,7 +76,7 @@ module StandardAPI::TestCase
76
76
  def model
77
77
  self.class.model
78
78
  end
79
-
79
+
80
80
  def mask
81
81
  {}
82
82
  end
@@ -98,11 +98,11 @@ module StandardAPI::TestCase
98
98
  def singular_name
99
99
  model.model_name.singular
100
100
  end
101
-
101
+
102
102
  def plural_name
103
103
  model.model_name.plural
104
104
  end
105
-
105
+
106
106
  def create_webmocks(attributes)
107
107
  attributes.each do |attribute, value|
108
108
  self.class.model.validators_on(attribute)
@@ -122,7 +122,7 @@ module StandardAPI::TestCase
122
122
  value
123
123
  end
124
124
  end
125
-
125
+
126
126
  def normalize_to_json(record, attribute, value)
127
127
  value = normalize_attribute(record, attribute, value)
128
128
  return nil if value.nil?
@@ -149,7 +149,7 @@ module StandardAPI::TestCase
149
149
 
150
150
  def controller_class
151
151
  controller_class_name = self.name.gsub(/Test$/, '')
152
- controller_class_name.constantize
152
+ controller_class_name.constantize
153
153
  rescue NameError => e
154
154
  raise e if e.message != "uninitialized constant #{controller_class_name}"
155
155
  end
@@ -166,7 +166,7 @@ module StandardAPI::TestCase
166
166
  return @model if defined?(@model) && @model
167
167
 
168
168
  klass_name = controller_class.name.gsub(/Controller$/, '').singularize
169
-
169
+
170
170
  begin
171
171
  @model = klass_name.constantize
172
172
  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 }]
@@ -35,7 +35,7 @@ module StandardAPI
35
35
  create_model
36
36
 
37
37
  math_column = model.columns.find { |x| CALCULATE_COLUMN_TYPES.include?(x.sql_type) }
38
-
38
+
39
39
  if math_column
40
40
  column = math_column
41
41
  selects = [{ count: column.name}, { maximum: column.name }, { minimum: column.name }, { average: column.name }]
@@ -85,15 +85,15 @@ module StandardAPI
85
85
  end
86
86
 
87
87
  test '#create.html' do
88
- return unless supports_format(:html)
88
+ return unless supports_format(:html, :create)
89
+
90
+ attrs = attributes_for(singular_name, :nested).select do |k,v|
91
+ !model.readonly_attributes.include?(k.to_s)
92
+ end
89
93
 
90
- attrs = attributes_for(singular_name, :nested).select{ |k,v| !model.readonly_attributes.include?(k.to_s) }
91
94
  mask.each { |k, v| attrs[k] = v }
92
95
  create_webmocks(attrs)
93
96
 
94
- file_upload = attrs.any? { |k, v| v.is_a?(Rack::Test::UploadedFile) }
95
- as = file_upload ? nil : :json
96
-
97
97
  assert_difference("#{model.name}.count") do
98
98
  post resource_path(:create), params: { singular_name => attrs }, as: :html
99
99
  assert_response :redirect
@@ -101,7 +101,7 @@ module StandardAPI
101
101
  end
102
102
 
103
103
  test '#create.html with invalid attributes renders edit action' do
104
- return unless supports_format(:html)
104
+ return unless supports_format(:html, :create)
105
105
 
106
106
  trait = FactoryBot.factories[singular_name].definition.defined_traits.any? { |x| x.name.to_s == 'invalid' }
107
107
 
@@ -37,10 +37,10 @@ module StandardAPI
37
37
 
38
38
  test '#index.json params[:limit] does not exceed maximum limit' do
39
39
  return if !resource_limit || resource_limit == Float::INFINITY
40
-
41
- assert_raises ActionController::UnpermittedParameters do
42
- get resource_path(:index, format: :json), params: { limit: resource_limit + 1 }
43
- end
40
+
41
+ get resource_path(:index, format: :json), params: { limit: resource_limit + 1 }
42
+ assert_response :bad_request
43
+ assert_equal 'found unpermitted parameters: :limit, 1001', response.body
44
44
  end
45
45
 
46
46
  test '#index.json params[:where]' do
@@ -9,13 +9,29 @@ module StandardAPI
9
9
  get resource_path(:schema, format: :json)
10
10
  assert_response :ok
11
11
  json = JSON(@response.body)
12
- assert json['columns']
12
+ assert json['attributes']
13
+
13
14
  model.columns.map do |column|
14
- assert json['columns'][column.name]['type'], "Missing `type` for \"#{model}\" attribute \"#{column.name}\""
15
+ actual_column = json['attributes'][column.name]
16
+ assert_not_nil actual_column['type'], "Missing `type` for \"#{model}\" attribute \"#{column.name}\""
17
+ assert_equal_or_nil model.primary_key == column.name, actual_column['primary_key']
18
+ assert_equal_or_nil column.null, actual_column['null']
19
+ assert_equal_or_nil column.array, actual_column['array']
20
+ assert_equal_or_nil column.comment, actual_column['comment']
21
+ assert_equal_or_nil (column.default || column.default_function), actual_column['default']
15
22
  end
23
+
16
24
  assert json['limit']
25
+ assert_equal_or_nil model.connection.table_comment(model.table_name), json['comment']
17
26
  end
18
27
 
28
+ def assert_equal_or_nil(expected, actual, msg=nil)
29
+ if expected.nil?
30
+ assert_nil actual, msg
31
+ else
32
+ assert_equal expected, actual, msg
33
+ end
34
+ end
19
35
  end
20
36
  end
21
- end
37
+ end
@@ -1,3 +1,3 @@
1
1
  module StandardAPI
2
- VERSION = '6.0.0.24'
2
+ VERSION = '6.0.0.30'
3
3
  end
@@ -0,0 +1,68 @@
1
+ if model.nil? && controller_name == "application"
2
+ routes = Rails.application.routes.routes.reject(&:internal).collect do |route|
3
+ { name: route.name,
4
+ verb: route.verb,
5
+ path: route.path.spec.to_s.gsub(/\(\.format\)\Z/, ''),
6
+ controller: route.requirements[:controller],
7
+ action: route.requirements[:action],
8
+ array: ['index'].include?(route.requirements[:action]) }
9
+ end
10
+
11
+ json.set! 'comment', ActiveRecord::Base.connection.database_comment
12
+
13
+ json.set! 'routes' do
14
+ json.array!(routes) do |route|
15
+ controller = if controller_name = route[:controller]
16
+ begin
17
+ controller_param = controller_name.underscore
18
+ const_name = "#{controller_param.camelize}Controller"
19
+ const = ActiveSupport::Dependencies.constantize(const_name)
20
+ if const.ancestors.include?(StandardAPI::Controller)
21
+ const
22
+ else
23
+ nil
24
+ end
25
+ rescue NameError
26
+ end
27
+ end
28
+
29
+ next if controller.nil?
30
+
31
+ resource_limit = controller.resource_limit if controller.respond_to?(:resource_limit)
32
+
33
+ json.set! 'path', route[:path]
34
+ json.set! 'method', route[:verb]
35
+ json.set! 'model', controller.model&.name
36
+ json.set! 'array', route[:array]
37
+ json.set! 'limit', resource_limit
38
+ end
39
+ end
40
+
41
+ json.set! 'models' do
42
+ models.each do |model|
43
+ json.set! model.name do
44
+ json.partial! partial: schema_partial(model), model: model
45
+ end
46
+ end
47
+ end
48
+
49
+ else
50
+
51
+ json.set! 'attributes' do
52
+ model.columns.each do |column|
53
+ json.set! column.name, {
54
+ type: json_column_type(column.sql_type),
55
+ default: column.default || column.default_function,
56
+ primary_key: column.name == model.primary_key,
57
+ null: column.null,
58
+ array: column.array,
59
+ comment: column.comment
60
+ }
61
+ end
62
+ end
63
+
64
+ json.set! 'limit', resource_limit # This should be removed?
65
+ json.set! 'comment', model.connection.table_comment(model.table_name)
66
+
67
+ end
68
+
@@ -0,0 +1,78 @@
1
+ if model.nil? && controller_name == "application"
2
+ routes = Rails.application.routes.routes.reject(&:internal).collect do |route|
3
+ { name: route.name,
4
+ verb: route.verb,
5
+ path: route.path.spec.to_s.gsub(/\(\.:format\)\Z/, ''),
6
+ controller: route.requirements[:controller],
7
+ action: route.requirements[:action],
8
+ array: ['index'].include?(route.requirements[:action]) }
9
+ end
10
+
11
+ json.object! do
12
+ json.set! 'comment', ActiveRecord::Base.connection.database_comment
13
+
14
+ json.set! 'routes' do
15
+ json.array!(routes) do |route|
16
+ controller = if controller_name = route[:controller]
17
+ begin
18
+ controller_param = controller_name.underscore
19
+ const_name = "#{controller_param.camelize}Controller"
20
+ const = ActiveSupport::Dependencies.constantize(const_name)
21
+ if const.ancestors.include?(StandardAPI::Controller)
22
+ const
23
+ else
24
+ nil
25
+ end
26
+ rescue NameError
27
+ end
28
+ end
29
+
30
+ next if controller.nil?
31
+
32
+ resource_limit = controller.resource_limit if controller.respond_to?(:resource_limit)
33
+
34
+ json.object! do
35
+ json.set! 'path', route[:path]
36
+ json.set! 'method', route[:verb]
37
+ json.set! 'model', controller.model&.name
38
+ json.set! 'array', route[:array]
39
+ json.set! 'limit', resource_limit
40
+ end
41
+ end
42
+ end
43
+
44
+
45
+ json.set! 'models' do
46
+ json.object! do
47
+ models.each do |model|
48
+ json.set! model.name do
49
+ json.partial!(schema_partial(model), model: model)
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ end
56
+ else
57
+
58
+ json.object! do
59
+ json.set! 'attributes' do
60
+ json.object! do
61
+ model.columns.each do |column|
62
+ json.set! column.name, {
63
+ type: json_column_type(column.sql_type),
64
+ default: column.default || column.default_function,
65
+ primary_key: column.name == model.primary_key,
66
+ null: column.null,
67
+ array: column.array,
68
+ comment: column.comment
69
+ }
70
+ end
71
+ end
72
+ end
73
+
74
+ json.set! 'limit', resource_limit
75
+ json.set! 'comment', model.connection.table_comment(model.table_name)
76
+ end
77
+
78
+ end
@@ -1,12 +1 @@
1
- json.set! 'columns' do
2
- model.columns.each do |column|
3
- json.set! column.name, {
4
- type: json_column_type(column.sql_type),
5
- primary_key: column.name == model.primary_key,
6
- null: column.null,
7
- array: column.array
8
- }
9
- end
10
- end
11
-
12
- json.set! 'limit', resource_limit
1
+ json.partial!('schema', model: model)
@@ -1,16 +1 @@
1
- json.object! do
2
- json.set! 'columns' do
3
- json.object! do
4
- model.columns.each do |column|
5
- json.set! column.name, {
6
- type: json_column_type(column.sql_type),
7
- primary_key: column.name == model.primary_key,
8
- null: column.null,
9
- array: column.array
10
- }
11
- end
12
- end
13
- end
14
-
15
- json.set! 'limit', resource_limit
16
- end
1
+ json.partial!('schema', model: model)
@@ -0,0 +1,55 @@
1
+ require "rails"
2
+ require "active_model/railtie"
3
+ require "active_record/railtie"
4
+ require "action_controller/railtie"
5
+ require "action_view/railtie"
6
+ require "rails/test_unit/railtie"
7
+ Bundler.require(*Rails.groups)
8
+
9
+ require 'standard_api'
10
+
11
+ # Test Application Config
12
+ Rails.env = 'test'
13
+
14
+ class TestApplication < Rails::Application
15
+ config.root = File.join(File.dirname(__FILE__), 'test_app')
16
+ config.secret_key_base = 'test key base'
17
+ config.eager_load = true
18
+ config.cache_classes = true
19
+ config.action_controller.perform_caching = true
20
+ config.cache_store = :memory_store, { size: 8.megabytes }
21
+ config.action_dispatch.show_exceptions = false
22
+
23
+ # if defined?(FactoryBotRails)
24
+ # config.factory_bot.definition_file_paths += [ '../factories' ]
25
+ # end
26
+ end
27
+
28
+ # Test Application initialization
29
+ TestApplication.initialize!
30
+
31
+ # Test Application Models
32
+ require 'standard_api/test_app/models'
33
+
34
+ # Test Application Controllers
35
+ require 'standard_api/test_app/controllers'
36
+
37
+ # Test Application Routes
38
+ Rails.application.routes.draw do
39
+ get :tables, to: 'application#tables', as: :tables
40
+ get :schema, to: 'application#schema', as: :schema
41
+
42
+ [:properties, :photos, :documents, :references, :sessions, :unlimited, :default_limit].each do |r|
43
+ standard_resources r
44
+ end
45
+
46
+ standard_resource :account
47
+ end
48
+
49
+ # Test Application Helpers
50
+ Object.const_set(:ApplicationHelper, Module.new)
51
+
52
+ # require 'turbostreamer'
53
+ # require 'wankel'
54
+ # ActionView::Template.unregister_template_handler :jbuilder
55
+ # ActionView::Template.register_template_handler :streamer, TurboStreamer::Handler
@@ -0,0 +1,4 @@
1
+ test:
2
+ adapter: postgresql
3
+ database: standardapi-test
4
+ encoding: utf8
@@ -0,0 +1,107 @@
1
+ class ApplicationController < ActionController::Base
2
+ include StandardAPI::Controller
3
+ prepend_view_path File.join(File.dirname(__FILE__), 'views')
4
+
5
+ private
6
+
7
+ def account_params
8
+ [ "property_id", "name" ]
9
+ end
10
+
11
+ def account_orders
12
+ [ "id" ]
13
+ end
14
+
15
+ def account_includes
16
+ [ "photos" ]
17
+ end
18
+
19
+ def property_params
20
+ [ :name,
21
+ :aliases,
22
+ :description,
23
+ :constructed,
24
+ :size,
25
+ :active,
26
+ :photos_attributes,
27
+ { photos_attributes: [ :id, :account_id, :property_id, :format] }
28
+ ]
29
+ end
30
+
31
+ def property_orders
32
+ ["id", "name", "aliases", "description", "constructed", "size", "created_at", "active"]
33
+ end
34
+
35
+ def property_includes
36
+ [:photos, :landlord, :english_name]
37
+ end
38
+
39
+ def reference_includes
40
+ { subject: [ :landlord, :photos ] }
41
+ end
42
+
43
+ end
44
+
45
+ class PropertiesController < ApplicationController
46
+ end
47
+
48
+ class AccountsController < ApplicationController
49
+ end
50
+
51
+ class DocumentsController < ApplicationController
52
+
53
+ def document_params
54
+ [ :file, :type ]
55
+ end
56
+
57
+ def document_orders
58
+ [:id]
59
+ end
60
+
61
+ end
62
+
63
+ class PhotosController < ApplicationController
64
+
65
+ def photo_params
66
+ [ :id, :account_id, :property_id, :format ]
67
+ end
68
+
69
+ def photo_orders
70
+ [:id]
71
+ end
72
+
73
+ def photo_includes
74
+ [:account]
75
+ end
76
+
77
+ end
78
+
79
+ class ReferencesController < ApplicationController
80
+ end
81
+
82
+ class SessionsController < ApplicationController
83
+ end
84
+
85
+ class UnlimitedController < ApplicationController
86
+
87
+ def self.model
88
+ Account
89
+ end
90
+
91
+ def resource_limit
92
+ nil
93
+ end
94
+
95
+ end
96
+
97
+ class DefaultLimitController < ApplicationController
98
+
99
+ def self.model
100
+ Account
101
+ end
102
+
103
+ def default_limit
104
+ 100
105
+ end
106
+
107
+ end
@@ -0,0 +1,94 @@
1
+ # = Models
2
+
3
+ class Account < ActiveRecord::Base
4
+ has_many :photos
5
+ belongs_to :property
6
+ end
7
+
8
+ class Photo < ActiveRecord::Base
9
+ belongs_to :account, :counter_cache => true
10
+ has_and_belongs_to_many :properties
11
+ end
12
+
13
+ class Document < ActiveRecord::Base
14
+ attr_accessor :file
15
+ end
16
+
17
+ class Pdf < Document
18
+ end
19
+
20
+ class Property < ActiveRecord::Base
21
+ has_and_belongs_to_many :photos
22
+ has_many :accounts
23
+ has_one :landlord, class_name: 'Account'
24
+
25
+ validates :name, presence: true
26
+ accepts_nested_attributes_for :photos
27
+
28
+ def english_name
29
+ 'A Name'
30
+ end
31
+ end
32
+
33
+ class Reference < ActiveRecord::Base
34
+ belongs_to :subject, polymorphic: true
35
+ end
36
+
37
+ # = Migration
38
+
39
+ class CreateModelTables < ActiveRecord::Migration[6.0]
40
+
41
+ def self.up
42
+
43
+ comment = "test comment"
44
+ exec_query(<<-SQL, "SQL")
45
+ COMMENT ON DATABASE #{quote_column_name(current_database)} IS #{quote(comment)};
46
+ SQL
47
+
48
+ create_table "accounts", force: :cascade do |t|
49
+ t.string 'name', limit: 255
50
+ t.integer 'property_id'
51
+ t.integer 'photos_count', null: false, default: 0
52
+ end
53
+
54
+ create_table "photos", force: :cascade do |t|
55
+ t.integer "account_id"
56
+ t.integer "property_id"
57
+ t.string "format", limit: 255
58
+ end
59
+
60
+ create_table "properties", force: :cascade do |t|
61
+ t.string "name", limit: 255
62
+ t.string "aliases", default: [], array: true
63
+ t.text "description"
64
+ t.integer "constructed"
65
+ t.decimal "size"
66
+ t.datetime "created_at", null: false
67
+ t.boolean "active", default: false
68
+ end
69
+
70
+ create_table "references", force: :cascade do |t|
71
+ t.integer "subject_id"
72
+ t.string "subject_type", limit: 255
73
+ t.string "key"
74
+ t.string "value"
75
+ end
76
+
77
+ create_table "photos_properties", force: :cascade do |t|
78
+ t.integer "photo_id"
79
+ t.integer "property_id"
80
+ end
81
+
82
+ create_table "landlords_properties", force: :cascade do |t|
83
+ t.integer "landlord_id"
84
+ t.integer "property_id"
85
+ end
86
+
87
+ create_table "documents", force: :cascade do |t|
88
+ t.string 'type'
89
+ end
90
+ end
91
+
92
+ end
93
+ ActiveRecord::Migration.verbose = false
94
+ CreateModelTables.up
@@ -0,0 +1,50 @@
1
+ FactoryBot.define do
2
+ factory :account do
3
+ name { Faker::Name.name }
4
+
5
+ trait(:nested) { }
6
+ trait(:invalid) do
7
+ name { nil }
8
+ end
9
+ end
10
+
11
+ factory :landlord do
12
+ name { Faker::Name.name }
13
+ end
14
+
15
+ factory :photo do
16
+ format { ['jpg', 'png', 'tiff'].sample }
17
+ end
18
+
19
+ factory :document do
20
+ file { fixture_file_upload(Rails.root + 'test/fixtures/photo.png', 'image/png') }
21
+ end
22
+
23
+ factory :pdf do
24
+ type { 'Pdf' }
25
+ file { fixture_file_upload(Rails.root + 'test/fixtures/photo.png', 'image/png') }
26
+ end
27
+
28
+ factory :reference do
29
+ subject_type { 'Photo' }
30
+ subject_id { create(:photo).id }
31
+ end
32
+
33
+ factory :property do
34
+ name { Faker::Lorem.words(number: Kernel.rand(1..4)).join(' ') }
35
+ description { Faker::Lorem.paragraphs.join("\n\n") }
36
+ constructed { Kernel.rand(1800..(Time.now.year - 2)) }
37
+ size { Kernel.rand(1000..10000000).to_f / 100 }
38
+ active { [true, false].sample }
39
+ photos { [create(:photo)] }
40
+
41
+ trait(:nested) do
42
+ photos_attributes { [attributes_for(:photo)] }
43
+ end
44
+
45
+ trait(:invalid) do
46
+ name { nil }
47
+ end
48
+ end
49
+
50
+ end
@@ -0,0 +1,15 @@
1
+ json.set! :id, photo.id
2
+ json.set! :account_id, photo.account_id
3
+ json.set! :property_id, photo.property_id
4
+ json.set! :format, photo.format
5
+ json.set! :template, 'photos/_photo'
6
+
7
+ if includes[:account]
8
+ json.set! :account do
9
+ if photo.account
10
+ json.partial! 'application/record', record: photo.account, includes: includes[:account]
11
+ else
12
+ json.null!
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ json.object! do
2
+ json.set! :id, photo.id
3
+ json.set! :account_id, photo.account_id
4
+ json.set! :property_id, photo.property_id
5
+ json.set! :format, photo.format
6
+ json.set! :template, 'photos/_photo'
7
+
8
+ if includes[:account]
9
+ json.set! :account do
10
+ if photo.account
11
+ json.partial! 'application/record', record: photo.account, includes: includes[:account]
12
+ else
13
+ json.null!
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1 @@
1
+ json.set! 'template', 'photos/schema'
@@ -0,0 +1,3 @@
1
+ json.object! do
2
+ json.set! 'template', 'photos/schema'
3
+ end
@@ -0,0 +1 @@
1
+ json.partial! 'schema'
@@ -0,0 +1 @@
1
+ json.partial! 'schema'
@@ -0,0 +1 @@
1
+ properties#edit.html
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: standardapi
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.0.0.24
4
+ version: 6.0.0.30
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Bracy
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-10-14 00:00:00.000000000 Z
11
+ date: 2020-02-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -245,6 +245,7 @@ extra_rdoc_files:
245
245
  files:
246
246
  - README.md
247
247
  - lib/standard_api.rb
248
+ - lib/standard_api/active_record/connection_adapters/postgresql/schema_statements.rb
248
249
  - lib/standard_api/controller.rb
249
250
  - lib/standard_api/errors.rb
250
251
  - lib/standard_api/helpers.rb
@@ -265,6 +266,8 @@ files:
265
266
  - lib/standard_api/version.rb
266
267
  - lib/standard_api/views/application/_record.json.jbuilder
267
268
  - lib/standard_api/views/application/_record.streamer
269
+ - lib/standard_api/views/application/_schema.json.jbuilder
270
+ - lib/standard_api/views/application/_schema.streamer
268
271
  - lib/standard_api/views/application/index.json.jbuilder
269
272
  - lib/standard_api/views/application/index.streamer
270
273
  - lib/standard_api/views/application/new.json.jbuilder
@@ -273,6 +276,20 @@ files:
273
276
  - lib/standard_api/views/application/schema.streamer
274
277
  - lib/standard_api/views/application/show.json.jbuilder
275
278
  - lib/standard_api/views/application/show.streamer
279
+ - test/standard_api/test_app.rb
280
+ - test/standard_api/test_app/config/database.yml
281
+ - test/standard_api/test_app/controllers.rb
282
+ - test/standard_api/test_app/models.rb
283
+ - test/standard_api/test_app/test/factories.rb
284
+ - test/standard_api/test_app/test/fixtures/photo.png
285
+ - test/standard_api/test_app/views/photos/_photo.json.jbuilder
286
+ - test/standard_api/test_app/views/photos/_photo.streamer
287
+ - test/standard_api/test_app/views/photos/_schema.json.jbuilder
288
+ - test/standard_api/test_app/views/photos/_schema.streamer
289
+ - test/standard_api/test_app/views/photos/schema.json.jbuilder
290
+ - test/standard_api/test_app/views/photos/schema.streamer
291
+ - test/standard_api/test_app/views/properties/edit.html.erb
292
+ - test/standard_api/test_app/views/sessions/new.html.erb
276
293
  homepage: https://github.com/waratuman/standardapi
277
294
  licenses:
278
295
  - MIT
@@ -283,6 +300,7 @@ rdoc_options:
283
300
  - README.md
284
301
  require_paths:
285
302
  - lib
303
+ - test
286
304
  required_ruby_version: !ruby/object:Gem::Requirement
287
305
  requirements:
288
306
  - - ">="