poise 2.2.3 → 2.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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +14 -0
  4. data/Gemfile +0 -2
  5. data/lib/poise.rb +3 -1
  6. data/lib/poise/backports.rb +27 -0
  7. data/lib/poise/backports/not_passed.rb +52 -0
  8. data/lib/poise/helpers.rb +1 -0
  9. data/lib/poise/helpers/chefspec_matchers.rb +5 -1
  10. data/lib/poise/helpers/inversion.rb +36 -13
  11. data/lib/poise/helpers/option_collector.rb +15 -4
  12. data/lib/poise/helpers/resource_name.rb +1 -1
  13. data/lib/poise/helpers/resource_subclass.rb +81 -0
  14. data/lib/poise/helpers/subresources/child.rb +50 -9
  15. data/lib/poise/helpers/subresources/container.rb +33 -6
  16. data/lib/poise/resource.rb +3 -1
  17. data/lib/poise/subcontext/resource_collection.rb +20 -1
  18. data/lib/poise/utils.rb +79 -7
  19. data/lib/poise/utils/resource_provider_mixin.rb +2 -2
  20. data/lib/poise/version.rb +1 -1
  21. data/test/spec/backports/not_passed_spec.rb +29 -0
  22. data/test/spec/helpers/chefspec_matchers_spec.rb +17 -0
  23. data/test/spec/helpers/inversion_spec.rb +72 -0
  24. data/test/spec/helpers/lwrp_polyfill_spec.rb +9 -0
  25. data/test/spec/helpers/option_collector_spec.rb +66 -30
  26. data/test/spec/helpers/resource_name_spec.rb +15 -2
  27. data/test/spec/helpers/resource_subclass_spec.rb +97 -0
  28. data/test/spec/helpers/subresources/child_spec.rb +234 -2
  29. data/test/spec/helpers/subresources/container_spec.rb +37 -0
  30. data/test/spec/resource_spec.rb +31 -2
  31. data/test/spec/subcontext/resource_collection_spec.rb +99 -0
  32. data/test/spec/utils/resource_provider_mixin_spec.rb +22 -0
  33. data/test/spec/utils_spec.rb +187 -1
  34. metadata +11 -3
  35. data/Berksfile.lock +0 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5906280e0c3c8e0555e6a255dad854a6a68f2d11
4
- data.tar.gz: 611ffdede3b4f1021e18d88299172aea3239890d
3
+ metadata.gz: 9346bbce570e141a731a72396f1d1ce0e6da5bae
4
+ data.tar.gz: 5a0d996080b83353b59eee2484b657613472d12c
5
5
  SHA512:
6
- metadata.gz: 075e7d9b3aecce2cc748430ca9bca6e3a33b3db88aa63701fb42269b1254b4f2e7915843bbafb85c239729d1b2c23b6bc775db7bc297435b9555f7a77f31b2d3
7
- data.tar.gz: f1aa2c2a67df267725169799a924df19f2f16f3927b2ec492b891886eff5aff00be251a5bbbe23d4421e146a902fe687ae936c17257a6ee469d7fecb2343b714
6
+ metadata.gz: f53889732b8ffe28dd15938fcb24d87686e4dfc984b3b44807fdbffe3412e0dce62263efa200faf3617f7bfe9beb4b6873a4121d2fbee92adf7477452110e735
7
+ data.tar.gz: d06c1b67c3bb6fd46edc21794cd1e28af06d89938e973da7dddeccf1f0eced60ef32295d14b78e6d927e4d8299dbdbdebd49b24c27308097746563b7b880ba32
data/.gitignore CHANGED
@@ -1,4 +1,5 @@
1
1
  Gemfile.lock
