cushion_defaults 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: bacbeab26c1d40ab130cc3fb66b06a7e0eb26012
4
- data.tar.gz: b056aa659f1279580225fa627482894ac8ac5e90
3
+ metadata.gz: 8b3c6fc2391e0197aea23df6eb08eaa1bbf91946
4
+ data.tar.gz: 995a6c5ad6d1caea8f5f2cbcc49f99bf3f87a15c
5
5
  SHA512:
6
- metadata.gz: 3d427f699e5ffb42d22c5e4cc7c2ae9817df5edeb347e984b0fff367924100defc1c8e30582f7ebcdead9b86006cd44a6bd5556357f096610335a80f25b4ed53
7
- data.tar.gz: 4dca3355fac1a733c93d85511df0d943548774e65bc590300656b4d8cb141fa54f09282f180837924b83d8f3908488e314ff300a02272b7d83398e05260e3f07
6
+ metadata.gz: 4c97a480b173e4a9b3dd1b11756eb502d84d1ba26fd15368323ca147eaec2b61d1c97c6a0d893c566938d725af94f56fa9cc3dc94a1d5564b726457fb86a3dfe
7
+ data.tar.gz: cc154a333022212a4cf9a4f628fe81c97fe4fce4c00dc816024f1b2762ed9683f8265783bb7985999a6f6f5174903dbac6e59a8a68fecc2cf1bf6e837feadfbb
data/CHANGELOG.md CHANGED
@@ -36,4 +36,13 @@
36
36
  - IMPROVEMENT: Testing
37
37
  - `test/` renamed to `spec/`
38
38
  - Add `.rspec` and `spec/spec_helper.rb`
39
- - Improve documentation further.
39
+ - Improve documentation further.
40
+
41
+ ## 0.3.x
42
+
43
+ - 0.3.0 - NEW FEATURE: Freezing and thawing defaults.
44
+ - You may wish to prevent a default from further modification, either permanently or temporarily. This can prevent silly mistakes that are otherwise difficult to track down. CushionDefaults now makes this possible via a freezing and thawing API.
45
+ - For more information, see especially:
46
+ - ClassMethods#freeze_default
47
+ - ClassMethods#deep_freeze_default
48
+ - ClassMethods#thaw_default
data/README.md CHANGED
@@ -11,6 +11,10 @@ Allows you to specify a "cushion" for various instance variables—effectively a
11
11
 
12
12
  If, for example, the default value for `@wheels` in class Car is 4 and we set up a new instance called `ford` (`ford = Car.new`), then calling `ford.wheels` will return 4—even though no instance variable `@wheels` has been directly specified for `ford`. And if we later change the default value of `wheels` for Car (`Car.defaults[:wheels] = 6`), then all subsequent calls to `ford.wheels` will return 6 (unless we crystallize it beforehand—see below for more details).
13
13
 
14
+ ### The Latest
15
+
16
+ I try to keep all of this up-to-date, but the latest information can always be found in CHANGELOG.md.
17
+
14
18
  ## Why Should I Care?
15
19
 
16
20
  1. Don't Repeat Yourself (DRY): Gather your defaults in one place, and specify them only once.
@@ -80,32 +84,8 @@ As soon as we do this, all Plants that do not have a color explicitly assigned w
80
84
  rhododendron.color # 'brown'
81
85
  ```
82
86
 
83
- ### Crystallizing Defaults
84
- You can prevent this auto-updating, if desired, by calling `#crystallize_default` on those instances you don't want auto-updated. `#crystallize_default(sym)` effectively says "If no value for `@sym` is explicitly set, then explictly set it to the default value." Obviously, then, `#crystallize_default(sym)` affects only those instances that do not have a value explicitly specified for `sym`.
85
-
86
- ```ruby
87
- tulip, rose = Plant.new, Plant.new
88
- tulip.color # 'brown'
89
- rose.color = 'red'
90
- tulip.has_specified?(:color) # false
91
- rose.has_specified?(:color) # true
92
-
93
- # crystallizes :color to 'brown'
94
- tulip.crystallize_default(:color)
95
-
96
- # has no effect, since :color is already set to 'red'
97
- rose.crystallize_default(:color)
98
-
99
- tulip.has_specified?(:color) # true
100
-
101
- Plant.defaults[:color] = 'green'
102
-
103
- tulip.color # 'brown'
104
- rose.color # 'red'
105
- Plant.new.color # 'green'
106
- ```
107
-
108
87
  ### Defaults and Inheritance
88
+
109
89
  Classes inherit the defaults of those ancestors that respond to `#defaults` with a Hash or a descendent thereof.
110
90
 
111
91
  This all takes place automatically. When CushionDefaults is included in a class, it automatically includes itself in all classes that subclass that class, and when a `cushion_reader` is called, it automatically moves up the class hierarchy if no value for the key is specified in the instance variable or in the current class.
