brainstem 1.1.1 → 1.3.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +81 -4
  3. data/Gemfile.lock +9 -9
  4. data/README.md +134 -37
  5. data/brainstem.gemspec +1 -1
  6. data/lib/brainstem/api_docs/endpoint.rb +40 -18
  7. data/lib/brainstem/api_docs/formatters/markdown/endpoint_formatter.rb +27 -22
  8. data/lib/brainstem/api_docs/formatters/markdown/helper.rb +9 -0
  9. data/lib/brainstem/api_docs/formatters/markdown/presenter_formatter.rb +14 -6
  10. data/lib/brainstem/api_docs/presenter.rb +3 -7
  11. data/lib/brainstem/concerns/controller_dsl.rb +138 -14
  12. data/lib/brainstem/concerns/presenter_dsl.rb +39 -6
  13. data/lib/brainstem/dsl/array_block_field.rb +25 -0
  14. data/lib/brainstem/dsl/block_field.rb +69 -0
  15. data/lib/brainstem/dsl/configuration.rb +13 -5
  16. data/lib/brainstem/dsl/field.rb +15 -1
  17. data/lib/brainstem/dsl/fields_block.rb +20 -2
  18. data/lib/brainstem/dsl/hash_block_field.rb +30 -0
  19. data/lib/brainstem/presenter.rb +10 -6
  20. data/lib/brainstem/presenter_validator.rb +20 -11
  21. data/lib/brainstem/version.rb +1 -1
  22. data/spec/brainstem/api_docs/endpoint_spec.rb +347 -14
  23. data/spec/brainstem/api_docs/formatters/markdown/endpoint_formatter_spec.rb +106 -13
  24. data/spec/brainstem/api_docs/formatters/markdown/helper_spec.rb +19 -0
  25. data/spec/brainstem/api_docs/formatters/markdown/presenter_formatter_spec.rb +150 -37
  26. data/spec/brainstem/api_docs/presenter_spec.rb +85 -18
  27. data/spec/brainstem/concerns/controller_dsl_spec.rb +615 -31
  28. data/spec/brainstem/concerns/inheritable_configuration_spec.rb +32 -9
  29. data/spec/brainstem/concerns/presenter_dsl_spec.rb +99 -25
  30. data/spec/brainstem/dsl/array_block_field_spec.rb +43 -0
  31. data/spec/brainstem/dsl/block_field_spec.rb +188 -0
  32. data/spec/brainstem/dsl/field_spec.rb +86 -20
  33. data/spec/brainstem/dsl/hash_block_field_spec.rb +166 -0
  34. data/spec/brainstem/presenter_collection_spec.rb +24 -24
  35. data/spec/brainstem/presenter_spec.rb +233 -9
  36. data/spec/brainstem/query_strategies/filter_and_search_spec.rb +1 -1
  37. data/spec/spec_helpers/presenters.rb +8 -0
  38. data/spec/spec_helpers/schema.rb +13 -0
  39. metadata +15 -6
@@ -71,7 +71,7 @@ module Brainstem
71
71
  def <=>(other)
72
72
 
73
73
  # Any unordered routes are assigned an index of +ACTION_ORDER.count+.
74
- ordered_actions_count = ACTION_ORDER.count
74
+ ordered_actions_count = ACTION_ORDER.count
75
75
  own_action_priority = ACTION_ORDER.index(action.to_s) || ordered_actions_count
76
76
  other_action_priority = ACTION_ORDER.index(other.action.to_s) || ordered_actions_count
77
77
 
@@ -120,32 +120,54 @@ module Brainstem
120
120
 
121
121
 
122
122
  #
123
- # Returns a hash of all root-level params with values of an array of
124
- # the parameters nested underneath, or +nil+ in the event they are
125
- # root-level non-nested params.
123
+ # Returns a hash of all params nested under the specified root or
124
+ # parent fields along with their type, item type & children.
126
125
  #
127
- # @return [Hash{Symbol => Array,NilClass}] root keys and the keys
128
- # nested under them, or nil if not a nested param.
126
+ # @return [Hash{Symbol => Hash}] root keys and their type info, item info & children
127
+ # nested under them.
129
128
  #
