brainstem 2.0.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +147 -0
  3. data/Gemfile.lock +68 -39
  4. data/lib/brainstem/api_docs.rb +9 -4
  5. data/lib/brainstem/api_docs/atlas.rb +3 -3
  6. data/lib/brainstem/api_docs/controller.rb +12 -4
  7. data/lib/brainstem/api_docs/controller_collection.rb +11 -2
  8. data/lib/brainstem/api_docs/endpoint.rb +17 -7
  9. data/lib/brainstem/api_docs/endpoint_collection.rb +9 -1
  10. data/lib/brainstem/api_docs/formatters/open_api_specification/helper.rb +19 -16
  11. data/lib/brainstem/api_docs/formatters/open_api_specification/version_2/endpoint/param_definitions_formatter.rb +52 -80
  12. data/lib/brainstem/api_docs/formatters/open_api_specification/version_2/endpoint/response_definitions_formatter.rb +64 -84
  13. data/lib/brainstem/api_docs/formatters/open_api_specification/version_2/endpoint_formatter.rb +1 -1
  14. data/lib/brainstem/api_docs/formatters/open_api_specification/version_2/field_definitions/endpoint_param_formatter.rb +39 -0
  15. data/lib/brainstem/api_docs/formatters/open_api_specification/version_2/field_definitions/presenter_field_formatter.rb +147 -0
  16. data/lib/brainstem/api_docs/formatters/open_api_specification/version_2/field_definitions/response_field_formatter.rb +146 -0
  17. data/lib/brainstem/api_docs/formatters/open_api_specification/version_2/presenter_formatter.rb +53 -55
  18. data/lib/brainstem/api_docs/formatters/open_api_specification/version_2/tags_formatter.rb +1 -1
  19. data/lib/brainstem/api_docs/presenter.rb +16 -8
  20. data/lib/brainstem/api_docs/presenter_collection.rb +8 -5
  21. data/lib/brainstem/api_docs/sinks/open_api_specification_sink.rb +3 -1
  22. data/lib/brainstem/cli/generate_api_docs_command.rb +4 -0
  23. data/lib/brainstem/concerns/controller_dsl.rb +90 -20
  24. data/lib/brainstem/concerns/presenter_dsl.rb +16 -8
  25. data/lib/brainstem/dsl/association.rb +12 -0
  26. data/lib/brainstem/dsl/fields_block.rb +1 -1
  27. data/lib/brainstem/version.rb +1 -1
  28. data/spec/brainstem/api_docs/controller_spec.rb +127 -5
  29. data/spec/brainstem/api_docs/endpoint_spec.rb +489 -57
  30. data/spec/brainstem/api_docs/formatters/open_api_specification/helper_spec.rb +15 -4
  31. data/spec/brainstem/api_docs/formatters/open_api_specification/version_2/endpoint/param_definitions_formatter_spec.rb +112 -66
  32. data/spec/brainstem/api_docs/formatters/open_api_specification/version_2/endpoint/response_definitions_formatter_spec.rb +404 -32
  33. data/spec/brainstem/api_docs/formatters/open_api_specification/version_2/field_definitions/endpoint_param_formatter_spec.rb +335 -0
  34. data/spec/brainstem/api_docs/formatters/open_api_specification/version_2/field_definitions/presenter_field_formatter_spec.rb +237 -0
  35. data/spec/brainstem/api_docs/formatters/open_api_specification/version_2/field_definitions/response_field_formatter_spec.rb +413 -0
  36. data/spec/brainstem/api_docs/formatters/open_api_specification/version_2/presenter_formatter_spec.rb +116 -4
  37. data/spec/brainstem/api_docs/presenter_spec.rb +406 -24
  38. data/spec/brainstem/cli/generate_api_docs_command_spec.rb +8 -0
  39. data/spec/brainstem/concerns/controller_dsl_spec.rb +606 -45
  40. data/spec/brainstem/concerns/presenter_dsl_spec.rb +34 -2
  41. data/spec/brainstem/dsl/association_spec.rb +54 -3
  42. metadata +11 -2
@@ -7,6 +7,14 @@ module Brainstem
7
7
  class EndpointCollection < AbstractCollection
8
8
  include Concerns::Formattable
9
9
 
10
+ attr_accessor :include_internal
11
+
12
+ def valid_options
13
+ super | [
14
+ :include_internal
15
+ ]
16
+ end
17
+
10
18
  def find_from_route(route)
11
19
  find do |endpoint|
12
20
  endpoint.path == route[:path] &&