2
+ Berksfile.lock
2
3
  test/gemfiles/*.lock
3
4
  .kitchen/
4
5
  .kitchen.local.yml
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## v2.3.0
4
+
5
+ * New helper: `ResourceSubclass`, a helper for subclassing a resource while
6
+ still using the providers as the base class.
7
+ * New feature: Non-default containers. Use `container_default: false` to mark
8
+ a container class as ineligible for default lookup.
9
+ * New feature: parent attribute defaults. You can set a `parent_default` to
10
+ provide a default value for the parent of a resource. This supports the
11
+ `lazy { }` helper as with normal default values.
12
+ * New feature: use `forced_keys: [:name]` on an option collector property to
13
+ force keys that would otherwise be clobbered by resource methods.
14
+ * Can enable verbose logging mode via a node attribute in addition to an
15
+ environment variable.
16
+
3
17
  ## v2.2.3
4
18
 
5
19
  * Add `ancestor_send` utility method for use in other helpers.
data/Gemfile CHANGED
@@ -29,5 +29,3 @@ end
29
29
 
30
30
  dev_gem 'halite'
31
31
  dev_gem 'poise-boiler'
32
-
33
- gem 'kitchen-docker', github: 'portertech/kitchen-docker'
@@ -22,7 +22,9 @@ require 'poise/utils/resource_provider_mixin'
22
22
 
23
23
  module Poise
24
24
  include Poise::Utils::ResourceProviderMixin
25
+ autoload :Backports, 'poise/backports'
25
26
  autoload :Helpers, 'poise/helpers'
27
+ autoload :NOT_PASSED, 'poise/backports/not_passed'
26
28
  autoload :Provider, 'poise/provider'
27
29
  autoload :Resource, 'poise/resource'
28
30
  autoload :Subcontext, 'poise/subcontext'
@@ -57,7 +59,7 @@ def Poise(options={})
57
59
  # Resource-specific options.
58
60
  if klass < Chef::Resource
59
61
  klass.poise_subresource(options[:parent], options[:parent_optional], options[:parent_auto]) if options[:parent]
60
- klass.poise_subresource_container(options[:container_namespace]) if options[:container]
62
+ klass.poise_subresource_container(options[:container_namespace], options[:container_default]) if options[:container]
61
63
  klass.poise_fused if options[:fused]
62
64
  klass.poise_inversion(options[:inversion_options_resource]) if options[:inversion]
63
65
  end
@@ -0,0 +1,27 @@
1
+ #
2
+ # Copyright 2015, Noah Kantrowitz
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+
18
+ module Poise
19
+ # Backported features from Chef to be able to use them with older versions.
20
+ #
21
+ # @since 2.3.0
22
+ module Backports
23
+ autoload :NOT_PASSED, 'poise/backports/not_passed'
24
+ end
25
+
26
+ autoload :NOT_PASSED, 'poise/backports/not_passed'
27
+ end
@@ -0,0 +1,52 @@
1
+ #
2
+ # Copyright 2015, Noah Kantrowitz
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ begin
18
+ require 'chef/constants'
19
+ rescue LoadError
20
+ # This space left intentionally blank.
21
+ end
22
+
23
+
24
+ module Poise
25
+ module Backports
26
+ # A sentinel value for optional arguments where nil is a valid value.
27
+ # @since 2.3.0
28
+ # @!parse NOT_PASSED = Object.new
29
+ NOT_PASSED = if defined?(Chef::NOT_PASSED)
30
+ Chef::NOT_PASSED
31
+ else
32
+ # Copyright 2015, Chef Software Inc.
33
+ # Used under Apache License, Version 2.0.
34
+ Object.new.tap do |not_passed|
35
+ def not_passed.to_s
36
+ "NOT_PASSED"
37
+ end
38
+ def not_passed.inspect
39
+ to_s
40
+ end
41
+ not_passed.freeze
42
+ end
43
+ end
44
+
45
+ end
46
+
47
+ # An alias to {Backports::NOT_PASSED} to avoid typing so much.
48
+ #
49
+ # @since 2.3.0
50
+ # @see Backports::NOT_PASSED
51
+ NOT_PASSED = Backports::NOT_PASSED
52
+ end
@@ -28,6 +28,7 @@ module Poise
28
28
  autoload :OptionCollector, 'poise/helpers/option_collector'
29
29
  autoload :ResourceCloning, 'poise/helpers/resource_cloning'
30
30
  autoload :ResourceName, 'poise/helpers/resource_name'
31
+ autoload :ResourceSubclass, 'poise/helpers/resource_subclass'
31
32
  autoload :Subresources, 'poise/helpers/subresources'
32
33
  autoload :TemplateContent, 'poise/helpers/template_content'
33
34
  end
@@ -62,6 +62,10 @@ module Poise
62
62
  # @see Resource::ResourceName.provides
63
63
  def provides(name, *args, &block)
64
64
  ChefSpec.define_matcher(name) if defined?(ChefSpec)
65
+ # Call #actions here to grab any actions from a parent class.
66
+ actions.each do |action|
67
+ ChefspecMatchers.create_matcher(name, action)
68
+ end
65
69
  super(name, *args, &block)
66
70
  end
67
71
 
@@ -72,7 +76,7 @@ module Poise
72
76
  super.tap do |actions|
73
77
  actions.each do |action|
74
78
  ChefspecMatchers.create_matcher(resource_name, action)
75
- end
79
+ end if resource_name
76
80
  end
77
81
  end
78
82
 
@@ -19,6 +19,7 @@ require 'chef/node_map'
19
19
  require 'chef/provider'
20
20
  require 'chef/resource'
21
21
 
22
+ require 'poise/backports'
22
23
  require 'poise/helpers/defined_in'
23
24
  require 'poise/error'
24
25
  require 'poise/helpers/inversion/options_resource'
@@ -117,6 +118,9 @@ module Poise
117
118
  define_singleton_method(:name) do
118
119
  "#{enclosing_class}::OptionsResource"
119
120
  end
121
+ define_singleton_method(:inversion_resource_class) do
122
+ enclosing_class
123
+ end
120
124
  provides(options_resource_name)
121
125
  inversion_resource(name)
122
126
  end
@@ -126,6 +130,9 @@ module Poise
126
130
  define_singleton_method(:name) do
127
131
  "#{enclosing_class}::OptionsProvider"
128
132
  end
133
+ define_singleton_method(:inversion_resource_class) do
134
+ enclosing_class
135
+ end
129
136
  provides(options_resource_name)
130
137
  end
131
138
  end
@@ -170,40 +177,48 @@ module Poise
170
177
  module ClassMethods
171
178
  # @overload inversion_resource()
172
179
  # Return the inversion resource name for this class.
173
- # @return [Symbol]
180
+ # @return [Symbo, nill]
174
181
  # @overload inversion_resource(val)
175
182
  # Set the inversion resource name for this class. You can pass either
176
183
  # a symbol in DSL format or a resource class that uses Poise. This
177
184
  # name is used to determine which resources the inversion provider is
178
185
  # a candidate for.
179
186
  # @param val [Symbol, Class] Name to set.
180
- # @return [Symbol]
181
- def inversion_resource(val=nil)
182
- if val
187
+ # @return [Symbol, nil]
188
+ def inversion_resource(val=Poise::NOT_PASSED)
189
+ if val != Poise::NOT_PASSED
183
190
  val = val.resource_name if val.is_a?(Class)
184
191
  Chef::Log.debug("[#{self.name}] Setting inversion resource to #{val}")
185
192
  @poise_inversion_resource = val.to_sym
186
193
  end
187
- @poise_inversion_resource || (superclass.respond_to?(:inversion_resource) ? superclass.inversion_resource : nil)
194
+ if defined?(@poise_inversion_resource)
195
+ @poise_inversion_resource
196
+ else
197
+ Poise::Utils.ancestor_send(self, :inversion_resource, default: nil)
198
+ end
188
199
  end
189
200
 
190
201
  # @overload inversion_attribute()
191
202
  # Return the inversion attribute name(s) for this class.
192
- # @return [Array<String>]
203
+ # @return [Array<String>, nil]
193
204
  # @overload inversion_attribute(val)
194
205
  # Set the inversion attribute name(s) for this class. This is
195
206
  # used by {.resolve_inversion_attribute} to load configuration data
196
207
  # from node attributes. To specify a nested attribute pass an array
197
208
  # of strings corresponding to the keys.
198
209
  # @param val [String, Array<String>] Attribute path.
199
- # @return [Array<String>]
200
- def inversion_attribute(val=nil)
201
- if val
210
+ # @return [Array<String>, nil]
211
+ def inversion_attribute(val=Poise::NOT_PASSED)
212
+ if val != Poise::NOT_PASSED
202
213
  # Coerce to an array of strings.
203
214
  val = Array(val).map {|name| name.to_s }
204
215
  @poise_inversion_attribute = val
205
216
  end
206
- @poise_inversion_attribute || (superclass.respond_to?(:inversion_attribute) ? superclass.inversion_attribute : nil)
217
+ if defined?(@poise_inversion_attribute)
218
+ @poise_inversion_attribute
219
+ else
220
+ Poise::Utils.ancestor_send(self, :inversion_attribute, default: nil)
221
+ end
207
222
  end
208
223
 
209
224
  # Default attribute paths to check for inversion options. Based on
@@ -235,6 +250,7 @@ module Poise
235
250
  def resolve_inversion_attribute(node)
236
251
  # Default to using just the name of the cookbook.
237
252
  attribute_names = inversion_attribute || default_inversion_attributes(node)
253
+ return {} if attribute_names.empty?
238
254
  attribute_names.inject(node) do |memo, key|
239
255
  memo[key] || begin
240
256
  raise Poise::Error.new("Attribute #{key} not set when expanding inversion attribute for #{self.name}: #{memo}")
@@ -257,13 +273,13 @@ module Poise
257
273
  # Class-level defaults.
258
274
  opts.update(default_inversion_options(node, resource))
259
275
  # Resource options for all providers.
260
- opts.update(resource.options)
276
+ opts.update(resource.options) if resource.respond_to?(:options)
261
277
  # Global provider from node attributes.
262
278
  opts.update(provider: attrs['provider']) if attrs['provider']
263
279
  # Attribute options for all providers.
264
280
  opts.update(attrs['options']) if attrs['options']
265
281
  # Resource options for this provider.
266
- opts.update(resource.options(provides))
282
+ opts.update(resource.options(provides)) if resource.respond_to?(:options)
267
283
  # Attribute options for this resource name.
268
284
  opts.update(attrs[resource.name]) if attrs[resource.name]
269
285
  # Options resource options for all providers.
@@ -323,7 +339,14 @@ module Poise
323
339
  # @return [Boolean]
324
340
  def provides?(node, resource)
325
341
  raise Poise::Error.new("Inversion resource name not set for #{self.name}") unless inversion_resource
326
- return false unless resource.resource_name == inversion_resource
342
+ resource_name_equivalents = {resource.resource_name => true}
343
+ # If subclass_providers! might be in play, check for those names too.
344
+ if resource.class.respond_to?(:subclass_resource_equivalents)
345
+ resource.class.subclass_resource_equivalents.each do |name|
346
+ resource_name_equivalents[name] = true
347
+ end
348
+ end
349
+ return false unless resource_name_equivalents[inversion_resource]
327
350
  provider_name = resolve_inversion_provider(node, resource)
328
351
  Chef::Log.debug("[#{resource}] Checking provides? on #{self.name}. Got provider_name #{provider_name.inspect}")
329
352
  provider_name == provides.to_s || ( provider_name == 'auto' && provides_auto?(node, resource) )
@@ -44,18 +44,25 @@ module Poise
44
44
  class OptionEvalContext
45
45
  attr_reader :_options
46
46
 
47
- def initialize(parent)
47
+ def initialize(parent, forced_keys)
48
48
  @parent = parent
49
+ @forced_keys = forced_keys
49
50
  @_options = {}
50
51
  end
51
52
 
52
53
  def method_missing(method_sym, *args, &block)
54
+ # Deal with forced keys.
55
+ if @forced_keys.include?(method_sym)
56
+ @_options[method_sym] = args.first || block if !args.empty? || block
57
+ return @_options[method_sym]
58
+ end
59
+ # Try the resource context.
53
60
  @parent.send(method_sym, *args, &block)
54
61
  rescue NameError
55
62
  # Even though method= in the block will set a variable instead of
56
63
  # calling method_missing, still try to cope in case of self.method=.
57
64
  method_sym = method_sym.to_s.chomp('=').to_sym
58
- if args.length > 0 || block
65
+ if !args.empty? || block
59
66
  @_options[method_sym] = args.first || block
60
67
  elsif !@_options.include?(method_sym)
61
68
  # We haven't seen this name before, re-raise the NameError.
@@ -86,8 +93,12 @@ module Poise
86
93
  # @param parser [Proc, Symbol] Optional parser method. If a symbol it is
87
94
  # called as a method on self. Takes a non-hash value and returns a
88
95
  # hash of its parsed representation.
89
- def option_collector_attribute(name, default: {}, parser: nil)
96
+ # @param forced_keys [Array<Symbol>, Set<Symbol>] Method names that will be forced
97
+ # to be options rather than calls to the parent resource.
98
+ def option_collector_attribute(name, default: {}, parser: nil, forced_keys: Set.new)
90
99
  raise Poise::Error.new("Parser must be a Proc or Symbol: #{parser.inspect}") if parser && !(parser.is_a?(Proc) || parser.is_a?(Symbol))
100
+ # Cast to a set at definition time.
101
+ forced_keys = Set.new(forced_keys) unless forced_keys.is_a?(Set)
91
102
  # Unlike LWRPBase.attribute, I don't care about Ruby 1.8. Worlds tiniest violin.
92
103
  define_method(name.to_sym) do |arg=nil, &block|
93
104
  iv_sym = :"@#{name}"
@@ -110,7 +121,7 @@ module Poise
110
121
  value.update(arg)
111
122
  end
112
123
  if block
113
- ctx = OptionEvalContext.new(self)
124
+ ctx = OptionEvalContext.new(self, forced_keys)
114
125
  ctx.instance_exec(&block)
115
126
  value.update(ctx._options)
116
127
  end
@@ -61,7 +61,7 @@ module Poise
61
61
  end
62
62
  end
63
63
  # Store the name for later.
64
- @provides_name = name
64
+ @provides_name ||= name
65
65
  # Call the original if present. The defined? is for old Chef.
66
66
  super(name, *args, &block) if defined?(super)
67
67
  end
@@ -0,0 +1,81 @@
1
+ #
2
+ # Copyright 2015, Noah Kantrowitz
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ require 'poise/error'
18
+ require 'poise/helpers/resource_name'
19
+
20
+
21
+ module Poise
22
+ module Helpers
23
+ # A resource mixin to help subclass existing resources.
24
+ #
25
+ # @since 2.3.0
26
+ module ResourceSubclass
27
+ include ResourceName
28
+
29
+ module ClassMethods
30
+ def subclass_providers!(superclass_resource_name=nil)
31
+ resource_name = self.resource_name
32
+ superclass_resource_name ||= if superclass.respond_to?(:resource_name)
33
+ superclass.resource_name
34
+ elsif superclass.respond_to?(:dsl_name)
35
+ superclass.dsl_name
36
+ else
37
+ raise Poise::Error.new("Unable to determine superclass resource name for #{superclass}. Please specify name manually via subclass_providers!('name').")
38
+ end.to_sym
39
+ # Deal with the node maps.
40
+ node_maps = {}
41
+ node_maps['handler map'] = Chef.provider_handler_map if defined?(Chef.provider_handler_map)
42
+ node_maps['priority map'] = Chef.provider_priority_map if defined?(Chef.provider_priority_map)
43
+ # Patch anything in the descendants tracker.
44
+ Chef::Provider.descendants.each do |provider|
45
+ node_maps["#{provider} node map"] = provider.node_map if defined?(provider.node_map)
46
+ end if defined?(Chef::Provider.descendants)
47
+ node_maps.each do |map_name, node_map|
48
+ map = node_map.respond_to?(:map, true) ? node_map.send(:map) : node_map.instance_variable_get(:@map)
49
+ if map.include?(superclass_resource_name)
50
+ Chef::Log.debug("[#{self}] Copying provider mapping in #{map_name} from #{superclass_resource_name} to #{resource_name}")
51
+ map[resource_name] = map[superclass_resource_name].dup
52
+ end
53
+ end
54
+ # Add any needed equivalent names.
55
+ if superclass.respond_to?(:subclass_resource_equivalents)
56
+ subclass_resource_equivalents.concat(superclass.subclass_resource_equivalents)
57
+ else
58
+ subclass_resource_equivalents << superclass_resource_name
59
+ end
60
+ end
61
+
62
+ # An array of names for the resources this class is equivalent to for
63
+ # the purposes of provider resolution.
64
+ #
65
+ # @return [Array<Symbol>]
66
+ def subclass_resource_equivalents
67
+ @subclass_resource_names ||= [resource_name.to_sym]
68
+ end
69
+
70
+ # @api private
71
+ def included(klass)
72
+ super
73
+ klass.extend(ClassMethods)
74
+ end
75
+ end
76
+
77
+ extend ClassMethods
78
+ end
79
+
80
+ end
81
+ end
@@ -61,7 +61,7 @@ module Poise
61
61
  if self.class.parent_type == true
62
62
  raise NoMethodError.new("undefined method `parent' for #{self}")
63
63
  end
64
- _parent(:parent, self.class.parent_type, self.class.parent_optional, self.class.parent_auto, *args)
64
+ _parent(:parent, self.class.parent_type, self.class.parent_optional, self.class.parent_auto, self.class.parent_default, *args)
65
65
  end
66
66
 
67
67
  # Register ourself with parents in case this is not a nested resource.
@@ -71,7 +71,7 @@ module Poise
71
71
  super
72
72
  self.class.parent_attributes.each_key do |name|
73
73
  parent = self.send(name)
74
- parent.register_subresource(self) if parent
74
+ parent.register_subresource(self) if parent && parent.respond_to?(:register_subresource)
75
75
  end
76
76
  end
77
77
 
@@ -81,7 +81,7 @@ module Poise
81
81
  #
82
82
  # @since 2.0.0
83
83
  # @see #parent
84
- def _parent(name, parent_type, parent_optional, parent_auto, *args)
84
+ def _parent(name, parent_type, parent_optional, parent_auto, parent_default, *args)
85
85
  # Allow using a DSL symbol as the parent type.
86
86
  if parent_type.is_a?(Symbol)
87
87
  parent_type = Chef::Resource.resource_for_node(parent_type, node)
@@ -96,7 +96,18 @@ module Poise
96
96
  else
97
97
  if val.is_a?(String) && !val.include?('[')
98
98
  raise Poise::Error.new("Cannot use a string #{name} without defining a parent type") if parent_type == Chef::Resource
99
- val = "#{parent_type.resource_name}[#{val}]"
99
+ # Try to find the most recent instance of parent_type with a
100
+ # matching name. This takes subclassing parent_type into account.
101
+ found_val = nil
102
+ iterator = run_context.resource_collection.respond_to?(:recursive_each) ? :recursive_each : :each
103
+ # This will find the last matching value due to overwriting
104
+ # found_val as it goes. Will be the nearest match.
105
+ run_context.resource_collection.public_send(iterator) do |res|
106
+ found_val = res if res.is_a?(parent_type) && res.name == val
107
+ end
108
+ # If found_val is nil, fall back to using lookup even though
109
+ # it won't work with subclassing, better than nothing?
110
+ val = found_val || "#{parent_type.resource_name}[#{val}]"
100
111
  end
101
112
  if val.is_a?(String) || val.is_a?(Hash)
102
113
  parent = @run_context.resource_collection.find(val)
@@ -109,7 +120,14 @@ module Poise
109
120
  end
110
121
  parent_ref = ParentRef.new(parent)
111
122
  elsif !parent_ref || !parent_ref.resource
112
- if parent_auto
123
+ if parent_default
124
+ parent = if parent_default.is_a?(Chef::DelayedEvaluator)
125
+ instance_eval(&parent_default)
126
+ else
127
+ parent_default
128
+ end
129
+ end
130
+ if !parent && parent_auto
113
131
  # Automatic sibling lookup for sequential composition.
114
132
  # Find the last instance of the parent class as the default parent.
115
133
  # This is super flaky and should only be a last resort.
@@ -121,6 +139,7 @@ module Poise
121
139
  else
122
140
  parent = parent_ref.resource
123
141
  end
142
+ raise Poise::Error.new("Cannot set the #{name} of #{self} to itself") if parent.equal?(self)
124
143
  # Store the ivar back.
125
144
  instance_variable_set(:"@#{name}", parent_ref)
126
145
  # Return the actual resource.
@@ -141,7 +160,9 @@ module Poise
141
160
  # Setting to true shouldn't actually do anything if a type was already set.
142
161
  @parent_type = type unless type == true && !@parent_type.nil?
143
162
  end
144
- @parent_type || Poise::Utils.ancestor_send(self, :parent_type, default: Chef::Resource)
163
+ # First ancestor_send looks for a non-true && non-default value,
164
+ # second one is to check for default vs true if no real value is found.
165
+ @parent_type || Poise::Utils.ancestor_send(self, :parent_type, ignore: [Chef::Resource, true]) || Poise::Utils.ancestor_send(self, :parent_type, default: Chef::Resource)
145
166
  end
146
167
 
147
168
  # @overload parent_optional()
@@ -180,6 +201,26 @@ module Poise
180
201
  end
181
202
  end
182
203
 
204
+ # @overload parent_default()
205
+ # Get the default value for the default parent link on this resource.
206
+ # @since 2.3.0
207
+ # @return [Object, Chef::DelayedEvaluator]
208
+ # @overload parent_default(val)
209
+ # Set the default value for the default parent link on this resource.
210
+ # @since 2.3.0
211
+ # @param val [Object, Chef::DelayedEvaluator] Default value to set.
212
+ # @return [Object, Chef::DelayedEvaluator]
213
+ def parent_default(*args)
214
+ unless args.empty?
215
+ @parent_default = args.first
216
+ end
217
+ if defined?(@parent_default)
218
+ @parent_default
219
+ else
220
+ Poise::Utils.ancestor_send(self, :parent_default)
221
+ end
222
+ end
223
+
183
224
  # Create a new kind of parent link.
184
225
  #
185
226
  # @since 2.0.0
@@ -189,11 +230,11 @@ module Poise
189
230
  # @param optional [Boolean] If the parent is optional.
190
231
  # @param auto [Boolean] If the parent is auto-detected.
191
232
  # @return [void]
192
- def parent_attribute(name, type: Chef::Resource, optional: false, auto: true)
233
+ def parent_attribute(name, type: Chef::Resource, optional: false, auto: true, default: nil)
193
234
  name = :"parent_#{name}"
194
235
  (@parent_attributes ||= {})[name] = type
195
236
  define_method(name) do |*args|
196
- _parent(name, type, optional, auto, *args)
237
+ _parent(name, type, optional, auto, default, *args)
197
238
  end
198
239
  end
199
240
 
@@ -204,7 +245,7 @@ module Poise
204
245
  def parent_attributes
205
246
  {}.tap do |attrs|
206
247
  # Grab superclass's attributes if possible.
207
- attrs.update(superclass.parent_attributes) if superclass.respond_to?(:parent_attributes)
248
+ attrs.update(Poise::Utils.ancestor_send(self, :parent_attributes, default: {}))
208
249
  # Local default parent.
209
250
  attrs[:parent] = parent_type
210
251
  # Extra locally defined parents.