poise 2.2.3 → 2.3.0

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