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
@@ -1,6 +1,8 @@
1
1
  require 'brainstem/concerns/inheritable_configuration'
2
2
  require 'brainstem/dsl/association'
3
3
  require 'brainstem/dsl/field'
4
+ require 'brainstem/dsl/array_block_field'
5
+ require 'brainstem/dsl/hash_block_field'
4
6
  require 'brainstem/dsl/conditional'
5
7
 
6
8
  require 'brainstem/dsl/base_block'
@@ -50,6 +52,13 @@ module Brainstem
50
52
  configuration[:nodoc] = true
51
53
  end
52
54
 
55
+ #
56
+ # Temporary implementation to track controllers that have been documented.
57
+ #
58
+ def documented!
59
+ configuration[:documented] = true
60
+ end
61
+
53
62
  # Declare a helper module or block whose methods will be available in dynamic fields and associations.
54
63
  def helper(mod = nil, &block)
55
64
  if mod
@@ -114,15 +123,17 @@ module Brainstem
114
123
 
115
124
 
116
125
  #
117
- # @overload filter(name, options = {})
126
+ # @overload filter(name, type, options = {})
118
127
  # @param [Symbol] name The name of the scope that may be applied as a
119
128
  # filter.
129
+ # @param [Symbol] type The type of the value that filter holds.
120
130
  # @option options [Object] :default If set, causes this filter to be
121
131
  # applied to every request. If the filter accepts parameters, the
122
132
  # value given here will be passed to the filter when it is applied.
123
133
  # @option options [String] :info Docstring for the filter.
134
+ # @option options [Array] :items List of assignable values for the filter.
124
135
  #
125
- # @overload filter(name, options = {}, &block)
136
+ # @overload filter(name, type, options = {}, &block)
126
137
  # @param [Symbol] name The filter can be requested using this name.
127
138
  # @yieldparam scope [ActiveRecord::Relation] The scope that the
128
139
  # filter should use as a base.
@@ -131,16 +142,25 @@ module Brainstem
131
142
  # @yieldreturn [ActiveRecord::Relation] A new scope that filters the
132
143
  # scope that was yielded.
133
144
  #
134
- def filter(name, options = {}, &block)
135
- valid_options = %w(default info include_params nodoc)
145
+ def filter(name, type = DEFAULT_FILTER_DATA_TYPE, options = {}, &block)
146
+ if type.is_a?(Hash)
147
+ options = type if options.blank?
148
+ type = DEFAULT_FILTER_DATA_TYPE
149
+
150
+ deprecated_type_warning
151
+ elsif type.to_s == 'array'
152
+ options[:item_type] = options[:item_type].to_s.presence || DEFAULT_FILTER_DATA_TYPE
153
+ end
154
+
155
+ valid_options = %w(default info include_params nodoc items item_type)
136
156
  options.select! { |k, v| valid_options.include?(k.to_s) }
137
157
 
138
158
  configuration[:filters][name] = options.merge({
139
- value: (block_given? ? block : nil)
159
+ value: (block_given? ? block : nil),
160
+ type: type.to_s,
140
161
  })
141
162
  end
142
163
 
143
-
144
164
  def search(&block)
145
165
  configuration[:search] = block
146
166
  end
@@ -166,6 +186,19 @@ module Brainstem
166
186
  configuration.nonheritable!(:description)
167
187
  configuration.nonheritable!(:nodoc)
168
188
  end
189
+
190
+ DEFAULT_FILTER_DATA_TYPE = 'string'
191
+ private_constant :DEFAULT_FILTER_DATA_TYPE
192
+
193
+ def deprecated_type_warning
194
+ ActiveSupport::Deprecation.warn(
195
+ 'Please specify the `type` of the filter as the second argument. If not specified, '\
196
+ 'it will default to `:string`. This default behavior will be deprecated in the next major '\
197
+ 'version and will need to be explicitly specified. '\
198
+ 'e.g. `filter :status, :string, items: ["Started", "Completed"]`',
199
+ caller
200
+ )
201
+ end
169
202
  end
170
203
  end
171
204
  end