@@ -18,7 +26,7 @@ module Brainstem
18
26
  alias_method :find_by_route, :find_from_route
19
27
 
20
28
  def create_from_route(route, controller)
21
- Endpoint.new(atlas) do |ep|
29
+ Endpoint.new(atlas, include_internal: self.include_internal) do |ep|
22
30
  ep.path = route[:path]
23
31
  ep.http_methods = route[:http_methods]
24
32
  ep.controller = controller
@@ -18,7 +18,7 @@ module Brainstem
18
18
  name.underscore.titleize.strip
19
19
  end
20
20
 
21
- def format_description(description)
21
+ def format_sentence(description)
22
22
  return '' if description.blank?
23
23
 
24
24
  desc = description.to_s.strip.tap { |desc| desc[0] = desc[0].upcase }
@@ -32,11 +32,10 @@ module Brainstem
32
32
  description.strip.tap { |desc| desc[0] = desc[0].downcase }
33
33
  end
34
34
 
35
- # TODO: multi nested
36
35
  def type_and_format(type, item_type = nil)
37
36
  result = case type.to_s.downcase
38
37
  when 'array'
39
- { 'type' => 'array', 'items' => { 'type' => item_type.presence || 'string' } }
38
+ { 'type' => 'array', 'items' => type_and_format(item_type.presence || 'string') }
40
39
  else
41
40
  TYPE_INFO[type.to_s]
42
41
  end
@@ -44,19 +43,23 @@ module Brainstem
44
43
  end
45
44
 
46
45
  TYPE_INFO = {
47
- 'string' => { 'type' => 'string' },
48
- 'boolean' => { 'type' => 'boolean' },
49
- 'integer' => { 'type' => 'integer', 'format' => 'int32' },
50
- 'long' => { 'type' => 'integer', 'format' => 'int64' },
51
- 'float' => { 'type' => 'number', 'format' => 'float' },
52
- 'double' => { 'type' => 'number', 'format' => 'double' },
53
- 'byte' => { 'type' => 'string', 'format' => 'byte' },
54
- 'binary' => { 'type' => 'string', 'format' => 'binary' },
55
- 'date' => { 'type' => 'string', 'format' => 'date' },
56
- 'datetime' => { 'type' => 'string', 'format' => 'date-time' },
57
- 'password' => { 'type' => 'string', 'format' => 'password' },
58
- 'id' => { 'type' => 'integer', 'format' => 'int32' },
59
- 'decimal' => { 'type' => 'number', 'format' => 'float' },
46
+ 'string' => { 'type' => 'string' },
47
+ 'boolean' => { 'type' => 'boolean' },
48
+ 'integer' => { 'type' => 'integer', 'format' => 'int32' },
49
+ 'long' => { 'type' => 'integer', 'format' => 'int64' },
50
+ 'float' => { 'type' => 'number', 'format' => 'float' },
51
+ 'double' => { 'type' => 'number', 'format' => 'double' },
52
+ 'byte' => { 'type' => 'string', 'format' => 'byte' },
53
+ 'binary' => { 'type' => 'string', 'format' => 'binary' },
54
+ 'date' => { 'type' => 'string', 'format' => 'date' },
55
+ 'datetime' => { 'type' => 'string', 'format' => 'date-time' },
56
+ 'password' => { 'type' => 'string', 'format' => 'password' },
57
+ 'id' => { 'type' => 'integer', 'format' => 'int32' },
58
+ 'decimal' => { 'type' => 'number', 'format' => 'float' },
59
+ 'csv' => { 'type' => 'string', 'collectionFormat' => 'csv' },
60
+ 'ssv' => { 'type' => 'string', 'collectionFormat' => 'ssv' },
61
+ 'tsv' => { 'type' => 'string', 'collectionFormat' => 'tsv' },
62
+ 'pipes' => { 'type' => 'string', 'collectionFormat' => 'pipes' },
60
63
  }
61
64
  private_constant :TYPE_INFO
62
65
  end
@@ -3,6 +3,7 @@ require 'active_support/core_ext/hash/compact'
3
3
  require 'active_support/inflector'
4
4
  require 'brainstem/api_docs/formatters/abstract_formatter'
5
5
  require 'brainstem/api_docs/formatters/open_api_specification/helper'
6
+ require 'brainstem/api_docs/formatters/open_api_specification/version_2/field_definitions/endpoint_param_formatter'
6
7
  require 'brainstem/api_docs/formatters/markdown/helper'