130
- def root_param_keys
131
- @root_param_keys ||= begin
132
- valid_params.to_h
133
- .inject({}) do |hsh, (field_name, data)|
134
- next hsh if data[:nodoc]
135
-
136
- if data.has_key?(:root)
137
- key = data[:root].respond_to?(:call) ? data[:root].call(controller.const) : data[:root]
138
- (hsh[key] ||= []) << field_name
139
- else
140
- hsh[field_name] = nil
129
+ def params_configuration_tree
130
+ @params_configuration_tree ||= begin
131
+ valid_params
132
+ .to_h
133
+ .deep_dup
134
+ .with_indifferent_access
135
+ .inject(ActiveSupport::HashWithIndifferentAccess.new) do |result, (field_name_proc, field_config)|
136
+
137
+ next result if field_config[:nodoc]
138
+
139
+ field_name = evaluate_field_name(field_name_proc)
140
+ if field_config.has_key?(:ancestors)
141
+ ancestors = field_config[:ancestors].map { |ancestor_key| evaluate_field_name(ancestor_key) }
142
+
143
+ parent = ancestors.inject(result) do |traversed_hash, ancestor_name|
144
+ traversed_hash[ancestor_name] ||= { :_config => { type: 'hash' } }
145
+ traversed_hash[ancestor_name]
141
146
  end
142
147
 
143
- hsh
148
+ parent[field_name] = { :_config => field_config.except(:root, :ancestors) }
149
+ else
150
+ result[field_name] = { :_config => field_config }
144
151
  end
152
+
153
+ result
154
+ end
145
155
  end
146
156
  end
147
157
 
148
158
 
159
+ #
160
+ # Evaluate field name if proc and symbolize it.
161
+ #
162
+ def evaluate_field_name(field_name_or_proc)
163
+ return field_name_or_proc if field_name_or_proc.nil?
164
+
165
+ field_name = field_name_or_proc.respond_to?(:call) ? field_name_or_proc.call(controller.const) : field_name_or_proc
166
+ field_name.to_sym
167
+ end
168
+ alias_method :evaluate_root_name, :evaluate_field_name
169
+
170
+
149
171
  #
150
172
  # Retrieves the +presents+ settings.
151
173
  #
@@ -83,36 +83,34 @@ module Brainstem
83
83
  # Formats each parameter.
84
84
  #
85
85
  def format_params!
86
- return unless endpoint.root_param_keys.any?
86
+ return unless endpoint.params_configuration_tree.any?
87
87
 
88
88
  output << md_h5("Valid Parameters")
89
89
  output << md_ul do
90
- endpoint.root_param_keys.inject("") do |buff, (root_param_name, child_keys)|
91
- if child_keys.nil?
92
- buff += parameter_with_indent_level(
93
- root_param_name,
94
- endpoint.valid_params[root_param_name],
95
- 0
96
- )
97
- else
98
- text = md_inline_code(root_param_name) + "\n"
99
-
100
- child_keys.each do |param_name|
101
- text += parameter_with_indent_level(
102
- param_name,
103
- endpoint.valid_params[param_name],
104
- 1
105
- )
106
- end
107
-
108
- buff << md_li(text)
109
- end
110
-
90
+ endpoint.params_configuration_tree.inject("") do |buff, (param_name, param_config)|
91
+ buff << format_param_tree!("", param_name, param_config)
111
92
  buff
112
93
  end
113
94
  end
114
95
  end
115
96
 
97
+ #
98
+ # Formats the parent parameter and its children
99
+ #
100
+ def format_param_tree!(buffer, param_name, param_config, indentation = 0)
101
+ buffer += parameter_with_indent_level(
102
+ param_name,
103
+ param_config[:_config],
104
+ indentation
105
+ )
106
+
107
+ children = param_config.except(:_config) || []
108
+ children.each do |child_param_name, child_param_config|
109
+ buffer = format_param_tree!(buffer, child_param_name, child_param_config, indentation + 1)
110
+ end
111
+
112
+ buffer
113
+ end
116
114
 
117
115
  #
118
116
  # Formats a given parameter with a variable indent level. Useful for
@@ -120,11 +118,16 @@ module Brainstem
120
118
  #
121
119
  # @param [String] name the param name
122
120
  # @param [Hash] options information pertinent to the param
121
+ # @option [Boolean] options :required
123
122
  # @option [Boolean] options :legacy
124
123
  # @option [Boolean] options :recursive
125
124
  # @option [String,Symbol] options :only Deprecated: use +actions+
126
125
  # block instead
127
126
  # @option [String] options :info the doc string for the param
127
+ # @option [String] options :type The type of the field.
128
+ # e.g. string, integer, boolean, array, hash
129
+ # @option [String] options :item_type The type of the items in the field.
130
+ # Ideally used when the type of the field is an array or hash.
128
131
  # @param [Integer] indent how many levels the output should be