@@ -0,0 +1,25 @@
1
+ require 'brainstem/dsl/configuration'
2
+ require 'brainstem/dsl/block_field'
3
+
4
+ module Brainstem
5
+ module DSL
6
+ class ArrayBlockField < BlockField
7
+ def run_on(model, context, helper_instance = Object.new)
8
+ evaluated_models = evaluate_value_on(model, context, helper_instance)
9
+
10
+ evaluated_models.map do |evaluated_model|
11
+ result = {}
12
+
13
+ configuration.each do |field_name, field|
14
+ next unless field.presentable?(model, context)
15
+
16
+ model_for_field = use_parent_value?(field) ? evaluated_model : model
17
+ result[field_name] = field.run_on(model_for_field, context, context[:helper_instance])
18
+ end
19
+
20
+ result
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,69 @@
1
+ require 'brainstem/dsl/configuration'
2
+ require 'brainstem/dsl/field'
3
+
4
+ module Brainstem
5
+ module DSL
6
+ class BlockField < Field
7
+ attr_reader :configuration
8
+
9
+ delegate :to_h, :keys, :has_key?, :each, to: :configuration
10
+
11
+ def initialize(name, type, options, parent_field = nil)
12
+ super(name, type, options)
13
+ @parent_field = parent_field
14
+ end
15
+
16
+ def configuration
17
+ @configuration ||= begin
18
+ if @parent_field && @parent_field.respond_to?(:configuration)
19
+ DSL::Configuration.new(@parent_field.configuration)
20
+ else
21
+ DSL::Configuration.new
22
+ end
23
+ end
24
+ end
25
+
26
+ def [](key)
27
+ configuration[key]
28
+ end
29
+
30
+ def run_on(model, context, helper_instance = Object.new)
31
+ raise NotImplementedError.new("Override this method in a sub class")
32
+ end
33
+
34
+ def self.for(name, type, options, parent_field = nil)
35
+ case type.to_s
36
+ when 'array'
37
+ DSL::ArrayBlockField.new(name, type, options, parent_field)
38
+ when 'hash'
39
+ DSL::HashBlockField.new(name, type, options, parent_field)
40
+ else
41
+ raise "Unknown Brainstem Block Field type encountered: #{type}"
42
+ end
43
+ end
44
+
45
+ def evaluate_value_on(model, context, helper_instance = Object.new)
46
+ if options[:lookup]
47
+ run_on_with_lookup(model, context, helper_instance)
48
+ elsif options[:dynamic]
49
+ proc = options[:dynamic]
50
+ if proc.arity == 1
51
+ helper_instance.instance_exec(model, &proc)
52
+ else
53
+ helper_instance.instance_exec(&proc)
54
+ end
55
+ elsif options[:via]
56
+ model.send(options[:via])
57
+ else
58
+ raise "Block field #{name} can only be evaluated if :dynamic, :lookup, :via options are specified."
59
+ end
60
+ end
61
+
62
+ def use_parent_value?(field)
63
+ return true unless field.options.has_key?(:use_parent_value)
64
+
65
+ field.options[:use_parent_value]
66
+ end
67
+ end
68
+ end
69
+ end
@@ -78,8 +78,8 @@ module Brainstem
78
78
  # keys
79
79
  #
80
80
  def nonheritable!(key)
81
- key = key.to_s
82
- self.nonheritable_keys << key unless self.nonheritable_keys.include?(key)
81
+ formatted_key = format_key(key)
82
+ self.nonheritable_keys << formatted_key unless self.nonheritable_keys.include?(formatted_key)
83
83
  end
84
84
 
85
85
 
@@ -104,7 +104,7 @@ module Brainstem
104
104
  # @return [Hash] the hash, less nonheritable pairs.
105
105
  #
106
106
  def pairs_visible_to_children
107
- to_h.select {|k, v| keys_visible_to_children.include?(k.to_s) }
107
+ to_h.select {|k, v| keys_visible_to_children.include?(format_key(k)) }
108
108
  end
109
109
 
110
110
 
@@ -150,7 +150,7 @@ module Brainstem
150
150
  # @param [Symbol,String] key the key to check for nonheritability.
151
151
  #
152
152
  def key_nonheritable_in_parent?(*key)
153
- parent_nonheritable_keys.include?(key.first.to_s)
153
+ parent_nonheritable_keys.include?(format_key(key.first))
154
154
  end
155
155
 
156
156
 
@@ -164,7 +164,7 @@ module Brainstem
164
164
  # @param [Symbol,String] key the key to check for heritability.
165
165
  #