7
8
 
8
9
  #
@@ -30,17 +31,19 @@ module Brainstem
30
31
  def call
31
32
  format_path_params!
32
33
 
33
- if endpoint.action == 'index'
34
- format_pagination_params!
35
- format_search_param!
36
- format_only_param!
37
- format_sort_order_params!
38
- format_filter_params!
39
- end
34
+ if presenter
35
+ if endpoint.action == 'index'
36
+ format_pagination_params!
37
+ format_search_param!
38
+ format_only_param!
39
+ format_sort_order_params!
40
+ format_filter_params!
41
+ end
40
42
 
41
- if http_method != 'delete'
42
- format_optional_params!
43
- format_include_params!
43
+ if http_method != 'delete'
44
+ format_optional_params!
45
+ format_include_params!
46
+ end
44
47
  end
45
48
 
46
49
  format_query_params!
@@ -104,8 +107,12 @@ module Brainstem
104
107
  def format_sort_order_params!
105
108
  return if presenter.nil? || (valid_sort_orders = presenter.valid_sort_orders).empty?
106
109
 
107
- sort_orders = valid_sort_orders.map { |sort_name, _|
108
- [md_inline_code("#{sort_name}:asc"), md_inline_code("#{sort_name}:desc")]
110
+ sort_orders = valid_sort_orders.map { |sort_name, sort_config|
111
+ if sort_config[:direction]
112
+ [md_inline_code("#{sort_name}:asc"), md_inline_code("#{sort_name}:desc")]
113
+ else
114
+ md_inline_code(sort_name)
115
+ end
109
116
  }.flatten.sort
110
117
 
111
118
  description = <<-DESC.strip_heredoc
@@ -160,7 +167,7 @@ module Brainstem
160
167
  text = md_inline_code(association.name)
161
168
  text += " (#{ association.target_class.to_s })"
162
169
 
163
- desc = format_description(association.description)
170
+ desc = format_sentence(association.description)
164
171
  if association.options && association.options[:restrict_to_only]
165
172
  desc += " Restricted to queries using the #{md_inline_code("only")} parameter."
166
173
  end
@@ -208,13 +215,15 @@ module Brainstem
208
215
  'in' => 'query',
209
216
  'name' => param_name.to_s,
210
217
  'required' => param_config[:required],
211
- 'description' => format_description(param_config[:info]).presence,
218
+ 'description' => format_sentence(param_config[:info]).presence,
212
219
  }.merge(type_data).compact
213
220
  end
214
221
 
215
222
  def format_body_params!
216
- formatted_body_params = format_body_params
217
- return if formatted_body_params.blank?
223
+ properties, additional_properties = split_properties(endpoint.params_configuration_tree)
224
+ formatted_body_params = format_field_properties(properties)
225
+ formatted_dynamic_key_params = format_field_properties(additional_properties)
226
+ return if formatted_body_params.blank? && formatted_dynamic_key_params.blank?
218
227
 
219
228
  output << {
220
229
  'in' => 'body',
@@ -222,82 +231,45 @@ module Brainstem
222
231
  'name' => 'body',
223
232
  'schema' => {
224
233
  'type' => 'object',
225
- 'properties' => formatted_body_params
226
- },
234
+ 'properties' => formatted_body_params,
235
+ 'additionalProperties' => formatted_dynamic_key_params,
236
+ }.with_indifferent_access.reject { |_, v| v.blank? },
227
237
  }
228
238
  end
229
239
 
230
- def format_body_params
231
- ActiveSupport::HashWithIndifferentAccess.new.tap do |body_params|
232
- endpoint.params_configuration_tree.each do |param_name, param_config|
233
- next if nested_properties(param_config).blank?
234
-
235
- body_params[param_name] = format_parent_param(param_name, param_config)
236
- end
237
- end
238
- end
239
-
240
- def format_parent_param(param_name, param_data)
241
- param_config = param_data[:_config]
242
- result = case param_config[:type]
243
- when 'hash'
244
- {
245
- type: 'object',
246
- title: param_name.to_s,
247
- description: format_description(param_config[:info]),
248
- properties: format_param_branch(nested_properties(param_data))
249
- }
250
- when 'array'
251
- {
252
- type: 'array',
253
- title: param_name.to_s,
254
- description: format_description(param_config[:info]),
255
- items: {
256
- type: 'object',
257
- properties: format_param_branch(nested_properties(param_data))
258
- }
259
- }
240
+ def split_properties(field_properties)
241
+ split_properties = field_properties.each_with_object({ properties: {}, additional_properties: {} }) do |(field_name, field_config), acc|
242
+ if field_config[:_config][:dynamic_key]
243
+ acc[:additional_properties][field_name] = field_config
260
244
  else
