brainstem 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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,