166
166
  def key_inheritable_in_parent?(*key)
167
- !key_nonheritable_in_parent?(key.first.to_s)
167
+ !key_nonheritable_in_parent?(format_key(key.first))
168
168
  end
169
169
 
170
170
 
@@ -258,6 +258,14 @@ module Brainstem
258
258
  end
259
259
  end
260
260
 
261
+ # @api private
262
+ #
263
+ # Stringifies key if key is not a Proc.
264
+ #
265
+ def format_key(key)
266
+ key.respond_to?(:call) ? key : key.to_s
267
+ end
268
+
261
269
  # An Array-like object that provides `push`, `concat`, `each`, `empty?`, and `to_a` methods that act the combination
262
270
  # of its own entries and those of a parent InheritableAppendSet, if present.
263
271
  class InheritableAppendSet
@@ -9,7 +9,7 @@ module Brainstem
9
9
 
10
10
  def initialize(name, type, options)
11
11
  @name = name.to_s
12
- @type = type
12
+ @type = type.to_s
13
13
  @conditionals = [options[:if]].flatten.compact
14
14
  @options = options
15
15
  end
@@ -38,7 +38,12 @@ module Brainstem
38
38
  options[:optional]
39
39
  end
40
40
 
41
+ # Please override in sub classes to compute value of field with the given arguments.
41
42
  def run_on(model, context, helper_instance = Object.new)
43
+ evaluate_value_on(model, context, helper_instance)
44
+ end
45
+
46
+ def evaluate_value_on(model, context, helper_instance = Object.new)
42
47
  if options[:lookup]
43
48
  run_on_with_lookup(model, context, helper_instance)
44
49
  elsif options[:dynamic]
@@ -53,6 +58,15 @@ module Brainstem
53
58
  end
54
59
  end
55
60
 
61
+ def presentable?(model, context)
62
+ optioned?(context[:optional_fields]) && conditionals_match?(
63
+ model,
64
+ context[:conditionals],
65
+ context[:helper_instance],
66
+ context[:conditional_cache]
67
+ )
68
+ end
69
+
56
70
  def conditionals_match?(model, presenter_conditionals, helper_instance = Object.new, conditional_cache = { model: {}, request: {} })
57
71
  return true unless conditional?
58
72
 
@@ -6,18 +6,36 @@ module Brainstem
6
6
  configuration[name] = DSL::Field.new(name, type, smart_merge(block_options, format_options(options)))
7
7
  end
8
8
 
9
- def fields(name, &block)
10
- descend FieldsBlock, configuration.nest!(name), &block
9
+ def fields(name, type = :hash, options = {}, &block)
10
+ nested_field = DSL::BlockField.for(name, type, smart_merge(block_options, format_options(options)), configuration[name])
11
+ configuration[name] = nested_field
12
+
13
+ descend self.class, nested_field.configuration, merge_parent_options(block_options, options), &block
11
14
  end
12
15
 
13
16
  private
14
17
 
18
+ NON_INHERITABLE_FIELD_OPTIONS = [:dynamic, :via, :lookup, :lookup_fetch, :info, :type, :item_type]
19
+ private_constant :NON_INHERITABLE_FIELD_OPTIONS
20
+
21
+ def merge_parent_options(block_options, parent_options)
22
+ inheritable_options = parent_options.except(*NON_INHERITABLE_FIELD_OPTIONS)
23
+ inheritable_options[:use_parent_value] = true unless inheritable_options.has_key?(:use_parent_value)
24
+
25
+ block_options.deep_dup.merge(inheritable_options)
26
+ end
27
+
15
28
  def smart_merge(block_options, options)
16
29
  if_clause = ([block_options[:if]] + [options[:if]]).flatten(2).compact.uniq
17
30
  block_options.merge(options).tap do |opts|
18
31
  opts.merge!(if: if_clause) if if_clause.present?
19
32
  end
20
33
  end
34
+
35
+ def format_options(options)
36
+ options[:item_type] = options[:item_type].to_s if options.has_key?(:item_type)
37
+ super
38
+ end
21
39
  end
22
40
  end
23
41
  end
