standardapi 6.0.0.24 → 6.0.0.30

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 (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
  - - ">="