chef-resource 0.2.2

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