@@ -0,0 +1,30 @@
1
+ require 'brainstem/dsl/configuration'
2
+ require 'brainstem/dsl/block_field'
3
+
4
+ module Brainstem
5
+ module DSL
6
+ class HashBlockField < BlockField
7
+ def run_on(model, context, helper_instance = Object.new)
8
+ evaluated_model = nil
9
+ evaluated_model = evaluate_value_on(model, context, helper_instance) if executable?
10
+
11
+ result = {}
12
+ configuration.each do |field_name, field|
13
+ next unless field.presentable?(model, context)
14
+
15
+ model_for_field = (executable? && use_parent_value?(field)) ? evaluated_model : model
16
+ result[field_name] = field.run_on(model_for_field, context, context[:helper_instance]) if model_for_field
17
+ end
18
+
19
+ result
20
+ end
21
+
22
+ def executable?
23
+ (options.keys & EXECUTABLE_OPTIONS).present?
24
+ end
25
+
26
+ EXECUTABLE_OPTIONS = [:dynamic, :via, :lookup]
27
+ private_constant :EXECUTABLE_OPTIONS
28
+ end
29
+ end
30
+ end
@@ -308,15 +308,19 @@ module Brainstem
308
308
  def present_fields(model, context, fields, result = {})
309
309
  fields.each do |name, field|
310
310
  case field
311
- when DSL::Field
312
- if field.conditionals_match?(model, context[:conditionals], context[:helper_instance], context[:conditional_cache]) && field.optioned?(context[:optional_fields])
311
+ when DSL::HashBlockField
312
+ next if field.executable? && !field.presentable?(model, context)
313
+
314
+ # This preserves backwards compatibility
315
+ # In the case of a hash field, the individual attributes will call presentable
316
+ # If none of the individual attributes are presentable we will receive an empty hash
317
+ result[name] = field.run_on(model, context, context[:helper_instance])
318
+ when DSL::ArrayBlockField, DSL::Field
319
+ if field.presentable?(model, context)
313
320
  result[name] = field.run_on(model, context, context[:helper_instance])
314
321
  end
315
- when DSL::Configuration
316
- result[name] ||= {}
317
- present_fields(model, context, field, result[name])
318
322
  else
319
- raise "Unknown Brianstem Field type encountered: #{field}"
323
+ raise "Unknown Brainstem Field type encountered: #{field}"
320
324
  end
321
325
  end
322
326
  result
@@ -33,15 +33,17 @@ module Brainstem
33
33
  end
34
34
 
35
35
  def fields_exist(fields = presenter_class.configuration[:fields])
36
- fields.each do |name, field_or_fields|
37
- case field_or_fields
36
+ fields.each do |name, field|
37
+ case field
38
+ when DSL::HashBlockField
39
+ fields_exist(field) if presenter_class.presents.any? { !field.executable? }
38
40
  when DSL::Field
39
- method_name = field_or_fields.method_name
41
+ method_name = field.method_name
40
42
  if method_name && presenter_class.presents.any? { |klass| !klass.new.respond_to?(method_name) }
41
43
  errors.add(:fields, "'#{name}' is not valid because not all presented classes respond to '#{method_name}'")
42
44
  end
43
- when DSL::Configuration
44
- fields_exist(field_or_fields)
45
+ else
46
+ errors.add(:fields, "'#{name}' is an unknown Brainstem field type")
45
47
  end
46
48
  end
47
49
  end
@@ -61,16 +63,23 @@ module Brainstem
61
63
  end
62
64
 
63
65
  def conditionals_exist(fields = presenter_class.configuration[:fields])
64
- fields.each do |name, field_or_fields|
65
- case field_or_fields
66
+ fields.each do |name, field|
67
+ case field
68
+ when DSL::HashBlockField
69
+ if field.options[:if].present?
70
+ if Array.wrap(field.options[:if]).any? { |conditional| presenter_class.configuration[:conditionals][conditional].nil? }
71
+ errors.add(:fields, "'#{name}' is not valid because one or more of the specified conditionals does not exist")
72
+ end
73
+ end
74
+ conditionals_exist(field) if presenter_class.presents.any? { |klass| !field.executable? }
66
75
  when DSL::Field
67
- if field_or_fields.options[:if].present?
68
- if Array.wrap(field_or_fields.options[:if]).any? { |conditional| presenter_class.configuration[:conditionals][conditional].nil? }
76
+ if field.options[:if].present?
77
+ if Array.wrap(field.options[:if]).any? { |conditional| presenter_class.configuration[:conditionals][conditional].nil? }
69
78
  errors.add(:fields, "'#{name}' is not valid because one or more of the specified conditionals does not exist")
