brainstem 1.1.1 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +81 -4
- data/Gemfile.lock +9 -9
- data/README.md +134 -37
- data/brainstem.gemspec +1 -1
- data/lib/brainstem/api_docs/endpoint.rb +40 -18
- data/lib/brainstem/api_docs/formatters/markdown/endpoint_formatter.rb +27 -22
- data/lib/brainstem/api_docs/formatters/markdown/helper.rb +9 -0
- data/lib/brainstem/api_docs/formatters/markdown/presenter_formatter.rb +14 -6
- data/lib/brainstem/api_docs/presenter.rb +3 -7
- data/lib/brainstem/concerns/controller_dsl.rb +138 -14
- data/lib/brainstem/concerns/presenter_dsl.rb +39 -6
- data/lib/brainstem/dsl/array_block_field.rb +25 -0
- data/lib/brainstem/dsl/block_field.rb +69 -0
- data/lib/brainstem/dsl/configuration.rb +13 -5
- data/lib/brainstem/dsl/field.rb +15 -1
- data/lib/brainstem/dsl/fields_block.rb +20 -2
- data/lib/brainstem/dsl/hash_block_field.rb +30 -0
- data/lib/brainstem/presenter.rb +10 -6
- data/lib/brainstem/presenter_validator.rb +20 -11
- data/lib/brainstem/version.rb +1 -1
- data/spec/brainstem/api_docs/endpoint_spec.rb +347 -14
- data/spec/brainstem/api_docs/formatters/markdown/endpoint_formatter_spec.rb +106 -13
- data/spec/brainstem/api_docs/formatters/markdown/helper_spec.rb +19 -0
- data/spec/brainstem/api_docs/formatters/markdown/presenter_formatter_spec.rb +150 -37
- data/spec/brainstem/api_docs/presenter_spec.rb +85 -18
- data/spec/brainstem/concerns/controller_dsl_spec.rb +615 -31
- data/spec/brainstem/concerns/inheritable_configuration_spec.rb +32 -9
- data/spec/brainstem/concerns/presenter_dsl_spec.rb +99 -25
- data/spec/brainstem/dsl/array_block_field_spec.rb +43 -0
- data/spec/brainstem/dsl/block_field_spec.rb +188 -0
- data/spec/brainstem/dsl/field_spec.rb +86 -20
- data/spec/brainstem/dsl/hash_block_field_spec.rb +166 -0
- data/spec/brainstem/presenter_collection_spec.rb +24 -24
- data/spec/brainstem/presenter_spec.rb +233 -9
- data/spec/brainstem/query_strategies/filter_and_search_spec.rb +1 -1
- data/spec/spec_helpers/presenters.rb +8 -0
- data/spec/spec_helpers/schema.rb +13 -0
- 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
|
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
|
124
|
-
#
|
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 =>
|
128
|
-
# nested under them
|
126
|
+
# @return [Hash{Symbol => Hash}] root keys and their type info, item info & children
|
127
|
+
# nested under them.
|
129
128
|
#
|
130
|
-
def
|
131
|
-
@
|
132
|
-
valid_params
|
133
|
-
.
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
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
|
-
|
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.
|
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.
|
91
|
-
|
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 <<
|
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 =
|
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
|
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
|
-
|
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(
|
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 |
|
109
|
-
|
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
|
-
|
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(
|
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,
|
159
|
+
def valid(field_name, type = nil, options = {}, &block)
|
147
160
|
valid_params = configuration[brainstem_params_context][:valid_params]
|
148
|
-
|
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
|
-
|
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
|
-
|
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
|