261
- raise "Unknown Brainstem body param encountered(#{param_config[:type]}) for field #{param_name}"
245
+ acc[:properties][field_name] = field_config
246
+ end
262
247
  end
263
248
 
264
- result.with_indifferent_access.reject { |_, v| v.blank? }
249
+ [split_properties[:properties], split_properties[:additional_properties]]
265
250
  end
266
251
 
267
- def format_param_branch(branch)
268
- branch.inject(ActiveSupport::HashWithIndifferentAccess.new) do |buffer, (param_name, param_data)|
269
- nested_properties = nested_properties(param_data)
270
- param_config = param_data[:_config]
271
-
272
- branch_schema = if nested_properties.present?
273
- case param_config[:type].to_s
274
- when 'hash'
275
- { type: 'object', properties: format_param_branch(nested_properties) }
276
- when 'array'
277
- {
278
- type: 'array',
279
- items: { type: 'object', properties: format_param_branch(nested_properties) }
280
- }
281
- else
282
- raise "Unknown Brainstem Param type encountered(#{param_config[:type]}) for param #{param_name}"
283
- end
252
+ def format_field_properties(branches)
253
+ branches.inject(ActiveSupport::HashWithIndifferentAccess.new) do |buffer, (field_name, field_config)|
254
+ if dynamic_key_field?(field_config)
255
+ formatted_field('Dynamic Key Field', field_config)
284
256
  else
285
- param_data = type_and_format(param_config[:type].to_s, param_config[:item_type])
286
-
287
- if param_data.blank?
288
- raise "Unknown Brainstem Param type encountered(#{param_config[:type]}) for param #{param_name}"
289
- end
290
-
291
- param_data
257
+ buffer[field_name.to_s] = formatted_field(field_name, field_config) if nested_properties(field_config).present?
258
+ buffer
292
259
  end
260
+ end
261
+ end
293
262
 
294
- buffer[param_name.to_s] = {
295
- title: param_name.to_s,
296
- description: format_description(param_config[:info])
297
- }.merge(branch_schema).reject { |_, v| v.blank? }
263
+ def dynamic_key_field?(param_config)
264
+ param_config[:_config][:dynamic_key].presence
265
+ end
298
266
 
299
- buffer
300
- end
267
+ def formatted_field(param_name, param_data)
268
+ Brainstem::ApiDocs::FORMATTERS[:endpoint_param][:oas_v2].call(
269
+ endpoint,
270
+ param_name,
271
+ param_data
272
+ )
301
273
  end
302
274
  end
303
275
  end
@@ -2,6 +2,7 @@ require 'active_support/core_ext/hash/except'
2
2
  require 'active_support/inflector'
3
3
  require 'brainstem/api_docs/formatters/abstract_formatter'
4
4
  require 'brainstem/api_docs/formatters/open_api_specification/helper'
5
+ require 'brainstem/api_docs/formatters/open_api_specification/version_2/field_definitions/response_field_formatter'
5
6
  require 'forwardable'
6
7
 
7
8
  #
@@ -72,41 +73,70 @@ module Brainstem
72
73
  def format_schema_response!
73
74
  return if presenter.nil?
74
75
 
