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
@@ -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