chef-resource 0.2.2

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 (55) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +23 -0
  3. data/LICENSE +201 -0
  4. data/README.md +264 -0
  5. data/Rakefile +8 -0
  6. data/files/lib/chef_resource.rb +24 -0
  7. data/files/lib/chef_resource/camel_case.rb +23 -0
  8. data/files/lib/chef_resource/chef.rb +102 -0
  9. data/files/lib/chef_resource/chef_dsl/chef_cookbook_compiler.rb +44 -0
  10. data/files/lib/chef_resource/chef_dsl/chef_recipe.rb +10 -0
  11. data/files/lib/chef_resource/chef_dsl/chef_recipe_dsl_extensions.rb +84 -0
  12. data/files/lib/chef_resource/chef_dsl/chef_resource_base.rb +12 -0
  13. data/files/lib/chef_resource/chef_dsl/chef_resource_class_extensions.rb +30 -0
  14. data/files/lib/chef_resource/chef_dsl/chef_resource_extensions.rb +224 -0
  15. data/files/lib/chef_resource/chef_dsl/chef_resource_log.rb +54 -0
  16. data/files/lib/chef_resource/chef_dsl/resource_container_module.rb +80 -0
  17. data/files/lib/chef_resource/chef_dsl/resource_definition_dsl.rb +128 -0
  18. data/files/lib/chef_resource/constants.rb +8 -0
  19. data/files/lib/chef_resource/errors.rb +31 -0
  20. data/files/lib/chef_resource/lazy_proc.rb +82 -0
  21. data/files/lib/chef_resource/output/nested_converge.rb +91 -0
  22. data/files/lib/chef_resource/output/nested_converge/open_resource.rb +113 -0
  23. data/files/lib/chef_resource/output/region_stream.rb +145 -0
  24. data/files/lib/chef_resource/output/simple_output.rb +83 -0
  25. data/files/lib/chef_resource/resource.rb +428 -0
  26. data/files/lib/chef_resource/resource/resource_log.rb +197 -0
  27. data/files/lib/chef_resource/resource/resource_type.rb +74 -0
  28. data/files/lib/chef_resource/resource/struct_property.rb +39 -0
  29. data/files/lib/chef_resource/resource/struct_property_type.rb +185 -0
  30. data/files/lib/chef_resource/resource/struct_resource.rb +410 -0
  31. data/files/lib/chef_resource/resource/struct_resource_base.rb +11 -0
  32. data/files/lib/chef_resource/resource/struct_resource_type.rb +275 -0
  33. data/files/lib/chef_resource/simple_struct.rb +121 -0
  34. data/files/lib/chef_resource/type.rb +371 -0
  35. data/files/lib/chef_resource/types.rb +4 -0
  36. data/files/lib/chef_resource/types/boolean.rb +16 -0
  37. data/files/lib/chef_resource/types/byte_size.rb +10 -0
  38. data/files/lib/chef_resource/types/date_time_type.rb +18 -0
  39. data/files/lib/chef_resource/types/date_type.rb +18 -0
  40. data/files/lib/chef_resource/types/float_type.rb +28 -0
  41. data/files/lib/chef_resource/types/integer_type.rb +53 -0
  42. data/files/lib/chef_resource/types/interval.rb +21 -0
  43. data/files/lib/chef_resource/types/path.rb +39 -0
  44. data/files/lib/chef_resource/types/pathname_type.rb +34 -0
  45. data/files/lib/chef_resource/types/string_type.rb +16 -0
  46. data/files/lib/chef_resource/types/symbol_type.rb +18 -0
  47. data/files/lib/chef_resource/types/uri_type.rb +37 -0
  48. data/files/lib/chef_resource/version.rb +3 -0
  49. data/spec/integration/chef.rb +81 -0
  50. data/spec/integration/struct_spec.rb +611 -0
  51. data/spec/integration/struct_state_spec.rb +538 -0
  52. data/spec/integration/type_spec.rb +1123 -0
  53. data/spec/integration/validation_spec.rb +207 -0
  54. data/spec/support/spec_support.rb +7 -0
  55. 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,11 @@
1
+ require 'chef_resource/resource/struct_resource'
2
+ require 'chef_resource/resource/struct_resource_type'
3
+
4
+ module ChefResource
5
+ module Resource
6
+ class StructResourceBase
7
+ include StructResource
8
+ extend StructResourceType
9
+ end
10
+ end
11
+ 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