brainstem 1.1.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
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