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 +4 -4
- data/CHANGELOG.md +10 -1
- data/README.md +146 -33
- data/lib/cushion_defaults/class_methods.rb +102 -14
- data/lib/cushion_defaults/configuration.rb +48 -34
- data/lib/cushion_defaults/defaults_hash.rb +67 -21
- data/lib/cushion_defaults/errors.rb +20 -0
- data/lib/cushion_defaults.rb +2 -1
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8b3c6fc2391e0197aea23df6eb08eaa1bbf91946
|
4
|
+
data.tar.gz: 995a6c5ad6d1caea8f5f2cbcc49f99bf3f87a15c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
-
|
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
|
-
##
|
424
|
+
## Well, If You Ask Me...
|
312
425
|
|
313
426
|
Any feedback is very much appreciated!
|
314
427
|
|
315
|
-
###
|
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
|
-
#
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
-
# @!
|
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
|
-
|
156
|
-
|
157
|
-
|
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
|
data/lib/cushion_defaults.rb
CHANGED
@@ -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.
|
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.
|
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.
|
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.
|
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
|