70
79
  end
71
80
  end
72
- when DSL::Configuration
73
- conditionals_exist(field_or_fields)
81
+ else
82
+ errors.add(:fields, "'#{name}' is an unknown Brainstem field type")
74
83
  end
75
84
  end
76
85
  end
@@ -1,3 +1,3 @@
1
1
  module Brainstem
2
- VERSION = "1.1.1"
2
+ VERSION = "1.3.0"
3
3
  end
@@ -143,65 +143,398 @@ module Brainstem
143
143
  end
144
144
 
145
145
 
146
- describe "#root_param_keys" do
147
- let(:nested_param) { { title: { nodoc: nodoc, root: :sprocket } } }
148
- let(:proc_nested_param) { { title: { nodoc: nodoc, root: Proc.new { |klass| klass.brainstem_model_name } } } }
149
- let(:root_param) { { title: { nodoc: nodoc } } }
150
- let(:default_config) { { valid_params: which_param } }
146
+ describe "#params_configuration_tree" do
147
+ let(:default_config) { { valid_params: which_param } }
151
148
 
152
149
  context "non-nested params" do
150
+ let(:root_param) { { Proc.new { 'title' } => { nodoc: nodoc, type: 'string' } } }
153
151
  let(:which_param) { root_param }
154
152
 
155
153
  context "when nodoc" do
156
154
  let(:nodoc) { true }
157
155
 
158
156
  it "rejects the key" do
159
- expect(subject.root_param_keys).to be_empty
157
+ expect(subject.params_configuration_tree).to be_empty
160
158
  end
161
159
  end
162
160
 
163
161
  context "when not nodoc" do
162
+ let(:nodoc) { false }
163
+
164
164
  it "lists it as a root param" do
165
- expect(subject.root_param_keys).to have_key(:title)
165
+ expect(subject.params_configuration_tree).to eq(
166
+ {
167
+ title: {
168
+ _config: { nodoc: nodoc, type: 'string' }
169
+ }
170
+ }.with_indifferent_access
171
+ )
172
+ end
173
+
174
+ context "when param has an item" do
175
+ let(:which_param) { { only: { nodoc: nodoc, type: 'array', item: 'integer' } } }
176
+
177
+ it "lists it as a root param" do
178
+ expect(subject.params_configuration_tree).to eq(
179
+ {
180
+ only: {
181
+ _config: { nodoc: nodoc, type: 'array', item: 'integer' }
182
+ }
183
+ }.with_indifferent_access
184
+ )
185
+ end
166
186
  end
167
187
  end
168
188
  end
169
189
 
170
-
171
190
  context "nested params" do
191
+ let(:root_proc) { Proc.new { 'sprocket' } }
192
+ let(:nested_param) {
193
+ { Proc.new { 'title' } => { nodoc: nodoc, type: 'string', root: root_proc, ancestors: [root_proc] } }
194
+ }
172
195
  let(:which_param) { nested_param }
173
196
 
174
197
  context "when nodoc" do
175
198
  let(:nodoc) { true }
176
199
 
177
200
  it "rejects the key" do
178
- expect(subject.root_param_keys).to be_empty
201
+ expect(subject.params_configuration_tree).to be_empty
179
202
  end
180
203
  end
181
204
 
182
205
  context "when not nodoc" do
183
206
  it "lists it as a nested param" do
184
- expect(subject.root_param_keys).to eq({ sprocket: [ :title ] })
207
+ expect(subject.params_configuration_tree).to eq(
208
+ {
209
+ sprocket: {
210
+ _config: {
211
+ type: 'hash',
212
+ },
213
+ title: {
214
+ _config: {
215
+ nodoc: nodoc,
216
+ type: 'string'
217
+ }
218
+ }
219
+ }
220
+ }.with_indifferent_access
221
+ )
222
+ end
223
+
224
+ context "when nested param has an item" do
225
+ let(:which_param) {
226
+ {
227
+ Proc.new { 'ids' } => { nodoc: nodoc, type: 'array', item: 'integer', root: root_proc, ancestors: [root_proc] }
228
+ }
229
+ }
230
+
231
+ it "lists it as a nested param" do
232
+ expect(subject.params_configuration_tree).to eq(
233
+ {
234
+ sprocket: {
235
+ _config: {
236
+ type: 'hash'
237
+ },
238
+ ids: {
239
+ _config: {
240
+ nodoc: nodoc,
241
+ type: 'array',
242
+ item: 'integer'
243
+ }
244
+ }
245
+ }
246
+ }.with_indifferent_access
247
+ )
248
+ end
185
249
  end
