chef-resource 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +23 -0
- data/LICENSE +201 -0
- data/README.md +264 -0
- data/Rakefile +8 -0
- data/files/lib/chef_resource.rb +24 -0
- data/files/lib/chef_resource/camel_case.rb +23 -0
- data/files/lib/chef_resource/chef.rb +102 -0
- data/files/lib/chef_resource/chef_dsl/chef_cookbook_compiler.rb +44 -0
- data/files/lib/chef_resource/chef_dsl/chef_recipe.rb +10 -0
- data/files/lib/chef_resource/chef_dsl/chef_recipe_dsl_extensions.rb +84 -0
- data/files/lib/chef_resource/chef_dsl/chef_resource_base.rb +12 -0
- data/files/lib/chef_resource/chef_dsl/chef_resource_class_extensions.rb +30 -0
- data/files/lib/chef_resource/chef_dsl/chef_resource_extensions.rb +224 -0
- data/files/lib/chef_resource/chef_dsl/chef_resource_log.rb +54 -0
- data/files/lib/chef_resource/chef_dsl/resource_container_module.rb +80 -0
- data/files/lib/chef_resource/chef_dsl/resource_definition_dsl.rb +128 -0
- data/files/lib/chef_resource/constants.rb +8 -0
- data/files/lib/chef_resource/errors.rb +31 -0
- data/files/lib/chef_resource/lazy_proc.rb +82 -0
- data/files/lib/chef_resource/output/nested_converge.rb +91 -0
- data/files/lib/chef_resource/output/nested_converge/open_resource.rb +113 -0
- data/files/lib/chef_resource/output/region_stream.rb +145 -0
- data/files/lib/chef_resource/output/simple_output.rb +83 -0
- data/files/lib/chef_resource/resource.rb +428 -0
- data/files/lib/chef_resource/resource/resource_log.rb +197 -0
- data/files/lib/chef_resource/resource/resource_type.rb +74 -0
- data/files/lib/chef_resource/resource/struct_property.rb +39 -0
- data/files/lib/chef_resource/resource/struct_property_type.rb +185 -0
- data/files/lib/chef_resource/resource/struct_resource.rb +410 -0
- data/files/lib/chef_resource/resource/struct_resource_base.rb +11 -0
- data/files/lib/chef_resource/resource/struct_resource_type.rb +275 -0
- data/files/lib/chef_resource/simple_struct.rb +121 -0
- data/files/lib/chef_resource/type.rb +371 -0
- data/files/lib/chef_resource/types.rb +4 -0
- data/files/lib/chef_resource/types/boolean.rb +16 -0
- data/files/lib/chef_resource/types/byte_size.rb +10 -0
- data/files/lib/chef_resource/types/date_time_type.rb +18 -0
- data/files/lib/chef_resource/types/date_type.rb +18 -0
- data/files/lib/chef_resource/types/float_type.rb +28 -0
- data/files/lib/chef_resource/types/integer_type.rb +53 -0
- data/files/lib/chef_resource/types/interval.rb +21 -0
- data/files/lib/chef_resource/types/path.rb +39 -0
- data/files/lib/chef_resource/types/pathname_type.rb +34 -0
- data/files/lib/chef_resource/types/string_type.rb +16 -0
- data/files/lib/chef_resource/types/symbol_type.rb +18 -0
- data/files/lib/chef_resource/types/uri_type.rb +37 -0
- data/files/lib/chef_resource/version.rb +3 -0
- data/spec/integration/chef.rb +81 -0
- data/spec/integration/struct_spec.rb +611 -0
- data/spec/integration/struct_state_spec.rb +538 -0
- data/spec/integration/type_spec.rb +1123 -0
- data/spec/integration/validation_spec.rb +207 -0
- data/spec/support/spec_support.rb +7 -0
- metadata +167 -0
@@ -0,0 +1,410 @@
|
|
1
|
+
require 'chef_resource/errors'
|
2
|
+
require 'chef_resource/resource'
|
3
|
+
|
4
|
+
module ChefResource
|
5
|
+
module Resource
|
6
|
+
#
|
7
|
+
# A Resource with property_types and named getter/setters.
|
8
|
+
#
|
9
|
+
# The corresponding Type is
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# class Address
|
13
|
+
# include ChefResource::Resource::StructResource
|
14
|
+
# extend ChefResource::Resource::StructResourceType
|
15
|
+
# property :street
|
16
|
+
# property :city
|
17
|
+
# property :state
|
18
|
+
# end
|
19
|
+
# class Person
|
20
|
+
# include ChefResource::Resource::StructResource
|
21
|
+
# extend ChefResource::Resource::StructResourceType
|
22
|
+
# property :name
|
23
|
+
# property :home_address, Address
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# p = Person.open
|
27
|
+
# a = Address.open
|
28
|
+
# p.home_address = a # Sets p.updates[:home_address] = P::HomeAddress.open(p.address)
|
29
|
+
# p.home_address.city = 'Malarky' # p.address.updates[:city] = 'Malarky'
|
30
|
+
# p.update
|
31
|
+
# # first does p.home_address.update
|
32
|
+
# # -> sets p.home_address.current_resource.city -> a.city = 'Malarky'
|
33
|
+
# # sets p.current_resource.home_address = p.home_address.current_resource
|
34
|
+
#
|
35
|
+
module StructResource
|
36
|
+
include Resource
|
37
|
+
|
38
|
+
#
|
39
|
+
# Resource read/modify interface: reopen, identity, explicit_property_values
|
40
|
+
#
|
41
|
+
|
42
|
+
#
|
43
|
+
# Get a new copy of the Resource with only identity values set.
|
44
|
+
#
|
45
|
+
# Note: the Resource remains in :created state, not :identity_defined as
|
46
|
+
# one would get from `open`. Call resource_identity_defined if you want
|
47
|
+
# to be able to retrieve actual values.
|
48
|
+
#
|
49
|
+
# This method is used by ResourceType.get() and Resource.reload.
|
50
|
+
#
|
51
|
+
def reopen_resource
|
52
|
+
# Create a new Resource of our same type, with just identity values.
|
53
|
+
resource = self.class.new
|
54
|
+
explicit_property_values.each do |name,value|
|
55
|
+
resource.explicit_property_values[name] = value if self.class.property_types[name].identity?
|
56
|
+
end
|
57
|
+
resource
|
58
|
+
end
|
59
|
+
|
60
|
+
#
|
61
|
+
# Get the identity string of the resource.
|
62
|
+
#
|
63
|
+
def resource_identity_string
|
64
|
+
positionals = []
|
65
|
+
named = {}
|
66
|
+
self.class.property_types.each do |name, type|
|
67
|
+
next if !explicit_property_values.has_key?(name)
|
68
|
+
if type.identity?
|
69
|
+
value = public_send(name)
|
70
|
+
if type.required?
|
71
|
+
positionals << value
|
72
|
+
else
|
73
|
+
named[name] = value
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
if named.empty?
|
78
|
+
if positionals.empty?
|
79
|
+
return ""
|
80
|
+
elsif positionals.size == 1
|
81
|
+
return positionals[0].to_s
|
82
|
+
end
|
83
|
+
end
|
84
|
+
(positionals.map { |value| value.inspect } +
|
85
|
+
named.map { |name,value| "#{name}: #{value.inspect}" }).join(",")
|
86
|
+
end
|
87
|
+
|
88
|
+
#
|
89
|
+
# Tell whether a particular attribute is set.
|
90
|
+
#
|
91
|
+
# @param name [Symbol] The name of the attribute
|
92
|
+
# @return [Boolean] Whether the attribute is set
|
93
|
+
#
|
94
|
+
def is_set?(name)
|
95
|
+
explicit_property_values.has_key?(name)
|
96
|
+
end
|
97
|
+
|
98
|
+
#
|
99
|
+
# Define the identity of this struct, based on the given arguments and
|
100
|
+
# block. After this method, the identity is frozen.
|
101
|
+
#
|
102
|
+
# @param *args The arguments. Generally the user passed you these from
|
103
|
+
# some other function, and you are trusting the struct to do the right
|
104
|
+
# thing with them.
|
105
|
+
# @param &define_identity_block A block that should run after the arguments
|
106
|
+
# are parsed but before the resource identity is frozen.
|
107
|
+
#
|
108
|
+
def define_identity(*args, &define_identity_block)
|
109
|
+
#
|
110
|
+
# Process named arguments - open(..., a: 1, b: 2, c: 3, d: 4)
|
111
|
+
#
|
112
|
+
if args[-1].is_a?(Hash)
|
113
|
+
named_args = args.pop
|
114
|
+
named_args.each do |name, value|
|
115
|
+
type = self.class.property_types[name]
|
116
|
+
raise ArgumentError, "Property #{name} was passed to #{self.class}.define_identity, but does not exist on #{self.class}!" if !type
|
117
|
+
raise ArgumentError, "#{self.class}.open only takes identity properties, and #{name} is not an identity property on #{self.class}!" if !type.identity?
|
118
|
+
public_send(name, value)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
#
|
123
|
+
# Process positional arguments - open(1, 2, 3, ...)
|
124
|
+
#
|
125
|
+
required_identity_properties = self.class.property_types.values.
|
126
|
+
select { |attr| attr.identity? && attr.required? }.
|
127
|
+
map { |attr| attr.property_name }
|
128
|
+
|
129
|
+
if args.size > required_identity_properties.size
|
130
|
+
raise ArgumentError, "Too many arguments to #{self.class}.define_identity! (#{args.size} for #{required_identity_properties.size})!"
|
131
|
+
end
|
132
|
+
required_identity_properties.each_with_index do |name, index|
|
133
|
+
if args.size > index
|
134
|
+
# If the argument was passed positionally (open(a, b, c ...)) set it from that.
|
135
|
+
if named_args && named_args.has_key?(name)
|
136
|
+
raise ArgumentError, "Property #{name} specified twice in #{self}.define_identity! Both as argument ##{index} and as a named argument."
|
137
|
+
end
|
138
|
+
public_send(name, args[index])
|
139
|
+
else
|
140
|
+
# If the argument wasn't passed positionally, check whether it was passed in the hash. If not, error.
|
141
|
+
if !named_args || !named_args.has_key?(name)
|
142
|
+
raise ArgumentError, "Required property #{name} not passed to #{self}.define_identity!"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
#
|
148
|
+
# Run the block
|
149
|
+
#
|
150
|
+
instance_eval(&define_identity_block) if define_identity_block
|
151
|
+
|
152
|
+
#
|
153
|
+
# Freeze the identity properties
|
154
|
+
#
|
155
|
+
resource_identity_defined
|
156
|
+
end
|
157
|
+
|
158
|
+
#
|
159
|
+
# Reset changes to this struct (or to a property).
|
160
|
+
#
|
161
|
+
# Reset without parameters never resets identity properties--only normal
|
162
|
+
# properties.
|
163
|
+
#
|
164
|
+
# @param name Reset the property named `name`. If not passed, resets
|
165
|
+
# all properties.
|
166
|
+
# @raise PropertyDefinedError if the named property being referenced is
|
167
|
+
# defined (i.e. we are in identity_defined or fully_defined state).
|
168
|
+
# @raise ResourceStateError if we are in fully_defined state.
|
169
|
+
#
|
170
|
+
def reset(name=nil)
|
171
|
+
if name
|
172
|
+
property_type = self.class.property_types[name]
|
173
|
+
if !property_type
|
174
|
+
raise ArgumentError, "#{self.class} does not have property #{name}, cannot reset!"
|
175
|
+
end
|
176
|
+
if property_type.identity?
|
177
|
+
if resource_state != :created
|
178
|
+
raise PropertyDefinedError.new("Identity property #{self.class}.#{name} cannot be reset after open() or get() has been called (after the identity has been fully defined). Current sate: #{resource_state}", self, property_type)
|
179
|
+
end
|
180
|
+
else
|
181
|
+
if ![:created, :identity_defined].include?(resource_state)
|
182
|
+
raise PropertyDefinedError.new("Property #{self.class}.#{name} cannot be reset after the resource is fully defined.", self, property_type)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
explicit_property_values.delete(name)
|
187
|
+
else
|
188
|
+
# We only ever reset non-identity values
|
189
|
+
if ![:created, :identity_defined].include?(resource_state)
|
190
|
+
raise ResourceStateError.new("#{self.class} cannot be reset after it is fully defined", self)
|
191
|
+
end
|
192
|
+
explicit_property_values.keep_if { |name,value| self.class.property_types[name].identity? }
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
#
|
197
|
+
# A hash of the changes the user has made to keys
|
198
|
+
#
|
199
|
+
def explicit_property_values
|
200
|
+
@explicit_property_values ||= {}
|
201
|
+
end
|
202
|
+
|
203
|
+
#
|
204
|
+
# Take an action to update the real resource, as long as the given keys have
|
205
|
+
# actually changed from their real values. Their real values are obtained
|
206
|
+
# via `load` and `load_value`.
|
207
|
+
#
|
208
|
+
# @param *names [Symbol] A list of property names which must be different
|
209
|
+
# from their actual / default value in order to set them. If the last parameter is a String, it
|
210
|
+
# is treated as the description of the update.
|
211
|
+
# @yield [new_values] a Set containing the list of keys whose values have
|
212
|
+
# changed. This block is run in the context of the Resource. Its
|
213
|
+
# return value is ignored.
|
214
|
+
# @return the list of changes, or nil if there are no changes
|
215
|
+
#
|
216
|
+
def converge(*names, &update_block)
|
217
|
+
#
|
218
|
+
# Grab the user's description from the last parameter, if it was passed
|
219
|
+
#
|
220
|
+
if names[-1].is_a?(String)
|
221
|
+
*names, description = *names if names[-1].is_a?(String)
|
222
|
+
end
|
223
|
+
|
224
|
+
#
|
225
|
+
# Decide on the header, and fix up the list of names to include all names
|
226
|
+
# if the user didn't pass any names
|
227
|
+
#
|
228
|
+
if names.empty?
|
229
|
+
change_header = ""
|
230
|
+
names = self.class.property_types.keys if names.empty?
|
231
|
+
else
|
232
|
+
change_header = "#{ChefResource.english_list(*names)}"
|
233
|
+
end
|
234
|
+
|
235
|
+
#
|
236
|
+
# Figure out if anything changed
|
237
|
+
#
|
238
|
+
exists = resource_exists?
|
239
|
+
changed_names = names.inject({}) do |h, name|
|
240
|
+
if explicit_property_values.has_key?(name)
|
241
|
+
type = self.class.property_types[name]
|
242
|
+
|
243
|
+
desired_value = public_send(name)
|
244
|
+
if exists
|
245
|
+
current_value = type.current_property_value(self)
|
246
|
+
if desired_value != current_value
|
247
|
+
h[name] = [ type.value_to_s(desired_value), type.value_to_s(current_value) ]
|
248
|
+
end
|
249
|
+
else
|
250
|
+
h[name] = [ type.value_to_s(desired_value), nil ]
|
251
|
+
end
|
252
|
+
end
|
253
|
+
h
|
254
|
+
end
|
255
|
+
|
256
|
+
#
|
257
|
+
# Skip the action if nothing was changed
|
258
|
+
#
|
259
|
+
if exists
|
260
|
+
if changed_names.empty?
|
261
|
+
skip_action "skipping #{change_header}: no values changed"
|
262
|
+
return nil
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
#
|
267
|
+
# Figure out the printout for what's changing:
|
268
|
+
#
|
269
|
+
# update file[x.txt]
|
270
|
+
# set abc to blah
|
271
|
+
# set abcdef to 12
|
272
|
+
# set a to nil
|
273
|
+
#
|
274
|
+
description ||= exists ? "update #{change_header}" : "create #{change_header}"
|
275
|
+
name_width = changed_names.keys.map { |name| name.size }.max
|
276
|
+
description_lines = [ description ] +
|
277
|
+
changed_names.map do |name, (desired, current)|
|
278
|
+
" set #{name.to_s.ljust(name_width)} to #{desired}#{current ? " (was #{current})" : ""}"
|
279
|
+
end
|
280
|
+
|
281
|
+
#
|
282
|
+
# Actually take the action
|
283
|
+
#
|
284
|
+
take_action(description_lines, &update_block)
|
285
|
+
|
286
|
+
changed_names
|
287
|
+
end
|
288
|
+
|
289
|
+
#
|
290
|
+
# Hash-like interface: to_h, to_hash, as_json, to_json, ==, [], []=
|
291
|
+
#
|
292
|
+
|
293
|
+
#
|
294
|
+
# Returns this struct as a hash, including all properties and their defaults.
|
295
|
+
#
|
296
|
+
# @param only [Symbol] Which values to include. Default: `:only_known`. One of:
|
297
|
+
# - :only_known :: Values explicitly set by the user or current values.
|
298
|
+
# If the current value has not been loaded, this will NOT load it or
|
299
|
+
# show any of those values.
|
300
|
+
# - :only_changed :: Values which the user has set and which have
|
301
|
+
# actually changed from their current or default value.
|
302
|
+
# - :only_explicit :: Values explicitly set by the user.
|
303
|
+
# - :all :: All values, including default values.
|
304
|
+
#
|
305
|
+
def to_h(only=:only_known)
|
306
|
+
case only
|
307
|
+
when :only_changed
|
308
|
+
result = {}
|
309
|
+
explicit_property_values.each do |name, value|
|
310
|
+
current_property_value = self.class.property_types[name].current_property_value(self)
|
311
|
+
if value != current_property_value
|
312
|
+
result[name] = value
|
313
|
+
end
|
314
|
+
end
|
315
|
+
result
|
316
|
+
|
317
|
+
when :only_explicit
|
318
|
+
explicit_property_values.dup
|
319
|
+
|
320
|
+
when :all
|
321
|
+
result = {}
|
322
|
+
self.class.property_types.each_key do |name|
|
323
|
+
result[name] = public_send(name)
|
324
|
+
end
|
325
|
+
result
|
326
|
+
|
327
|
+
else
|
328
|
+
if current_resource_loaded?
|
329
|
+
current_resource.explicit_property_values.merge(explicit_property_values)
|
330
|
+
else
|
331
|
+
explicit_property_values.dup
|
332
|
+
end
|
333
|
+
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
#alias :to_hash :to_h
|
338
|
+
|
339
|
+
#
|
340
|
+
# as_json does most of the to_json heavy lifted. It exists here in case activesupport
|
341
|
+
# is loaded. activesupport will call as_json and skip over to_json. This ensure
|
342
|
+
# json is encoded as expected
|
343
|
+
#
|
344
|
+
# @param only_changed Returns only values which have actually changed from
|
345
|
+
# their current or default value.
|
346
|
+
# @param only_explicit Returns only values which have been explicitly set
|
347
|
+
# by the user.
|
348
|
+
#
|
349
|
+
def as_json(only_changed: false, only_explicit: false, **options)
|
350
|
+
to_h(only_changed: false, only_explicit: false)
|
351
|
+
end
|
352
|
+
|
353
|
+
#
|
354
|
+
# Serialize this object as a hash
|
355
|
+
#
|
356
|
+
# @param only_changed Returns only values which have actually changed from
|
357
|
+
# their current or default value.
|
358
|
+
# @param only_explicit Returns only values which have been explicitly set
|
359
|
+
# by the user.
|
360
|
+
#
|
361
|
+
def to_json(only_changed: false, only_explicit: false, **options)
|
362
|
+
results = as_json(only_changed: only_changed, only_explicit: only_explicit)
|
363
|
+
Chef::JSONCompat.to_json(results, **options)
|
364
|
+
end
|
365
|
+
|
366
|
+
#
|
367
|
+
# Returns true if these are the same type and their values are the same.
|
368
|
+
# Avoids comparing things that aren't modified in either struct.
|
369
|
+
#
|
370
|
+
def ==(other)
|
371
|
+
return false if !other.is_a?(self.class)
|
372
|
+
|
373
|
+
# Try to rule out differences via explicit_property_values first (this should
|
374
|
+
# handle any identity keys and prevent us from accidentally pulling on
|
375
|
+
# current_resource).
|
376
|
+
(explicit_property_values.keys & other.explicit_property_values.keys).each do |name|
|
377
|
+
return false if public_send(name) != other.public_send(name)
|
378
|
+
end
|
379
|
+
|
380
|
+
# If one struct has more desired (set) values than the other, compare
|
381
|
+
# the values to the current/default on the other.
|
382
|
+
(explicit_property_values.keys - other.explicit_property_values.keys).each do |name|
|
383
|
+
return false if public_send(name) != other.public_send(name)
|
384
|
+
end
|
385
|
+
(other.explicit_property_values.keys - explicit_property_values.keys).each do |attr|
|
386
|
+
return false if public_send(name) != other.public_send(name)
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
#
|
391
|
+
# Get the value of the given property from the struct
|
392
|
+
#
|
393
|
+
def [](name)
|
394
|
+
name = name.to_sym
|
395
|
+
if !property_types.has_key?(name)
|
396
|
+
raise ArgumentError, "#{name} is not a property of #{self.class}."
|
397
|
+
end
|
398
|
+
|
399
|
+
public_send(name)
|
400
|
+
end
|
401
|
+
|
402
|
+
#
|
403
|
+
# Set the value of the given property in the struct
|
404
|
+
#
|
405
|
+
def []=(name, value)
|
406
|
+
public_send(name.to_sym, value)
|
407
|
+
end
|
408
|
+
end
|
409
|
+
end
|
410
|
+
end
|
@@ -0,0 +1,275 @@
|
|
1
|
+
require 'chef_resource/errors'
|
2
|
+
require 'chef_resource/resource/resource_type'
|
3
|
+
require 'chef_resource/constants'
|
4
|
+
require 'chef_resource/resource/struct_property_type'
|
5
|
+
require 'chef_resource/camel_case'
|
6
|
+
require 'chef_resource/simple_struct'
|
7
|
+
|
8
|
+
module ChefResource
|
9
|
+
module Resource
|
10
|
+
#
|
11
|
+
# The Type for a StructResource.
|
12
|
+
#
|
13
|
+
module StructResourceType
|
14
|
+
include ResourceType
|
15
|
+
|
16
|
+
#
|
17
|
+
# Coerce the input into a struct of this type.
|
18
|
+
#
|
19
|
+
# Constructor form: required identity parameters first, and then non-required properties in a hash.
|
20
|
+
# - MyStruct.coerce(identity_attr, identity_attr2, ..., { attr1: value, attr2: value, ... }) -> open(identity1, identity2, ... { identity properties }), and set non-identity properties afterwards
|
21
|
+
#
|
22
|
+
# Hash form: a hash with names and values representing struct names and values.
|
23
|
+
# - MyStruct.coerce({ identity_attr: value, attr1: value, attr2: value, ... }) -> open({ identity properties }), and set non-identity properties afterwards
|
24
|
+
#
|
25
|
+
# nil:
|
26
|
+
# - MyStruct.coerce(nil) -> nil
|
27
|
+
#
|
28
|
+
# Resource of this type:
|
29
|
+
# - MyStruct.coerce(x = MyStruct.open) -> x
|
30
|
+
#
|
31
|
+
# Simple constructor form: identity parameters
|
32
|
+
# - MyStruct.coerce(identity_attr) -> open(identity_attr)
|
33
|
+
# - MyStruct.coerce(identity_attr, identity_attr2, ...) -> open(identity_attr, identity_attr2, ...)
|
34
|
+
# - MyStruct.coerce() -> open()
|
35
|
+
#
|
36
|
+
# Struct Form:
|
37
|
+
# - MyStruct.coerce(other_my_struct_instance)
|
38
|
+
#
|
39
|
+
def coerce(parent, *args)
|
40
|
+
if args[-1].is_a?(Hash)
|
41
|
+
#
|
42
|
+
# Constructor form: required identity parameters first, and then non-required properties in a hash.
|
43
|
+
# - MyStruct.coerce(identity_attr, identity_attr2, ..., { attr1: value, attr2: value, ... }) -> open(identity1, identity2, ... { identity properties }), and set non-identity properties afterwards
|
44
|
+
#
|
45
|
+
# Hash form: a hash with names and values representing struct names and values.
|
46
|
+
# - MyStruct.coerce({ identity_attr: value, attr1: value, attr2: value, ... }) -> open({ identity properties }), and set non-identity properties afterwards
|
47
|
+
#
|
48
|
+
|
49
|
+
# Split the identity properties from normal so we can call open() with
|
50
|
+
# just identity properties
|
51
|
+
explicit_property_values = args[-1]
|
52
|
+
identity_values = {}
|
53
|
+
explicit_property_values.each_key do |name|
|
54
|
+
type = property_types[name]
|
55
|
+
raise ValidationError, "#{self.class}.coerce was passed property #{name}, but #{name} is not a property on #{self.class}." if !type
|
56
|
+
identity_values[name] = explicit_property_values.delete(name) if type.identity?
|
57
|
+
end
|
58
|
+
|
59
|
+
# open the resource
|
60
|
+
resource = open(*args[0..-2], identity_values)
|
61
|
+
|
62
|
+
# Set the non-identity properties before returning
|
63
|
+
explicit_property_values.each do |name, value|
|
64
|
+
resource.public_send(name, value)
|
65
|
+
end
|
66
|
+
|
67
|
+
resource.resource_fully_defined
|
68
|
+
|
69
|
+
super(parent, resource)
|
70
|
+
|
71
|
+
elsif args.size == 1 && is_valid?(parent, args[0])
|
72
|
+
# nil:
|
73
|
+
# - MyStruct.coerce(nil) -> nil
|
74
|
+
#
|
75
|
+
# Resource of this type:
|
76
|
+
# - MyStruct.coerce(x = MyStruct.open) -> x
|
77
|
+
super(parent, args[0])
|
78
|
+
|
79
|
+
else
|
80
|
+
# Simple constructor form: identity parameters
|
81
|
+
# - MyStruct.coerce(identity_attr) -> open(identity_attr)
|
82
|
+
# - MyStruct.coerce(identity_attr, identity_attr2, ...) -> open(identity_attr, identity_attr2, ...)
|
83
|
+
# - MyStruct.coerce() -> open()
|
84
|
+
super(parent, open(*args))
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
#
|
90
|
+
# Struct.open() takes the identity properties of the struct and opens it up.
|
91
|
+
# Supports these forms:
|
92
|
+
#
|
93
|
+
# - open(identity1, identity2[, { identity3: value, identity4: value } ])
|
94
|
+
# - open({ identity1: value, identity2: value, identity3: value, identity4: value })
|
95
|
+
# - open() (if no identity properties)
|
96
|
+
#
|
97
|
+
#
|
98
|
+
# @example
|
99
|
+
# class MyStruct
|
100
|
+
# include ChefResource::Resource::StructResource
|
101
|
+
# extend ChefResource::Resource::StructResourceType
|
102
|
+
# property :x, identity: true
|
103
|
+
# property :y, identity: true
|
104
|
+
# end
|
105
|
+
#
|
106
|
+
# # Allows these statements to work:
|
107
|
+
# s = MyStruct.open(1, 2)
|
108
|
+
# puts s.x # 1
|
109
|
+
# puts s.y # 2
|
110
|
+
# s = MyStruct.open(x: 3, y: 4)
|
111
|
+
# puts s.x # 3
|
112
|
+
# puts s.y # 4
|
113
|
+
#
|
114
|
+
def open(*args, &define_identity_block)
|
115
|
+
resource = new
|
116
|
+
resource.define_identity(*args, &define_identity_block)
|
117
|
+
resource
|
118
|
+
end
|
119
|
+
|
120
|
+
#
|
121
|
+
# Struct definition: MyStruct.property
|
122
|
+
#
|
123
|
+
|
124
|
+
#
|
125
|
+
# Create a property on this struct.
|
126
|
+
#
|
127
|
+
# Makes three method calls available to the struct:
|
128
|
+
# - `struct.name` - Get the value of `name`.
|
129
|
+
# - `struct.name <value...>` - Set `name`.
|
130
|
+
# - `struct.name = <value>` - Set `name`.
|
131
|
+
#
|
132
|
+
# If the property is marked as an identity property, it also modifies
|
133
|
+
# `Struct.open()` to take it as a named parameter. Multiple identity
|
134
|
+
# property_types means multiple parameters to `open()`.
|
135
|
+
#
|
136
|
+
# @param name [String] The name of the property.
|
137
|
+
# @param type [Class] The type of the property. If passed, the property
|
138
|
+
# will use `type.open()`
|
139
|
+
# @param identity [Boolean] `true` if this is an identity
|
140
|
+
# property. Default: `false`
|
141
|
+
# @param required [Boolean] `true` if this is a required parameter.
|
142
|
+
# Defaults to `true`. Non-identity property_types do not support `required`
|
143
|
+
# and will ignore it. Non-required identity property_types will not be
|
144
|
+
# available as positioned arguments in ResourceClass.open(); they can
|
145
|
+
# only be specified by name (ResourceClass.open(x: 1))
|
146
|
+
# @param default [Object] The value to return if the user asks for the property
|
147
|
+
# when it has not been set. `nil` is a valid value for this.
|
148
|
+
# @param default [Proc] An optional block that will be called when
|
149
|
+
# the user asks for a value that has not been set. Called in the
|
150
|
+
# context of the struct (instance_eval), so you can access other
|
151
|
+
# properties of the struct to compute the value. Value is *not* cached,
|
152
|
+
# but rather is called every time.
|
153
|
+
#
|
154
|
+
# @example Property referencing a resource type by "snake case name"
|
155
|
+
# class MyResource < StructResourceBase
|
156
|
+
# property :blah, :my_resource
|
157
|
+
# end
|
158
|
+
# @example Typeless, optionless property.
|
159
|
+
# class MyResource < StructResourceBase
|
160
|
+
# property :simple
|
161
|
+
# end
|
162
|
+
# x = MyResource.open
|
163
|
+
# puts x.simple # nil
|
164
|
+
# x.simple = 10
|
165
|
+
# puts x.simple # 10
|
166
|
+
#
|
167
|
+
# @example Property with default
|
168
|
+
# class MyResource < StructResourceBase
|
169
|
+
# property :b, default: 10
|
170
|
+
# end
|
171
|
+
# x = MyResource.open
|
172
|
+
# puts x.b # 10
|
173
|
+
#
|
174
|
+
# @example Property with default block
|
175
|
+
# class MyResource < StructResourceBase
|
176
|
+
# property :a, default: 3
|
177
|
+
# property :b do
|
178
|
+
# a * 2
|
179
|
+
# end
|
180
|
+
# end
|
181
|
+
# x = MyResource.open
|
182
|
+
# puts x.b # 6
|
183
|
+
# x.a = 10
|
184
|
+
# puts x.b # 20
|
185
|
+
#
|
186
|
+
# @example Property with identity
|
187
|
+
# class MyResource < StructResourceBase
|
188
|
+
# property :a, identity: true
|
189
|
+
# end
|
190
|
+
# x = MyResource.new(10)
|
191
|
+
# puts x.a # 10
|
192
|
+
#
|
193
|
+
# @example Property with multiple identity
|
194
|
+
# class MyResource < StructResourceBase
|
195
|
+
# property :a, identity: true
|
196
|
+
# property :b, identity: true
|
197
|
+
# end
|
198
|
+
# x = MyResource.open(10, 20)
|
199
|
+
# puts x.a # 10
|
200
|
+
# puts x.b # 20
|
201
|
+
# x = MyResource.open(b: 2, a: 1)
|
202
|
+
# puts x.a # 1
|
203
|
+
# puts x.b # 2
|
204
|
+
# x = MyResource.open
|
205
|
+
# puts x.a # nil
|
206
|
+
# puts x.b # nil
|
207
|
+
# x = MyResource.open(1)
|
208
|
+
# puts x.a # 1
|
209
|
+
# puts x.b # nil
|
210
|
+
#
|
211
|
+
# @example Property with non-required identity
|
212
|
+
# class MyResource < StructResourceBase
|
213
|
+
# property :a, identity: true, required: false
|
214
|
+
# property :b, identity: true
|
215
|
+
# end
|
216
|
+
# x = MyResource.open(1)
|
217
|
+
# x.a # nil
|
218
|
+
# x.b # 1
|
219
|
+
#
|
220
|
+
# @example Property with struct typed property
|
221
|
+
# class Address < StructResourceBase
|
222
|
+
# property :street
|
223
|
+
# property :city
|
224
|
+
# property :state
|
225
|
+
# property :zip
|
226
|
+
# end
|
227
|
+
# class Person < StructResourceBase
|
228
|
+
# property :name
|
229
|
+
# property :home_address, Address
|
230
|
+
# end
|
231
|
+
# p = Person.open
|
232
|
+
# p.home_address = Address.open
|
233
|
+
#
|
234
|
+
def property(name, type=nil, identity: nil, default: NOT_PASSED, required: NOT_PASSED, load_value: NOT_PASSED, **type_properties, &override_block)
|
235
|
+
parent = self
|
236
|
+
name = name.to_sym
|
237
|
+
result = self.type(name, type, **type_properties) do
|
238
|
+
extend StructPropertyType
|
239
|
+
self.property_parent_type = parent
|
240
|
+
self.property_name name
|
241
|
+
self.identity identity
|
242
|
+
self.default default unless default == NOT_PASSED
|
243
|
+
self.required required unless required == NOT_PASSED
|
244
|
+
self.load_value load_value unless load_value == NOT_PASSED
|
245
|
+
instance_eval(&override_block) if override_block
|
246
|
+
end
|
247
|
+
property_types[result.property_name] = result
|
248
|
+
result.emit_property_methods
|
249
|
+
result
|
250
|
+
end
|
251
|
+
|
252
|
+
extend SimpleStruct
|
253
|
+
|
254
|
+
#
|
255
|
+
# The property type for each property.
|
256
|
+
#
|
257
|
+
# TODO use real merging in the future. This carries
|
258
|
+
# danger that someone could modify types on the parent.
|
259
|
+
# But it at least gets us basic inheritance for the
|
260
|
+
# normal case where people are adding new properties
|
261
|
+
# rather than overriding old ones.
|
262
|
+
#
|
263
|
+
property :property_types,
|
264
|
+
default: "@property_types = {}",
|
265
|
+
inherited: "@property_types = superclass.property_types.dup"
|
266
|
+
|
267
|
+
#
|
268
|
+
# The list of identity property types (property types with identity=true), in order.
|
269
|
+
#
|
270
|
+
def identity_property_types
|
271
|
+
property_types.values.select { |attr| attr.identity? }
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|