75
- brainstem_key = presenter.brainstem_keys.first
76
- model_klass = presenter.target_class
77
-
78
76
  output.merge! '200' => {
79
77
  description: success_response_description,
80
78
  schema: {
79
+ type: 'object',
80
+ properties: properties
81
+ }
82
+ }
83
+ end
84
+
85
+ def properties
86
+ brainstem_key = presenter.brainstem_keys.first
87
+ model_klass = presenter.target_class
88
+
89
+ {
90
+ count: type_and_format('integer'),
91
+ meta: {
81
92
  type: 'object',
82
93
  properties: {
83
94
  count: type_and_format('integer'),
84
- meta: {
85
- type: 'object',
86
- properties: {
87
- count: type_and_format('integer'),
88
- page_count: type_and_format('integer'),
89
- page_number: type_and_format('integer'),
90
- page_size: type_and_format('integer'),
91
- }
92
- },
93
- results: {
94
- type: 'array',
95
- items: {
96
- type: 'object',
97
- properties: {
98
- key: type_and_format('string'),
99
- id: type_and_format('string')
100
- }
101
- }
102
- },
103
- brainstem_key => {
104
- type: 'object',
105
- additionalProperties: {
106
- '$ref' => "#/definitions/#{model_klass}"
107
- }
95
+ page_count: type_and_format('integer'),
96
+ page_number: type_and_format('integer'),
97
+ page_size: type_and_format('integer'),
98
+ }
99
+ },
100
+ results: {
101
+ type: 'array',
102
+ items: {
103
+ type: 'object',
104
+ properties: {
105
+ key: type_and_format('string'),
106
+ id: type_and_format('string')
108
107
  }
109
108
  }
109
+ },
110
+ brainstem_key => object_reference(model_klass)
111
+ }.merge(associated_properties)
112
+ end
113
+
114
+ def associated_properties
115
+ presenter.valid_associations.each_with_object({}) do |(_key, association), obj|
116
+ if association.polymorphic?
117
+ associated_klasses = association.polymorphic_classes || []
118
+ associated_klasses.each do |assoc|
119
+ association_reference(assoc, obj)
120
+ end
121
+ else
122
+ association_reference(association.target_class, obj)
123
+ end
124
+ end
125
+ end
126
+
127
+ def association_reference(target_class, obj)
128
+ assoc_presenter = presenter.find_by_class(target_class)
129
+ brainstem_key = assoc_presenter.brainstem_keys.first
130
+ return if assoc_presenter.nodoc?
131
+
132
+ obj[brainstem_key] = object_reference(target_class)
133
+ end
134
+
135
+ def object_reference(klass)
136
+ {
137
+ type: 'object',
138
+ additionalProperties: {
139
+ '$ref' => "#/definitions/#{klass}"
110
140
  }
111
141
  }
112
142
  end
@@ -124,66 +154,16 @@ module Brainstem
124
154
  def format_custom_response!
125
155
  output.merge! '200' => {
126
156
  description: success_response_description,
127
- schema: format_response(endpoint.custom_response_configuration_tree)
157
+ schema: format_response!
128
158
  }
129
159
  end
130
160
 
131
- def format_response(response_tree)
132
- response_config = response_tree[:_config]
133
- response_branches = response_tree.except(:_config)
134
-
135
- format_response_field(response_config, response_branches)
136
- end
137
-
138
- def format_response_field(field_config, field_branches)
139
- if field_branches.present?
140
- formed_nested_field(field_config, field_branches)
141
- else
142
- format_response_leaf(field_config)
143
- end
144
- end
145
-
146
- def format_response_leaf(field_config)
147
- field_data = type_and_format(field_config[:type], field_config[:item_type])
148
-
149
- unless field_data
150
- raise "Unknown Brainstem Field type encountered(#{field_config[:type]}) for field #{field_config[:name]}"
151
- end
152
-
153
- field_data.merge!(description: format_description(field_config[:info])) if field_config[:info].present?
154
- field_data
155
- end
156
-
157
- def formed_nested_field(field_config, field_branches)
158
- result = case field_config[:type]
159
- when 'hash'
160
- {
161
- type: 'object',
162
- description: format_description(field_config[:info]),
163
- properties: format_response_branches(field_branches)
164
- }
165
- when 'array'
166
- {
167
- type: 'array',
168
- description: format_description(field_config[:info]),
169
- items: {
170
- type: 'object',
171
- properties: format_response_branches(field_branches)
172
- }
173
- }
174
- end
175
-
176
- result.with_indifferent_access.reject { |_, v| v.blank? }
177
- end
178
-
179
- def format_response_branches(branches)
180
- branches.inject(ActiveSupport::HashWithIndifferentAccess.new) do |buffer, (field_name, field_config)|
181
- config = field_config[:_config]
182
- branches = field_config.except(:_config)
183
-
184
- buffer[field_name.to_s] = format_response_field(config, branches)
185
- buffer
186
- end
161
+ def format_response!
162
+ Brainstem::ApiDocs::FORMATTERS[:response_field][:oas_v2].call(
163
+ endpoint,
164
+ 'schema',
165
+ endpoint.custom_response_configuration_tree
166
+ )
187
167
  end
188
168
  end
189
169
  end
@@ -104,7 +104,7 @@ module Brainstem
104
104
  #
105
105
  def format_optional_info!
106
106
  info = {
107
- description: format_description(description),
107
+ description: format_sentence(description),
108
108
  operation_id: endpoint.operation_id,
109
109
  consumes: endpoint.consumes,
110
110
  produces: endpoint.produces,