186
250
  end
187
251
  end
188
252
 
189
-
190
253
  context "proc nested params" do
191
- let(:which_param) { proc_nested_param }
254
+ let!(:root_proc) { Proc.new { |klass| klass.brainstem_model_name } }
255
+ let(:proc_nested_param) {
256
+ { Proc.new { 'title' } => { nodoc: nodoc, type: 'string', root: root_proc, ancestors: [root_proc] } }
257
+ }
258
+ let(:which_param) { proc_nested_param }
192
259
 
193
260
  context "when nodoc" do
194
261
  let(:nodoc) { true }
195
262
 
196
263
  it "rejects the key" do
197
- expect(subject.root_param_keys).to be_empty
264
+ expect(subject.params_configuration_tree).to be_empty
198
265
  end
199
266
  end
200
267
 
201
268
  context "when not nodoc" do
202
269
  it "evaluates the proc in the controller's context and lists it as a nested param" do
203
270
  mock.proxy(const).brainstem_model_name
204
- expect(subject.root_param_keys).to eq({ widget: [ :title ] })
271
+
272
+ result = subject.params_configuration_tree
273
+ expect(result.keys).to eq(%w(widget))
274
+
275
+ children_of_the_root = result[:widget].except(:_config)
276
+ expect(children_of_the_root.keys).to eq(%w(title))
277
+
278
+ title_param = children_of_the_root[:title][:_config]
279
+ expect(title_param.keys).to eq(%w(nodoc type))
280
+ expect(title_param[:nodoc]).to eq(nodoc)
281
+ expect(title_param[:type]).to eq('string')
282
+ end
283
+ end
284
+ end
285
+
286
+ context "multi nested params" do
287
+ let(:project_proc) { Proc.new { 'project' } }
288
+ let(:id_proc) { Proc.new { 'id' } }
289
+ let(:task_proc) { Proc.new { 'task' } }
290
+ let(:title_proc) { Proc.new { 'title' } }
291
+ let(:checklist_proc) { Proc.new { 'checklist' } }
292
+ let(:name_proc) { Proc.new { 'name' } }
293
+
294
+ context "has a root & ancestors" do
295
+ let(:which_param) {
296
+ {
297
+ id_proc => {
298
+ type: 'integer'
299
+ },
300
+ task_proc => {
301
+ type: 'hash',
302
+ root: project_proc,
303
+ ancestors: [project_proc]
304
+ },
305
+ title_proc => {
306
+ type: 'string',
307
+ ancestors: [project_proc, task_proc]
308
+ },
309
+ checklist_proc => {
310
+ type: 'array',
311
+ item: 'hash',
312
+ ancestors: [project_proc, task_proc]
313
+ },
314
+ name_proc => {
315
+ type: 'string',
316
+ ancestors: [project_proc, task_proc, checklist_proc]
317
+ }
318
+ }
319
+ }
320
+
321
+ context "when a leaf param has no doc" do
322
+ before do
323
+ which_param[name_proc][:nodoc] = true
324
+ end
325
+
326
+ it "rejects the key" do
327
+ expect(subject.params_configuration_tree).to eq(
328
+ {
329
+ id: {
330
+ _config: {
331
+ type: 'integer',
332
+ }
333
+ },
334
+ project: {
335
+ _config: {
336
+ type: 'hash',
337
+ },
338
+ task: {
339
+ _config: {
340
+ type: 'hash',
341
+ },
342
+ title: {
343
+ _config: {
344
+ type: 'string'
345
+ }
346
+ },
347
+ checklist: {
348
+ _config: {
349
+ type: 'array',
350
+ item: 'hash',
351
+ }
352
+ },
353
+ },
354
+ },
355
+ }.with_indifferent_access
356
+ )
357
+ end
358
+ end
359
+
360
+ context "when nodoc on a parent param" do
361
+ before do
362
+ which_param[checklist_proc][:nodoc] = true
363
+ which_param[name_proc][:nodoc] = true # This will be inherited from the parent when the param is defined.
364
+ end
365
+
366
+ it "rejects the parent key and its children" do
367
+ expect(subject.params_configuration_tree).to eq(
368
+ {
369
+ id: {
370
+ _config: {
371
+ type: 'integer'
372
+ }
373
+ },
374
+ project: {
375
+ _config: {
376
+ type: 'hash'
377
+ },
378
+ task: {
379
+ _config: {
380
+ type: 'hash',
381
+ },
382
+ title: {
383
+ _config: {
384
+ type: 'string'
385
+ }
386
+ },
387
+ },
388
+ },
389
+ }.with_indifferent_access
390
+ )
391
+ end
392
+ end
393
+
394
+ context "when not nodoc" do
395
+ it "evaluates the proc in the controller's context and lists it as a nested param" do
396
+ expect(subject.params_configuration_tree).to eq(
397
+ {
398
+ id: {
399
+ _config: {
400
+ type: 'integer'
401
+ }
402
+ },
403
+ project: {
404
+ _config: {
405
+ type: 'hash',
406
+ },
407
+ task: {
408
+ _config: {
409
+ type: 'hash',
410
+ },
411
+ title: {
412
+ _config: {
413
+ type: 'string'
414
+ }
415
+ },
416
+ checklist: {
417
+ _config: {
418
+ type: 'array',
419
+ item: 'hash'
420
+ },
421
+ name: {
422
+ _config: {
423
+ type: 'string'
424
+ }
425
+ },
426
+ },
427
+ },
428
+ },
429
+ }.with_indifferent_access
430
+ )
431
+ end
432
+ end
433
+ end
434
+
435
+ context "has only ancestors" do
436
+ let(:which_param) {
437
+ {
438
+ task_proc => {
439
+ type: 'hash',
440
+ },
441
+ title_proc => {
442
+ type: 'string',
443
+ ancestors: [task_proc]
444
+ },
445
+ checklist_proc => {
446
+ type: 'array',
447
+ item: 'hash',
448
+ ancestors: [task_proc]
449
+ },
450
+ name_proc => {
451
+ type: 'string',
452
+ ancestors: [task_proc, checklist_proc]
453
+ }
454
+ }
455
+ }
456
+
457
+ context "when a leaf param has no doc" do
458
+ before do
459
+ which_param[name_proc][:nodoc] = true
460
+ end
461
+
462
+ it "rejects the key" do
463
+ expect(subject.params_configuration_tree).to eq(
464
+ {
465
+ task: {
466
+ _config: {
467
+ type: 'hash'
468
+ },
469
+ title: {
470
+ _config: {
471
+ type: 'string'
472
+ }
473
+ },
474
+ checklist: {
475
+ _config: {
476
+ type: 'array',
477
+ item: 'hash'
478
+ }
479
+ },
480
+ },
481
+ }.with_indifferent_access
482
+ )
483
+ end
484
+ end
485
+
486
+ context "when parent param has nodoc" do
487
+ before do
488
+ which_param[checklist_proc][:nodoc] = true
489
+ which_param[name_proc][:nodoc] = true # This will be inherited from the parent when the param is defined.
490
+ end
491
+
492
+ it "rejects the parent key and its children" do
493
+ expect(subject.params_configuration_tree).to eq(
494
+ {
495
+ task: {
496
+ _config: {
497
+ type: 'hash'
498
+ },
499
+ title: {
500
+ _config: {
501
+ type: 'string'
502
+ }
503
+ }
504
+ }
505
+ }.with_indifferent_access
506
+ )
507
+ end
508
+ end
509
+
510
+ context "when not nodoc" do
511
+ it "evaluates the proc in the controller's context and lists it as a nested param" do
512
+ expect(subject.params_configuration_tree).to eq(
513
+ {
514
+ task: {
515
+ _config: {
516
+ type: 'hash'
517
+ },
518
+ title: {
519
+ _config: {
520
+ type: 'string'
521
+ }
522
+ },
523
+ checklist: {
524
+ _config: {
525
+ type: 'array',
526
+ item: 'hash',
527
+ },
528
+ name: {
529
+ _config: {
530
+ type: 'string',
531
+ }
532
+ },
533
+ },
534
+ },
535
+ }.with_indifferent_access
536
+ )
537
+ end
205
538
  end
206
539
  end
207
540
  end