129
132
  # indented from normal
130
133
  #
@@ -132,10 +135,12 @@ module Brainstem
132
135
  options = options.dup
133
136
  text = md_inline_code(title)
134
137
 
138
+ text += md_inline_type(options.delete(:type), options.delete(:item_type)) if options.has_key?(:type)
135
139
  text += " - #{options.delete(:info)}" if options.has_key?(:info)
136
140
 
137
141
  if options.keys.any?
138
142
  text += "\n"
143
+ text += md_li("Required: #{options[:required].to_s}", indent + 1) if options.has_key?(:required) && options[:required]
139
144
  text += md_li("Legacy: #{options[:legacy].to_s}", indent + 1) if options.has_key?(:legacy)
140
145
  text += md_li("Recursive: #{options[:recursive].to_s}", indent + 1) if options.has_key?(:recursive)
141
146
  text.chomp!
@@ -69,6 +69,15 @@ module Brainstem
69
69
  def md_a(text, link)
70
70
  "[#{text}](#{link})"
71
71
  end
72
+
73
+
74
+ def md_inline_type(type, item_type = nil)
75
+ return "" if type.blank?
76
+
77
+ text = type.to_s.capitalize
78
+ text += "<#{item_type.to_s.capitalize}>" if item_type.present?
79
+ " (#{md_inline_code(text)})"
80
+ end
72
81
  end
73
82
  end
74
83
  end
@@ -62,7 +62,7 @@ module Brainstem
62
62
 
63
63
  def format_field_leaf(field, indent_level)
64
64
  text = md_inline_code(field.name.to_s)
65
- text << " (#{md_inline_code(field.type.to_s.capitalize)})"
65
+ text << md_inline_type(field.type, field.options[:item_type])
66
66
 
67
67
  text << "\n"
68
68
  text << md_li(field.description, indent_level + 1) if field.description
@@ -85,9 +85,9 @@ module Brainstem
85
85
  def format_field_branch(branch, indent_level = 0)
86
86
  branch.inject("") do |buffer, (name, field)|
87
87
  if nested_field?(field)
88
- sub_fields = md_inline_code(name.to_s) + "\n"
88
+ sub_fields = format_field_leaf(field, indent_level) + "\n"
89
89
  sub_fields << format_field_branch(field.to_h, indent_level + 1)
90
- buffer += md_li(sub_fields, indent_level)
90
+ buffer += md_li(sub_fields, indent_level)
91
91
  else
92
92
  buffer += md_li(format_field_leaf(field, indent_level), indent_level)
93
93
  end
@@ -96,7 +96,7 @@ module Brainstem
96
96
 
97
97
 
98
98
  def nested_field?(field)
99
- !field.respond_to?(:options)
99
+ field.respond_to?(:configuration)
100
100
  end
101
101
 
102
102
 
@@ -120,10 +120,18 @@ module Brainstem
120
120
  output << md_ul do
121
121
  presenter.valid_filters.inject("") do |buffer, (name, opts)|
122
122
  text = md_inline_code(name)
123
+ text << md_inline_type(opts[:type])
124
+
125
+ if opts[:info] || opts[:items]
126
+ description = opts[:info].to_s
127
+
128
+ if opts[:items].present?
129
+ description += "." unless description =~ /\.\s*\z/
130
+ description += " Available values: #{opts[:items].join(', ')}."
131
+ end
123
132
 
124
- if opts[:info]
125
133
  text << "\n"
126
- text << md_li(opts[:info], 1)
134
+ text << md_li(description, 1)
127
135
  text.chomp!
128
136
  end
129
137
 
@@ -105,12 +105,8 @@ module Brainstem
105
105
 
106
106
 
107
107
  def valid_fields(fields = configuration[:fields])
108
- fields.to_h.reject do |k, v|
109
- if nested_field?(v)
110
- valid_fields_in(v).none?
111
- else
112
- invalid_field?(v)
113
- end
108
+ fields.to_h.reject do |_, field|
109
+ invalid_field?(field) || (nested_field?(field) && valid_fields_in(field).none?)
114
110
  end
115
111
  end
116
112
  alias_method :valid_fields_in, :valid_fields
@@ -122,7 +118,7 @@ module Brainstem
122
118
 
123
119
 
124
120
  def nested_field?(field)
125
- !field.respond_to?(:options)
121
+ field.respond_to?(:configuration)
126
122
  end
127
123
 
128
124
 
@@ -53,6 +53,14 @@ module Brainstem
53
53
  end
