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
@@ -0,0 +1,39 @@
1
+ require 'brainstem/api_docs/formatters/open_api_specification/version_2/field_definitions/response_field_formatter'
2
+
3
+ module Brainstem
4
+ module ApiDocs
5
+ module Formatters
6
+ module OpenApiSpecification
7
+ module Version2
8
+ module FieldDefinitions
9
+ class EndpointParamFormatter < ResponseFieldFormatter
10
+ def initialize(endpoint, param_name, param_tree)
11
+ @endpoint = endpoint
12
+ @param_name = param_name
13
+ @param_tree = param_tree
14
+ end
15
+
16
+ def format_object_field(field_config, field_properties, include_description = true)
17
+ super(field_config, field_properties, include_description).tap do |field_schema|
18
+ if (required_props = required_properties(field_properties)).present?
19
+ field_schema[:required] = required_props
20
+ end
21
+ end
22
+ end
23
+
24
+ def required_properties(field_properties)
25
+ field_properties.select do |_, property_data|
26
+ !property_data[:_config][:dynamic_key] && property_data[:_config][:required]
27
+ end.keys
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ Brainstem::ApiDocs::FORMATTERS[:endpoint_param][:oas_v2] =
38
+ Brainstem::ApiDocs::Formatters::OpenApiSpecification::Version2::FieldDefinitions::EndpointParamFormatter.method(:call)
39
+
@@ -0,0 +1,147 @@
1
+ require 'brainstem/api_docs'
2
+ require 'brainstem/api_docs/formatters/abstract_formatter'
3
+ require 'brainstem/api_docs/formatters/open_api_specification/helper'
4
+
5
+ module Brainstem
6
+ module ApiDocs
7
+ module Formatters
8
+ module OpenApiSpecification
9
+ module Version2
10
+ module FieldDefinitions
11
+ class PresenterFieldFormatter < AbstractFormatter
12
+ include Helper
13
+
14
+ def initialize(presenter, field)
15
+ @presenter = presenter
16
+ @field = field
17
+ end
18
+
19
+ def format
20
+ format_field(@field)
21
+ end
22
+ alias_method :call, :format
23
+
24
+ private
25
+
26
+ attr_reader :presenter
27
+
28
+ def has_properties?(field)
29
+ field.respond_to?(:configuration)
30
+ end
31
+
32
+ def format_field(field)
33
+ if field.options[:nested_levels]
34
+ format_nested_array_field(field)
35
+ elsif has_properties?(field)
36
+ format_nested_field(field)
37
+ else
38
+ format_simple_field(field)
39
+ end
40
+ end
41
+
42
+ def format_nested_array_field(field)
43
+ field_properties_data, nested_levels = format_array_items(field)
44
+
45
+ format_nested_array_parent(nested_levels, field_properties_data, format_description(field))
46
+ end
47
+
48
+ def format_array_items(field)
49
+ field_nested_levels = field.options[:nested_levels]
50
+
51
+ if has_properties?(field)
52
+ [format_nested_field(field, false), field_nested_levels - 1]
53
+ else
54
+ [type_and_format(field.options[:item_type]), field_nested_levels]
55
+ end
56
+ end
57
+
58
+ def format_nested_array_parent(nested_level, formatted_data, description = nil)
59
+ if nested_level == 1
60
+ {
61
+ 'type' => 'array',
62
+ 'description' => description,
63
+ 'items' => formatted_data
64
+ }
65
+ else
66
+ {
67
+ 'type' => 'array',
68
+ 'description' => description,
69
+ 'items' => format_nested_array_parent(nested_level - 1, formatted_data)
70
+ }
71
+ end.with_indifferent_access.reject { |_, v| v.blank? }
72
+ end
73
+
74
+ def format_nested_field(field, include_description = true)
75
+ case field.type
76
+ when 'hash'
77
+ format_object_field(field, include_description)
78
+ when 'array'
79
+ {
80
+ type: 'array',
81
+ description: include_description && format_description(field),
82
+ items: format_object_field(field, false)
83
+ }
84
+ end.with_indifferent_access.reject { |_, v| v.blank? }
85
+ end
86
+
87
+ def format_object_field(field, include_description = true)
88
+ {
89
+ type: 'object',
90
+ description: include_description && format_description(field),
91
+ properties: format_field_properties(field.to_h),
92
+ }.with_indifferent_access.reject { |_, v| v.blank? }
93
+ end
94
+
95
+ def format_simple_field(field)
96
+ field_data = type_and_format(field.type) || raise(invalid_type_error_message(field))
97
+ field_data.merge!(description: format_description(field))
98
+ field_data.with_indifferent_access.reject { |_, v| v.blank? }
99
+ end
100
+
101
+ def invalid_type_error_message(field)
102
+ <<-MSG.strip_heredoc
103
+ Unknown Brainstem Field type encountered(#{field.type}) for field #{field.name}
104
+ in #{presenter.target_class.to_s}.
105
+ MSG
106
+ end
107
+
108
+ def format_field_properties(branches)
109
+ branches.inject(ActiveSupport::HashWithIndifferentAccess.new) do |buffer, (field_name, field)|
110
+ buffer[field_name.to_s] = format_field(field)
111
+ buffer
112
+ end
113
+ end
114
+
115
+ def format_description(field)
116
+ field_description = format_sentence(field.description) || ''
117
+ field_description << format_conditional_description(field.options)
118
+ if field.optional?
119
+ field_description << "\nOnly returned when requested through the optional_fields param.\n"
120
+ end
121
+ field_description.try(:chomp!)
122
+ field_description
123
+ end
124
+
125
+ def format_conditional_description(field_options)
126
+ return '' if field_options[:if].blank?
127
+
128
+ conditions = field_options[:if]
129
+ .reject { |cond| presenter.conditionals[cond].options[:nodoc] }
130
+ .map { |cond| uncapitalize(presenter.conditionals[cond].description) }
131
+ .delete_if(&:empty?)
132
+ .uniq
133
+ .to_sentence
134
+
135
+ conditions.present? ? "\nVisible when #{conditions}.\n" : ''
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
144
+
145
+ Brainstem::ApiDocs::FORMATTERS[:presenter_field][:oas_v2] =
146
+ Brainstem::ApiDocs::Formatters::OpenApiSpecification::Version2::FieldDefinitions::PresenterFieldFormatter.method(:call)
147
+
@@ -0,0 +1,146 @@
1
+ require 'brainstem/api_docs'
2
+ require 'brainstem/api_docs/formatters/abstract_formatter'
3
+ require 'brainstem/api_docs/formatters/open_api_specification/helper'
4
+
5
+ module Brainstem
6
+ module ApiDocs
7
+ module Formatters
8
+ module OpenApiSpecification
9
+ module Version2
10
+ module FieldDefinitions
11
+ class ResponseFieldFormatter < AbstractFormatter
12
+ include Helper
13
+
14
+ def initialize(endpoint, param_name, param_tree)
15
+ @endpoint = endpoint
16
+ @param_name = param_name
17
+ @param_tree = param_tree
18
+ end
19
+
20
+ def format
21
+ field_config = @param_tree[:_config]
22
+ field_properties = @param_tree.except(:_config)
23
+
24
+ format_field(field_config, field_properties)
25
+ end
26
+ alias_method :call, :format
27
+
28
+ private
29
+
30
+ def format_field(field_config, field_branches)
31
+ if field_config[:nested_levels]
32
+ format_nested_array_field(field_config, field_branches)
33
+ elsif field_branches.present?
34
+ format_nested_field(field_config, field_branches)
35
+ else
36
+ format_simple_field(field_config)
37
+ end
38
+ end
39
+
40
+ def format_nested_array_field(field_config, field_properties)
41
+ field_properties_data, nested_levels = format_array_items(field_config, field_properties)
42
+
43
+ format_nested_array_parent(nested_levels, field_properties_data)
44
+ end
45
+
46
+ def format_array_items(field_config, field_properties)
47
+ field_nested_levels = field_config[:nested_levels]
48
+
49
+ if field_properties.present?
50
+ [format_nested_field(field_config, field_properties), field_nested_levels - 1]
51
+ else
52
+ [type_and_format(field_config[:item_type]), field_nested_levels]
53
+ end
54
+ end
55
+
56
+ def format_nested_array_parent(nested_level, formatted_data)
57
+ if nested_level == 1
58
+ {
59
+ 'type' => 'array',
60
+ 'items' => formatted_data
61
+ }
62
+ else
63
+ {
64
+ 'type' => 'array',
65
+ 'items' => format_nested_array_parent(nested_level - 1, formatted_data)
66
+ }
67
+ end
68
+ end
69
+
70
+ def format_nested_field(field_config, field_properties)
71
+ case field_config[:type]
72
+ when 'hash'
73
+ format_object_field(field_config, field_properties)
74
+ when 'array'
75
+ {
76
+ type: 'array',
77
+ description: format_description(field_config),
78
+ items: format_object_field(field_config, field_properties, false)
79
+ }
80
+ end.with_indifferent_access.reject { |_, v| v.blank? }
81
+ end
82
+
83
+ def format_object_field(field_config, field_properties, include_description = true)
84
+ properties, additional_properties = split_properties(field_properties)
85
+
86
+ {
87
+ type: 'object',
88
+ description: include_description && format_description(field_config),
89
+ properties: format_field_properties(properties),
90
+ additionalProperties: format_field_properties(additional_properties),
91
+ }.with_indifferent_access.reject { |_, v| v.blank? }
92
+ end
93
+
94
+ def split_properties(field_properties)
95
+ split_properties = field_properties.each_with_object({ properties: {}, additional_properties: {} }) do |(field_name, field_config), acc|
96
+ if field_config[:_config][:dynamic_key]
97
+ acc[:additional_properties][field_name] = field_config
98
+ else
99
+ acc[:properties][field_name] = field_config
100
+ end
101
+ end
102
+
103
+ [split_properties[:properties], split_properties[:additional_properties]]
104
+ end
105
+
106
+ def format_simple_field(field_config)
107
+ field_data = type_and_format(field_config[:type], field_config[:item_type])
108
+ raise(invalid_type_error_message(field_config)) unless field_data
109
+ field_data.merge!(description: format_sentence(field_config[:info])) if field_config[:info].present?
110
+ field_data
111
+ end
112
+
113
+ def invalid_type_error_message(field_config)
114
+ <<-MSG.strip_heredoc
115
+ Unknown Brainstem Field type encountered(#{field_config[:type]}) for field #{field_config[:name]}
116
+ in #{@endpoint.controller_name} for #{@endpoint.action} action.
117
+ MSG
118
+ end
119
+
120
+ def format_field_properties(branches)
121
+ branches.inject(ActiveSupport::HashWithIndifferentAccess.new) do |buffer, (field_name, field_config)|
122
+ config = field_config[:_config]
123
+ branches = field_config.except(:_config)
124
+
125
+ if config[:dynamic_key]
126
+ format_field(config, branches)
127
+ else
128
+ buffer[field_name.to_s] = format_field(config, branches)
129
+ buffer
130
+ end
131
+ end
132
+ end
133
+
134
+ def format_description(field_config)
135
+ format_sentence(field_config[:info])
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
144
+
145
+ Brainstem::ApiDocs::FORMATTERS[:response_field][:oas_v2] =
146
+ Brainstem::ApiDocs::Formatters::OpenApiSpecification::Version2::FieldDefinitions::ResponseFieldFormatter.method(:call)
@@ -1,6 +1,7 @@
1
1
  require 'active_support/core_ext/string/inflections'
2
2
  require 'brainstem/api_docs/formatters/abstract_formatter'
3
3
  require 'brainstem/api_docs/formatters/open_api_specification/helper'
4
+ require 'brainstem/api_docs/formatters/open_api_specification/version_2/field_definitions/presenter_field_formatter'
4
5
 
5
6
  module Brainstem
6
7
  module ApiDocs
@@ -30,6 +31,7 @@ module Brainstem
30
31
  format_description!
31
32
  format_type!
32
33
  format_fields!
34
+ sort_properties!
33
35
 
34
36
  output.merge!(presenter.target_class => definition.reject {|_, v| v.blank?})
35
37
  end
@@ -42,8 +44,16 @@ module Brainstem
42
44
  definition.merge! title: presenter_title(presenter)
43
45
  end
44
46
 
47
+ def sort_properties!
48
+ return if definition[:properties].blank?
49
+
50
+ definition[:properties] = definition[:properties].sort.each_with_object({}) do |(key, val), obj|
51
+ obj[key] = val
52
+ end
53
+ end
54
+
45
55
  def format_description!
46
- definition.merge! description: format_description(presenter.description)
56
+ definition.merge! description: format_sentence(presenter.description)
47
57
  end
48
58
 
49
59
  def format_type!
@@ -51,75 +61,63 @@ module Brainstem
51
61
  end
52
62
 
53
63
  def format_fields!
54
- return unless presenter.valid_fields.any?
64
+ return unless presenter.valid_fields.any? || presenter.valid_associations.any?
55
65
 
56
- definition.merge! properties: format_field_branch(presenter.valid_fields)
66
+ properties = format_field_branch(presenter.valid_fields)
67
+ with_associations = format_field_associations(properties)
68
+
69
+ definition.merge! properties: with_associations
57
70
  end
58
71
 
59
72
  def format_field_branch(branch)
60
- branch.inject(ActiveSupport::HashWithIndifferentAccess.new) do |buffer, (name, field)|
61
- if nested_field?(field)
62
- buffer[name.to_s] = case field.type
63
- when 'hash'
64
- {
65
- type: 'object',
66
- properties: format_field_branch(field.to_h)
67
- }.with_indifferent_access
68
- when 'array'
69
- {
70
- type: 'array',
71
- items: {
72
- type: 'object',
73
- properties: format_field_branch(field.to_h)
74
- }
75
- }.with_indifferent_access
76
- else
77
- raise "Unknown Brainstem Field type encountered(#{field.type}) for field #{name}"
78
- end
79
- else
80
- buffer[name.to_s] = format_field_leaf(field)
81
- end
82
-
83
- buffer
73
+ branch.each_with_object(ActiveSupport::HashWithIndifferentAccess.new) do |(name, field), buffer|
74
+ buffer[name.to_s] = format_field(field)
84
75
  end
85
76
  end
86
77
 
87
- def nested_field?(field)
88
- field.respond_to?(:configuration)
89
- end
78
+ def format_field_associations(properties)
79
+ presenter.valid_associations.each_with_object(properties) do |(name, association), props|
80
+ if association.polymorphic?
81
+ key = association.name + "_ref"
82
+ props[key] = {
83
+ type: 'object',
84
+ description: association_description(key, name),
85
+ properties: {
86
+ key: type_and_format(:string),
87
+ id: type_and_format(:string)
88
+ }
89
+ }
90
+ next
91
+ end
90
92
 
91
- def format_field_leaf(field)
92
- field_data = type_and_format(field.type, field.options[:item_type])
93
+ key = association_key(association)
94
+ description = association_description(key, association.name)
95
+ formatted_type = if association.type == :has_many
96
+ type_and_format(:array, :string)
97
+ else
98
+ type_and_format(:string)
99
+ end.merge(description: description)
93
100
 
94
- unless field_data
95
- raise "Unknown Brainstem Field type encountered(#{field.type}) for field #{field.name}"
101
+ props[key] = formatted_type unless props[key]
96
102
  end
97
-
98
- field_data.merge!(description: format_description_for(field))
99
- field_data.delete(:description) if field_data[:description].blank?
100
-
101
- field_data
102
103
  end
103
104
 
104
- def format_description_for(field)
105
- field_description = format_description(field.description) || ''
106
- field_description << format_conditional_description(field.options)
107
- field_description << "\nOnly returned when requested through the optional_fields param.\n" if field.optional?
108
- field_description.try(:chomp!)
109
- field_description
105
+ def association_description(key, name)
106
+ ["`#{key}` will only be included in the response if `#{name}` is in the list of included associations.",
107
+ "See <a href='#section/Includes'>include</a> section for usage."].join(' ')
110
108
  end
111
109
 
112
- def format_conditional_description(field_options)
113
- return '' if field_options[:if].blank?
114
-
115
- conditions = field_options[:if]
116
- .reject { |cond| presenter.conditionals[cond].options[:nodoc] }
117
- .map { |cond| uncapitalize(presenter.conditionals[cond].description) }
118
- .delete_if(&:empty?)
119
- .uniq
120
- .to_sentence
110
+ def association_key(association)
111
+ if association.response_key
112
+ association.response_key
113
+ else
114
+ key = association.name.singularize
115
+ association.type == :has_many ? "#{key}_ids" : "#{key}_id"
116
+ end
117
+ end
121
118
 
122
- conditions.present? ? "\nVisible when #{conditions}.\n" : ''
119
+ def format_field(field)
120
+ Brainstem::ApiDocs::FORMATTERS[:presenter_field][:oas_v2].call(presenter, field)
123
121
  end
124
122
  end
125
123
  end