@@ -151,6 +131,7 @@ z.second # 'totally new value'
151
131
  ```
152
132
 
153
133
  ### Adding and Removing Readers and Writers
134
+
154
135
  Now, if we were to later add a new default to Plant
155
136
 
156
137
  ```ruby
@@ -225,12 +206,71 @@ update_readers: true
225
206
  update_writers: true
226
207
  ```
227
208
 
209
+ ### Freezing and Thawing Defaults
210
+
211
+ You may wish to prevent a default from further modification, either permanently or temporarily. This can prevent silly mistakes that are otherwise difficult to track down. CushionDefaults makes this possible via a freezing and thawing API. The key methods here are `#freeze_default` and `#thaw_default`.
212
+
213
+ ```ruby
214
+ class BuffaloNY
215
+ include CushionDefaults
216
+ self.defaults = {temperature: -10}
217
+ freeze_default :temperature
218
+ end
219
+
220
+ # Raises CushionDefaults::FrozenDefaultError (< RunTimeError)
221
+ BuffaloNY.defaults[:temperature] = 60
222
+
223
+ # Assuming we caught the above error...
224
+ BuffaloNY.defaults[:temperature] == -10 # true
225
+
226
+ # Come summer, we can thaw the default
227
+ BuffaloNY.thaw_default :temperature
228
+
229
+ #And we can reset it without error
230
+ BuffaloNY.defaults[:temperature] = 60
231
+ ```
232
+
233
+ Frozen defaults can still have their values overridden by child classes.
234
+
235
+ ```ruby
236
+ class NaturalLog
237
+ include CushionDefaults
238
+ self.defaults = {base: Math::E}
239
+ freeze_default :base
240
+ end
241
+
242
+ class UnnaturalLog < NaturalLog; end
243
+
244
+ # Raises CushionDefaults::FrozenDefaultError
245
+ NaturalLog.defaults[:base] = 1i
246
+
247
+ # Works
248
+ UnnaturalLog.defaults[:base] = 1i
249
+ ```
250
+
251
+ Note that frozen defaults can still have their values modified if those values are themselves mutable. To prevent this, we need to use `#deep_freeze`—but this should be done with caution.
252
+
253
+ (In some situations, even this can can fail to "fully" freeze an object. Check out [ice_nine](https://github.com/dkubb/ice_nine) for a fuller solution.)
254
+
255
+ Finally, to freeze or thaw all defaults en masse, the API makes available `#freeze_defaults` and `#thaw_defaults`.
256
+
228
257
  ### Storing Class Defaults in YAML Files
229
258
 
230
259
  By default, CushionDefaults checks for YAML files for each class but does not complain if no YAML files are found. (If you want it to complain, set `config.whiny_yaml` to true.)
231
260
 
232
261
  CushionDefaults looks for these YAML files at `config/cushion_defaults/class_name.yaml`. For class Klass, then, it would expect a config file at `config/cushion_defaults/klass.yaml`. Classes in a namespace are expected to have their YAML files in a folder named after their namespace, e.g. Modjewel::Klass in `config/cushion_defaults/modjewel/klass.yaml`.
233
262
 
263
+ These YAML files are completely unremarkable in form. Note that all defaults should be specified at root (not in `defaults:`, and currently only simple types are processed. For the above Chair class, we could place the defaults in a YAML class file like the following:
264
+
265
+ ```yaml
266
+ # config/cushion_defaults/chair.yaml
267
+ material: 'wood'
268
+ comfort_factor: 5
269
+ number_accomodated: 1
270
+ ```
271
+
272
+ For an example of all of this in action, look at `examples/example3/example3.rb` and its class default files in `examples/example3/config/cushion_defaults/`.
273
+
234
274
  You can specify a different YAML source folder relative to the calling directory (`config/cushion_defaults/` by default) by setting `config.yaml_source_folder`, or you can specify an absolute path to the YAML source folder by setting `config.yaml_source_full_path`.
235
275
 
236
276
  If you ever are bug-hunting and want to see where CushionDefaults expects a YAML file to be located, you can pass the class object to `config.yaml_file_for(klass)`.
@@ -243,31 +283,76 @@ These YAML files are loaded automatically (unless `config.auto_load_from_yaml` h
243
283
 
244
284
  You can use the above techniques to maintain different sets of class defaults for all of your classes. This is especially useful if your application needs to run in different environments or regions. For a more complex (but still simple enough) example, see Example 4 in the examples folder. Following is a trivial example.
245
285
 
286
+ Assume we have four YAML class defaults files as follows:
287
+
288
+ ```yaml
289
+ # config/cushion_defaults/spr/user.yaml
290
+ weather_judgment: 'how nice'
291
+ ```
292
+
293
+ ```yaml
294
+ # config/cushion_defaults/sum/user.yaml
295
+ weather_judgment: 'is it hot in here or is it just me?'
296
+ ```
297
+
298
+ ```yaml
299
+ # config/cushion_defaults/fal/user.yaml
300
+ weather_judgment: "if only it weren\'t for the leaves"
301
+ ```
302
+
303
+ ```yaml
304
+ # config/cushion_defaults/win/user.yaml
305
+ weather_judgment: "baby it\'s cold outside"
306
+ ```
307
+
308
+ Combine this with the following Ruby, and we can get different defaults depending on the current meteorological season.
309
+
246
310
  ```ruby
247
311
  class Season
248
- # Obviously this is only meteorological seasons, and only valid for the Northern hemisphere
312
+ # Obviously this is only valid for the Northern hemisphere
249
313
  attr_accessor :months, :short_code
314
+
250
315
  def initialize(&block)
251
316
  yield(self) if block_given?
252
317
  end
318
+
253
319
  def include?(date)
254
320
  months.include?(date.month)
255
321
  end
322
+
256
323
  def yaml_source_path
257
324
  "config/cushion_defaults/#{short_code}/"
258
325
  end
259
326
  end
327
+
260
328
  seasons = [
261
329
  Season.new {|s| s.months=[3,4,5]; s.short_code='spr'},
262
330
  Season.new {|s| s.months=[6,7,8]; s.short_code='sum'},
263
331
  Season.new {|s| s.months=[9,10,11]; s.short_code='fal'},
264
332
  Season.new {|s| s.months=[12,1,2]; s.short_code='win'}
265
333
  ]
334
+
266
335
  current_season = seasons.select{|s| s.include?(Date.today)}.first
267
- CushionDefaults.configure {|conf| conf.yaml_source_path = current_season.yaml_source_path}
336
+
337
+ # The following will set the root directory for all class defaults, depending on the current season.
338
+ # Possible resulting config paths:
339
+ # - `config/cushion_defaults/spr/`
340
+ # - `config/cushion_defaults/sum/`
341
+ # - `config/cushion_defaults/fal/`
342
+ # - `config/cushion_defaults/win/`
343
+ CushionDefaults.configure do |conf|
344
+ conf.yaml_source_path = current_season.yaml_source_path
345
+ end
346
+
347
+ class User
348
+ # Automatically loads in defaults from the above-selected path
349
+ cushion :weather_judgment
350
+ end
351
+
352
+ # Returns one of the messages from the above YAML files, depending on the current meteorological season
353
+ User.new.weather_judgment
268
354
  ```
269
355
 
270
- The above will set the root directory for all class defaults, depending on the current date, to one of the following: `config/cushion_defaults/spr/`, `config/cushion_defaults/sum/`, `config/cushion_defaults/fal/`, or `config/cushion_defaults/win/`.
271
356
 
272
357
  #### Multiple Defaults for a Single Class
273
358
 
@@ -283,7 +368,35 @@ class Person
283
368
  end
284
369
  ```
285
370
 
286
- In this example, we load (randomly) either `person_en.yaml`, `person_fr.yaml`, or `person_de.yaml`.
371
+ In this example, we load (randomly) either `person_en.yaml`, `person_fr.yaml`, or `person_de.yaml`. For a fuller example along these lines, see `examples/example4/example4.rb`.
372
+
373
+ ### Crystallizing Defaults
374
+
375
+ You can prevent auto-updating of default values, if desired, by calling `#crystallize_default` on those instances you don't want auto-updated. `#crystallize_default(sym)` effectively says "If no value for `@sym` is explicitly set, then explictly set it to the default value." Obviously, then, `#crystallize_default(sym)` affects only those instances that do not have a value explicitly specified for `sym`.
376
+
377
+ ```ruby
378
+ tulip, rose = Plant.new, Plant.new
379
+ tulip.color # 'brown'
380
+ rose.color = 'red'
381
+ tulip.has_specified?(:color) # false
382
+ rose.has_specified?(:color) # true
383
+
384
+ # crystallizes :color to 'brown'
385
+ tulip.crystallize_default(:color)
386
+
387
+ # has no effect, since :color is already set to 'red'
388
+ rose.crystallize_default(:color)
389
+
390
+ tulip.has_specified?(:color) # true
391
+
392
+ Plant.defaults[:color] = 'green'
393
+
394
+ tulip.color # 'brown'
395
+ rose.color # 'red'
396
+ Plant.new.color # 'green'
397
+ ```
398
+
399
+ (Crystallizing defaults should be carefully distinguished from freezing defaults: crystallizing defaults applies to specific instances, whereas freezing defaults applies to the class itself.)
287
400
 
288
401
  ### Pushy and Polite Defaults
289
402
 
@@ -308,21 +421,21 @@ If you need more than CushionDefaults offers right now, you've got a couple diff
308
421
  2. Code a feature. Fork and pull, and I'll fold it in and implement it if it looks solid and generally useful.
309
422
  3. Check out Cascading Configuration. You may want to check out [Cascading Configuration](https://github.com/RidiculousPower/cascading_configuration), which tackles a similar problem but offers a different approach and featureset.
310
423
 
311
- ## Feedback
424
+ ## Well, If You Ask Me...
312
425
 
313
426
  Any feedback is very much appreciated!
314
427
 
315
- ###Bugs
428
+ ### For the Entomologists
316
429
 
317
430
  Run into any bugs or issues? Please report them on the [GitHub issue tracker](https://github.com/posgarou/cushion_defaults/issues).
318
431
 
319
- ###Like It?
432
+ ### Like It?
320
433
 
321
434
  1. Tell a friend.
322
435
  2. Star or [fork](https://github.com/posgarou/cushion_defaults/fork) the [GitHub repository](https://github.com/posgarou/cushion_defaults).
323
436
  3. If you're feeling generous, [offer a tip](https://gratipay.com/posgarou/).
324
437
 
325
- ###Not Sold?
438
+ ### Not Sold?
326
439
 
327
440
  If you have the time, tell me why.
328
441
 
@@ -5,17 +5,6 @@ module CushionDefaults
5
5
  # Reader for the +defaults+ +DefaultsHash+.
6
6
  attr_reader :defaults
7
7
 
8
- # Either set up or wipe @defaults. Should not usually be called directly.
9
- # @api private
10
- def initialize_defaults_hash
11
- if @defaults
12
- # We need to maintain the identity of the hash, as child classes may have stored a reference to it
13
- @defaults.clear
14
- else
15
- @defaults = DefaultsHash.new(self)
16
- end
17
- end
18
-
19
8
  # @!attribute [w] defaults
20
9
  # Wipe @defaults and replace it with the keys/vals of +replacement_hash+.
21
10
  #
@@ -25,9 +14,9 @@ module CushionDefaults
25
14
  def defaults=(replacement_hash)
26
15
  # Need to copy over keys/vals to ensure @defaults remains a DefaultsHash and retains identity
27
16
 
28
- CushionDefaults.log("Old defaults deleted for #{self}:#{@defaults.reduce(''){|s,(k,v)| s+"\n\t\t#{k}: #{v}"}}", :info) unless @defaults.empty?
17
+ CushionDefaults.log("Old defaults deleted for #{self}:#{defaults.empty? ? ' [none]' : @defaults.reduce(''){|s,(k,v)| s+"\n\t\t#{k}: #{v}"}}", :info) unless @defaults.empty?
29
18
  @defaults.replace(replacement_hash)
30
- CushionDefaults.log("New defaults added for #{self}:#{@defaults.reduce(''){|s,(k,v)| s+"\n\t\t#{k}: #{v}"}}", :info)
19
+ CushionDefaults.log("New defaults added for #{self}:#{defaults.empty? ? ' [none]' : @defaults.reduce(''){|s,(k,v)| s+"\n\t\t#{k}: #{v}"}}", :info)
31
20
  @defaults.keys.each do |key|
32
21
  unless key.is_a? Symbol
33
22
  @defaults[key.to_sym] = @defaults[key].delete!
@@ -42,6 +31,8 @@ module CushionDefaults
42
31
  @defaults[key.to_sym] = val
43
32
  end
44
33
 
34
+ # @!group Mass Default Setting Methods
35
+
45
36
  # Load in the defaults for this class from a YAML file.
46
37
  # If +file_name+ is specified, this YAML file is loaded. Otherwise, {Configuration#yaml_file_for} is evaluated for
47
38
  # the current class. By default, the yaml file for +Klass+ is expected to be at +config/cushion_defaults/klass.yaml+.
@@ -62,7 +53,7 @@ module CushionDefaults
62
53
  end
63
54
 
64
55
  initialize_defaults_hash
65
- log_str = "New defaults added for #{self}:"
56
+ log_str = "New defaults added for #{self}:#{' [none]' if yaml.empty?}"
66
57
  # If automatic readers and writers are enabled, this will set them up as a consequence.
67
58
  yaml.each do |key, val|
68
59
  log_str << "\n\t\t#{key}: #{val}"
@@ -71,6 +62,19 @@ module CushionDefaults
71
62
  CushionDefaults.log(log_str, :info)
72
63
  end
73
64
 
65
+ # Either set up or wipe @defaults. Should not usually be called directly.
66
+ # @api private
67
+ def initialize_defaults_hash
68
+ if @defaults
69
+ # We need to maintain the identity of the hash, as child classes may have stored a reference to it
70
+ @defaults.clear
71
+ else
72
+ @defaults = DefaultsHash.new(self)
73
+ end
74
+ end
75
+
76
+ # @!group Reader/Writer
77
+
74
78
  # Sets up a cushion_reader for each :sym in +syms+.
75
79
  #
76
80
  # Each reader method checks if its instance variable (:sym) is defined. If it is, it returns that. If not, it
@@ -204,6 +208,88 @@ module CushionDefaults
204
208
  CushionDefaults.log("cushions established for #{self}'s defaults': #{defaults.keys.join(', ')}", :info)
205
209
  end
206
210
 
211
+ # @!group Freeze/Thaw Defaults
212
+
213
+ # Prevents a default from being set to a new value. Freezing a default is permanent in the life of the program.
214
+ #
215
+ # Note that while a frozen default is guaranteed to maintain its identity, its attributes can still be modified
216
+ # (e.g., by bang! methods). To prevent any modification to a default value, call {#deep_freeze_default}.
217
+ #
218
+ # Does not throw an error when you attempt to freeze an already-frozen default, but it does log at warning level.
219
+ #
220
+ # @note A frozen default can still be overridden lower in the class hierarchy.
221
+ # @see #thaw_default(sym)
222
+ #
223
+ # @api freeze
224
+ def freeze_default(*syms)
225
+ syms.each do |sym|
226
+ sym = sym.to_sym
227
+ unless defaults.freeze_default! sym
228
+ # It returns nil if the key was already present in the Set
229
+ CushionDefaults.log("Cannot freeze #{sym}: it was already frozen for #{self}", :warn)
230
+ end
231
+ end
232
+ end
233
+
234
+ # Calls {#freeze_default} for all defaults.
235
+ #
236
+ # @api freeze
237
+ def freeze_defaults
238
+ freeze_default *defaults
239
+ end
240
+
241
+ # In addition to preventing the +default[:sym]+ from being set to a new value (see {#freeze_default}), also freezes
242
+ # the value to which +default[:sym]+ is currently set.
243
+ #
244
+ # Does not throw an error when you attempt to freeze an already-frozen default, but it does log at warning level.
245
+ #
246
+ # Equivalent to calling: +freeze_default :sym; defaults[:sym].freeze+
247
+ #
248
+ # @api freeze
249
+ def deep_freeze_default(*syms)
250
+ syms.each do |sym|
251
+ sym = sym.to_sym
252
+ freeze_default sym
253
+ defaults[sym].freeze
254
+ end
255
+ end
256
+
257
+ # Calls {#deep_freeze_default} for all defaults.
258
+ #
259
+ # @api freeze
260
+ def deep_freeze_defaults
261
+ deep_freeze_default *defaults
262
+ end
263
+
264
+ # Thaws a frozen default, allowing it to be set to a new value.
265
+ #
266
+ # Note that if the value of the +default[:sym]+ is itself frozen (using {#deep_freeze_default} or +Object#freeze+),
267
+ # thaw_default allows you to set +default[:sym]+ to a *new* value but does not and cannot allow you to modify the
268
+ # value to which +default[:sym]+ points until and unless +default[:sym]+ is set to a new value.
269
+ #
270
+ # Does not throw an error when you attempt to thaw a non-frozen default, but it does log at warning level.
271
+ #
272
+ # @api freeze
273
+ def thaw_default(*syms)
274
+ syms.each do |sym|
275
+ sym = sym.to_sym
276
+ unless defaults.thaw_default! sym
277
+ # It returns nil if the key was not present in the Set
278
+ CushionDefaults.log("Cannot thaw #{sym}: it was already thawed for #{self}", :warn)
279
+ end
280
+ puts "After thawing, currently frozen defaults are #{defaults.frozen_defaults.to_a.join(', ')}"
281
+ end
282
+ end
283
+
284
+ # Calls {#thaw_default} for all defaults.
285
+ #
286
+ # @api freeze
287
+ def thaw_defaults
288
+ thaw_default *defaults
289
+ end
290
+
291
+ # @!group Pushy/Polite Defaults
292
+
207
293
  # Declare each sym to be pushy. Pushy defaults will be returned by {#cushion_reader}s regardless of what the
208
294
  # instance variables are set to.
209
295
  def make_pushy(*syms)
@@ -216,6 +302,8 @@ module CushionDefaults
216
302
  syms.each { |sym| @defaults.not_pushy!(sym) }
217
303
  end
218
304
 
305
+ # @!endgroup
306
+
219
307
  # Ensure that if class +Klass+ includes +CushionDefaults+, then any class that subclasses +Klass+ will include it as
220
308
  # well.
221
309
  #
@@ -10,6 +10,8 @@ module CushionDefaults
10
10
  class Configuration
11
11
  # adapted from http://brandonhilkert.com/blog/ruby-gem-configuration-patterns/
12
12
 
13
+ # @!group Option Reader/Writers
14
+
13
15
  # @return [boolean] true if readers should automatically be added, false otherwise. Def. false.
14
16
  attr_accessor :update_readers
15
17
 
@@ -55,17 +57,35 @@ module CushionDefaults
55
57
  # nil or '' (as applicable). Has no effect if {#ignore_attempts_to_set_nil} == false. Def. false.
56
58
  attr_accessor :whiny_ignores
57
59
 
58
- # Has no effect.
59
- # @deprecated
60
- attr_accessor :cushion_child_defaults_in_parent
61
-
62
60
  # @return [Logger] returns the Logger object to which CushionDefaults sends log entries.
63
61
  attr_reader :logger
64
62
 
65
- # Initializes with the values specified in {#defaults!}
66
- def initialize
67
- defaults!
68
- @we_have_a_pushy = false
63
+ # @!attribute [w] logger
64
+ # Sets @logger to either a different Logger or another object that implements roughly the same interface. Note
65
+ # that only minimal checking is done to ensure the interface is implemented, and passing in an object with an
66
+ # incomplete implementation of the interface will cause errors. If you want to disable logging, you should instead
67
+ # set {#record_in_log} to false.
68
+ # @see #record_in_log
69
+ def logger=(new_logger)
70
+ if new_logger.nil?
71
+ self.record_in_log = false
72
+ @logger = nil
73
+ else
74
+ if new_logger.respond_to? :info
75
+ @logger = new_logger
76
+ assign_formatter_to_logger
77
+ else
78
+ CushionDefaults.log("config.logger not set to #{new_logger}, as it does not appear to implement standard logging methods.", :error)
79
+ end
80
+ end
81
+ end
82
+
83
+ # @!attribute [w] log_lvl
84
+ # Set the level of logging. Alias of logger.level=. 0: debug, 1: info, 2: warn, 3: error, 4: fatal, 5: unknown
85
+ def log_lvl=(lvl)
86
+ if @logger
87
+ @logger.level = lvl
88
+ end
69
89
  end
70
90
 
71
91
  # @!attribute [r] yaml_source_full_path
@@ -75,6 +95,14 @@ module CushionDefaults
75
95
  @yaml_source_full_path || CushionDefaults::CALLING_PATH + yaml_source_folder
76
96
  end
77
97
 
98
+ # @!endgroup
99
+
100
+ # Initializes with the values specified in {#defaults!}
101
+ def initialize
102
+ defaults!
103
+ @we_have_a_pushy = false
104
+ end
105
+
78
106
  # Update configuration options with those values contained within +loaded_config+.
79
107
  #
80
108
  # @param loaded_config [Hash] hash of config options and settings
@@ -90,6 +118,8 @@ module CushionDefaults
90
118
  CushionDefaults.log(log_str, :info)
91
119
  end
92
120
 
121
+ # @!group Mass Setter Methods
122
+
93
123
  # Sets or resets configuration object to default configuration settings.
94
124
  def defaults!
95
125
  self.update_readers = false
@@ -114,25 +144,7 @@ module CushionDefaults
114
144
  self.log_lvl = Logger::DEBUG
115
145
  end
116
146
 
117
- # @!attribute [w] logger
118
- # Sets @logger to either a different Logger or another object that implements roughly the same interface. Note
119
- # that only minimal checking is done to ensure the interface is implemented, and passing in an object with an
120
- # incomplete implementation of the interface will cause errors. If you want to disable logging, you should instead
121
- # set {#record_in_log} to false.
122
- # @see #record_in_log
123
- def logger=(new_logger)
124
- if new_logger.nil?
125
- self.record_in_log = false
126
- @logger = nil
127
- else
128
- if new_logger.respond_to? :info
129
- @logger = new_logger
130
- assign_formatter_to_logger
131
- else
132
- CushionDefaults.log("config.logger not set to #{new_logger}, as it does not appear to implement standard logging methods.", :error)
133
- end
134
- end
135
- end
147
+ # @!endgroup
136
148
 
137
149
  # CamelCase to underscores
138
150
  # @param s [String] CamelCase
@@ -152,13 +164,9 @@ module CushionDefaults
152
164
  "#{CushionDefaults::configuration.yaml_source_full_path}#{underscore(klass.to_s)+'.yaml'}"
153
165
  end
154
166
 
155
- # @!attribute [w] log_lvl
156
- # Set the level of logging. Alias of logger.level=. 0: debug, 1: info, 2: warn, 3: error, 4: fatal, 5: unknown
157
- def log_lvl=(lvl)
158
- if @logger
159
- @logger.level = lvl
160
- end
161
- end
167
+
168
+
169
+ # @!group Deprecated/Untrustworthy Methods
162
170
 
163
171
  # @!attribute [r] we_have_a_pushy
164
172
  # Denotes whether (over the life of the program) any defaults have been marked pushy.
@@ -186,6 +194,12 @@ module CushionDefaults
186
194
  @we_have_a_pushy = true
187
195
  end
188
196
 
197
+ # Has no effect.
198
+ # @deprecated
199
+ attr_accessor :cushion_child_defaults_in_parent
200
+
201
+ # @!endgroup
202
+
189
203
  protected
190
204
 
191
205
  # Specifies the formatter for logger. Will not be called if a logger is assigned with +should_assign_formatter+ set to
@@ -16,6 +16,8 @@ module CushionDefaults
16
16
  attr_reader :pushy_defaults
17
17
  # Set of defaults marked as polite.
18
18
  attr_reader :polite_defaults
19
+ # Set of defaults frozen
20
+ attr_reader :frozen_defaults
19
21
 
20
22
  # @param owner [Class] class for which this DefaultsHash holds the defaults.
21
23
  def initialize(owner)
@@ -24,8 +26,11 @@ module CushionDefaults
24
26
  @owner = owner
25
27
  @pushy_defaults = Set.new
26
28
  @polite_defaults = Set.new
29
+ @frozen_defaults = Set.new
27
30
  end
28
31
 
32
+ # @!group Custom/New Hash Methods
33
+
29
34
  # Allows addition of hashes. Works as expected.
30
35
  #
31
36
  # Note that this also enables +=.
@@ -47,35 +52,19 @@ module CushionDefaults
47
52
  self
48
53
  end
49
54
 
50
- # Tell owner to add readers and writers for a newly-added key, as configured.
51
- # @param key [#to_sym] name of new default
52
- # @see Configuration#update_readers
53
- # @see Configuration#update_writers
54
- # @see ClassMethods#cushion
55
- def add_methods_as_needed(key)
56
- @owner.cushion_reader key.to_sym if CushionDefaults.conf.update_readers
57
- @owner.cushion_writer key.to_sym if CushionDefaults.conf.update_writers
58
- end
59
-
60
- # Tell owner to remove methods for a newly-deleted key, as configured.
61
- # @param key [#to_sym] name of deleted default
62
- # @see Configuration#update_readers
63
- # @see Configuration#update_writers
64
- # @see ClassMethods#remove_reader
65
- # @see ClassMethods#remove_writer
66
- def remove_methods_as_needed(key)
67
- @owner.remove_reader key.to_sym if CushionDefaults.conf.update_readers
68
- @owner.remove_writer key.to_sym if CushionDefaults.conf.update_writers
69
- end
70
-
71
55
  # Custom key/value set method. Prevents writing a default when a parent has it marked as pushy (and it is not
72
56
  # otherwise marked as polite), and it also tells @owner to add methods as needed (if writers or readers are to be
73
57
  # automatically added).
58
+ #
59
+ # @raise [FrozenDefaultError] if key is frozen
74
60
  def []=(key,val)
75
61
  key = key.to_sym
76
62
  if !@polite_defaults.include?(key) && pushy_in_parent?(key)
77
63
  raise ArgumentError, 'You cannot set a default value marked as pushy in a parent class without first marking it as polite.'
78
64
  end
65
+ if has_key?(key) && default_frozen?(key)
66
+ raise FrozenDefaultError.new(@owner, key), "#{@owner}.defaults[:#{key} is frozen!"
67
+ end
79
68
  unless has_ish_key?(key)
80
69
  add_methods_as_needed(key)
81
70
  end
@@ -86,9 +75,11 @@ module CushionDefaults
86
75
  # need to remove the key from @pushy_defaults and @polite_defaults if it exists.
87
76
  def delete(key)
88
77
  key = key.to_sym
78
+ return unless has_key? key
89
79
  remove_methods_as_needed(key)
90
80
  @pushy_defaults.delete(key)
91
81
  @polite_defaults.delete(key)
82
+ @frozen_defaults.delete(key)
92
83
  super(key)
93
84
  CushionDefaults.log("Default for #{key} in #{@owner} deleted.")
94
85
  end
@@ -101,6 +92,7 @@ module CushionDefaults
101
92
  end
102
93
  @pushy_defaults.clear
103
94
  @polite_defaults.clear
95
+ @frozen_defaults.clear
104
96
  super
105
97
  CushionDefaults.log("All defaults cleared for #{@owner}.", :info)
106
98
  end
@@ -137,6 +129,35 @@ module CushionDefaults
137
129
  end
138
130
  end
139
131
 
132
+ # @!group Frozen Defaults
133
+
134
+ # @return [boolean] true if the current default (not its value) is frozen, false otherwise.
135
+ def default_frozen? key
136
+ key = key.to_sym
137
+ @frozen_defaults.include? key
138
+ end
139
+
140
+ # Freezes the specified default (not its value).
141
+ #
142
+ # @see {#thaw_default!}
143
+ # @see {ClassMethods#freeze_default}
144
+ # @see {ClassMethods#deep_freeze_default}
145
+ # @return [Set] @frozen_defaults if key was not already present, nil otherwise
146
+ def freeze_default! key
147
+ @frozen_defaults.add? key
148
+ end
149
+
150
+ # Thaws the specified default (not its value).
151
+ #
152
+ # @see {#freeze_default!}
153
+ # @see {ClassMethods#thaw_default}
154
+ # @return [Set] @frozen_defaults if key was present, nil otherwise
155
+ def thaw_default! key
156
+ @frozen_defaults.delete? key
157
+ end
158
+
159
+ # @!group Pushy Defaults
160
+
140
161
  # Mark sym as pushy
141
162
  def pushy!(sym)
142
163
  CushionDefaults.conf.we_have_a_pushy!
@@ -178,6 +199,8 @@ module CushionDefaults
178
199
  !pushy?(sym)
179
200
  end
180
201
 
202
+ # @!group Debug
203
+
181
204
  # Returns the nearest class that has a default set for sym, or nil if no default is set for it.
182
205
  def where_is_that_default_again(sym)
183
206
  if has_key? sym
@@ -189,6 +212,29 @@ module CushionDefaults
189
212
  end
190
213
  end
191
214
 
215
+ # @!endgroup
216
+
217
+ # Tell owner to add readers and writers for a newly-added key, as configured.
218
+ # @param key [#to_sym] name of new default
219
+ # @see Configuration#update_readers
220
+ # @see Configuration#update_writers
221
+ # @see ClassMethods#cushion
222
+ def add_methods_as_needed(key)
223
+ @owner.cushion_reader key.to_sym if CushionDefaults.conf.update_readers
224
+ @owner.cushion_writer key.to_sym if CushionDefaults.conf.update_writers
225
+ end
226
+
227
+ # Tell owner to remove methods for a newly-deleted key, as configured.
228
+ # @param key [#to_sym] name of deleted default
229
+ # @see Configuration#update_readers
230
+ # @see Configuration#update_writers
231
+ # @see ClassMethods#remove_reader
232
+ # @see ClassMethods#remove_writer
233
+ def remove_methods_as_needed(key)
234
+ @owner.remove_reader key.to_sym if CushionDefaults.conf.update_readers
235
+ @owner.remove_writer key.to_sym if CushionDefaults.conf.update_writers
236
+ end
237
+
192
238
  protected
193
239
 
194
240
  # Return (unless cached) a reference to the superclass' +defaults+ hash.
@@ -0,0 +1,20 @@
1
+ module CushionDefaults
2
+ # Raised when a program attempts to overwrite a frozen default value.
3
+ class FrozenDefaultError < RuntimeError
4
+ # @return [Class] whose frozen default {#frozen_default_name} the program tried to override
5
+ attr_reader :originating_class
6
+ # @return [symbol] the frozen default in {#originating_class} that the program attempted to override
7
+ attr_reader :frozen_default_name
8
+
9
+ def initialize(orignating_class, frozen_default_name)
10
+ @originating_class = orignating_class
11
+ @frozen_default_name = frozen_default_name
12
+ end
13
+
14
+ # Include {#originating_class} and {#frozen_default_name} in #to_s
15
+ # @return [String]
16
+ def to_s
17
+ "#{@originating_class}.defaults[:#{@frozen_default_name} is frozen!"
18
+ end
19
+ end
20
+ end
@@ -3,6 +3,7 @@ require 'set' # :nodoc:
3
3
  require 'cushion_defaults/configuration'
4
4
  require 'cushion_defaults/class_methods'
5
5
  require 'cushion_defaults/defaults_hash'
6
+ require 'cushion_defaults/errors'
6
7
 
7
8
  # Base module. Should be included in any class that needs the functionality offered by CushionDefaults. For a basic
8
9
  # introduction, you are strongly encouraged to consult the {file:README.md readme}.
@@ -27,7 +28,7 @@ require 'cushion_defaults/defaults_hash'
27
28
  module CushionDefaults
28
29
 
29
30
  # Version constant
30
- VERSION = '0.2.0'
31
+ VERSION = '0.3.0'
31
32
 
32
33
  # The path of the first file that +includes+ CushionDefaults.
33
34
  CALLING_PATH = File.expand_path(File.dirname($0)) + '/'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cushion_defaults
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Mitchell
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '2.0'
19
+ version: '2.11'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '2.0'
26
+ version: '2.11'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: yard
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -54,6 +54,7 @@ files:
54
54
  - lib/cushion_defaults/class_methods.rb
55
55
  - lib/cushion_defaults/configuration.rb
56
56
  - lib/cushion_defaults/defaults_hash.rb
57
+ - lib/cushion_defaults/errors.rb
57
58
  homepage: https://github.com/posgarou/cushion_defaults
58
59
  licenses:
59
60
  - MIT