54
54
 
55
55
 
56
+ #
57
+ # Temporary implementation to track controllers that have been documented.
58
+ #
59
+ def documented!
60
+ configuration[brainstem_params_context][:documented] = true
61
+ end
62
+
63
+
56
64
  #
57
65
  # Specifies that the scope should not be documented. Setting this on
58
66
  # the default context will force the controller to be undocumented,
@@ -127,7 +135,7 @@ module Brainstem
127
135
  # method accepting the controller constant and returning one
128
136
  #
129
137
  def model_params(root = Proc.new { |klass| klass.brainstem_model_name }, &block)
130
- with_options({ root: root.is_a?(Symbol) ? root.to_s : root }, &block)
138
+ with_options(format_root_ancestry_options(root), &block)
131
139
  end
132
140
 
133
141
 
@@ -136,16 +144,37 @@ module Brainstem
136
144
  # the info sent with it.
137
145
  #
138
146
  # @param [Symbol] field_name the name of the param
147
+ # @param [String,Symbol] type the data type of the field. If not specified, will default to `string`.
139
148
  # @param [Hash] options
140
149
  # @option options [String] :info the documentation for the param
141
150
  # @option options [String,Symbol] :root if this is a nested param,
142
151
  # under which param should it be nested?
143
152
  # @option options [Boolean] :nodoc should this param appear in the
144
153
  # documentation?
154
+ # @option options [Boolean] :required if the param is required for
155
+ # the endpoint
156
+ # @option options [String,Symbol] :item_type The data type of the items contained in a field.
157
+ # Ideally used when the data type of the field is an `array`, `object` or `hash`.
145
158
  #
146
- def valid(field_name, options = { nodoc: false })
159
+ def valid(field_name, type = nil, options = {}, &block)
147
160
  valid_params = configuration[brainstem_params_context][:valid_params]
148
- valid_params[field_name.to_sym] = options
161
+ field_config = format_field_configuration(type, options, &block)
162
+
163
+ # Inherit `nodoc` attribute from parent
164
+ parent_key = (options[:ancestors] || []).reverse.first
165
+ field_config[:nodoc] = true if parent_key && valid_params[parent_key] && valid_params[parent_key][:nodoc]
166
+
167
+ # Rollup `required` attribute to ancestors if true
168
+ if field_config[:required]
169
+ (options[:ancestors] || []).reverse.each do |ancestor_key|
170
+ valid_params[ancestor_key][:required] = true if valid_params.has_key?(ancestor_key)
171
+ end
172
+ end
173
+
174
+ procified_field_name = format_field_name(field_name)
175
+ valid_params[procified_field_name] = field_config
176
+
177
+ with_options(format_field_ancestry_options(procified_field_name, field_config), &block) if block_given?
149
178
  end
150
179
 
151
180
 
@@ -209,8 +238,7 @@ module Brainstem
209
238
  # be output in the documentation.
210
239
  #
211
240
  def description(text, options = { nodoc: false })
212
- configuration[brainstem_params_context][:description] = \
213
- options.merge(info: text)
241
+ configuration[brainstem_params_context][:description] = options.merge(info: text)
214
242
  end
215
243
 
216
244
 
@@ -230,12 +258,114 @@ module Brainstem
230
258
  # output in the documentation.
231
259
  #
232
260
  def title(text, options = { nodoc: false })
233
- configuration[brainstem_params_context][:title] = \
234
- options.merge(info: text)
261
+ configuration[brainstem_params_context][:title] = options.merge(info: text)
262
+ end
263
+
264
+ #
265
+ # Converts the field name into a Proc.
266
+ #
267
+ # @param [String, Symbol, Proc] text The title to set
268
+ # @return [Proc]
269
+ #
270
+ def format_field_name(field_name_or_proc)
271
+ field_name_or_proc.respond_to?(:call) ? field_name_or_proc : Proc.new { field_name_or_proc.to_s }
272
+ end
273
+ alias_method :format_root_name, :format_field_name
274
+
275
+
276
+ #
277
+ # Formats the ancestry options of the field. Returns a hash with ancestors & root.
278
+ #
279
+ def format_root_ancestry_options(root_name)
280
+ root_proc = format_root_name(root_name)
281
+ ancestors = [root_proc]
282
+
283
+ { root: root_proc, ancestors: ancestors }.with_indifferent_access.reject { |_, v| v.blank? }
284
+ end
285
+
286
+
287
+ #
288
+ # Formats the ancestry options of the field. Returns a hash with ancestors & root.
289
+ #
290
+ def format_field_ancestry_options(field_name_proc, options = {})
291
+ ancestors = options[:ancestors].try(:dup) || []
292
+ ancestors << field_name_proc
293
+
294
+ { ancestors: ancestors }.with_indifferent_access.reject { |_, v| v.blank? }
295
+ end
296
+
297
+
298
+ #
299
+ # Formats the configuration of the field and returns the default configuration if not specified.
300
+ #
301
+ def format_field_configuration(type = nil, options = {}, &block)
302
+ options = type if type.is_a?(Hash) && options.empty?
303
+
304
+ options[:type] = sanitize_param_data_type(type, &block)
305
+ options[:item_type] = options[:item_type].to_s if options.has_key?(:item_type)
306
+
307
+ DEFAULT_PARAM_OPTIONS.merge(options).with_indifferent_access
308
+ end
309
+
310
+ DEFAULT_PARAM_OPTIONS = { nodoc: false, required: false }
311
+ private_constant :DEFAULT_PARAM_OPTIONS
312
+
313
+
314
+ #
315
+ # Returns the type of the param and adds a deprecation warning if not specified.
316
+ #
317
+ def sanitize_param_data_type(type, &block)
318
+ if type.is_a?(Hash) || type.blank?
319
+ deprecated_type_warning
320
+ type = block_given? ? DEFAULT_BLOCK_DATA_TYPE : DEFAULT_DATA_TYPE
321
+ end
322
+
323
+ type.to_s
324
+ end
325
+
326
+ DEFAULT_DATA_TYPE = 'string'
327
+ private_constant :DEFAULT_DATA_TYPE
328
+
329
+ DEFAULT_BLOCK_DATA_TYPE = 'hash'
330
+ private_constant :DEFAULT_BLOCK_DATA_TYPE
331
+
332
+
333
+ #
334
+ # Adds deprecation warning if the type argument is not specified when defining a valid param.
335
+ #
336
+ def deprecated_type_warning
337
+ ActiveSupport::Deprecation.warn(
338
+ 'Please specify the `type` of the parameter as the second argument. If not specified, '\
339
+ 'it will default to `:string`. This default behavior will be deprecated in the next major '\
340
+ 'version and will need to be explicitly specified. e.g. `post.valid :message, :text, required: true`',
341
+ caller
342
+ )
235
343
  end
236
344
  end
237
345
 
238
346
 
347
+ def valid_params_tree(requested_context = action_name.to_sym)
348
+ contextual_key(requested_context, :valid_params)
349
+ .to_h
350
+ .inject(ActiveSupport::HashWithIndifferentAccess.new) do |hsh, (field_name_proc, field_config)|
351
+
352
+ field_name = field_name_proc.call(self.class)
353
+ if field_config.has_key?(:ancestors)
354
+ ancestors = field_config[:ancestors].map { |ancestor_key| ancestor_key.call(self.class) }
355
+ parent = ancestors.inject(hsh) do |traversed_hash, ancestor_name|
356
+ traversed_hash[ancestor_name] ||= {}
357
+ traversed_hash[ancestor_name]
358
+ end
359
+
360
+ parent[field_name] = { :_config => field_config.except(:root, :ancestors) }
361
+ else
362
+ hsh[field_name] = { :_config => field_config }
363
+ end
364
+
365
+ hsh
366
+ end
367
+ end
368
+
239
369
  #
240
370
  # Lists all valid parameters for the current action. Falls back to the
241
371
  # valid parameters for the default context.
@@ -246,12 +376,7 @@ module Brainstem
246
376
  # descriptions or sub-hashes.
247
377
  #
248
378
  def brainstem_valid_params(requested_context = action_name.to_sym, root_param_name = brainstem_model_name)
249
- contextual_key(requested_context, :valid_params)
250
- .to_h
251
- .select do |k, v|
252
- root = v[:root].respond_to?(:call) ? v[:root].call(self.class) : v[:root]
253
- root.to_s == root_param_name.to_s
254
- end
379
+ valid_params_tree(requested_context)[root_param_name.to_s]
255
380
  end
256
381
  alias_method :brainstem_valid_params_for, :brainstem_valid_params
257
382
 
@@ -293,7 +418,6 @@ module Brainstem
293
418
  configuration[DEFAULT_BRAINSTEM_PARAMS_CONTEXT][key.to_sym]
294
419
  end
295
420
  end
296
-
297
421
  private :contextual_key
298
422
  end
299
423
  end