configliere 0.3.4 → 0.4.4
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +3 -0
- data/.watchr +20 -0
- data/CHANGELOG.textile +99 -3
- data/Gemfile +26 -0
- data/Gemfile.lock +54 -0
- data/README.textile +162 -138
- data/Rakefile +30 -21
- data/VERSION +1 -1
- data/bin/configliere +77 -77
- data/bin/configliere-decrypt +85 -0
- data/bin/configliere-delete +85 -0
- data/bin/configliere-dump +85 -0
- data/bin/configliere-encrypt +85 -0
- data/bin/configliere-list +85 -0
- data/bin/configliere-set +85 -0
- data/configliere.gemspec +53 -23
- data/examples/config_block_script.rb +9 -2
- data/examples/encrypted_script.rb +28 -16
- data/examples/env_var_script.rb +2 -2
- data/examples/help_message_demo.rb +16 -0
- data/examples/independent_config.rb +28 -0
- data/examples/prompt.rb +23 -0
- data/examples/simple_script.rb +28 -15
- data/examples/simple_script.yaml +1 -1
- data/lib/configliere.rb +22 -24
- data/lib/configliere/commandline.rb +135 -116
- data/lib/configliere/commands.rb +38 -54
- data/lib/configliere/config_block.rb +4 -2
- data/lib/configliere/config_file.rb +30 -52
- data/lib/configliere/crypter.rb +8 -5
- data/lib/configliere/deep_hash.rb +368 -0
- data/lib/configliere/define.rb +83 -89
- data/lib/configliere/encrypted.rb +17 -18
- data/lib/configliere/env_var.rb +5 -7
- data/lib/configliere/param.rb +37 -64
- data/lib/configliere/prompt.rb +23 -0
- data/spec/configliere/commandline_spec.rb +156 -57
- data/spec/configliere/commands_spec.rb +75 -30
- data/spec/configliere/config_block_spec.rb +10 -1
- data/spec/configliere/config_file_spec.rb +83 -55
- data/spec/configliere/crypter_spec.rb +3 -2
- data/spec/configliere/deep_hash_spec.rb +401 -0
- data/spec/configliere/define_spec.rb +121 -42
- data/spec/configliere/encrypted_spec.rb +53 -20
- data/spec/configliere/env_var_spec.rb +24 -4
- data/spec/configliere/param_spec.rb +25 -27
- data/spec/configliere/prompt_spec.rb +50 -0
- data/spec/configliere_spec.rb +3 -9
- data/spec/spec_helper.rb +17 -6
- metadata +110 -35
- data/lib/configliere/core_ext.rb +0 -2
- data/lib/configliere/core_ext/blank.rb +0 -93
- data/lib/configliere/core_ext/hash.rb +0 -108
- data/lib/configliere/core_ext/sash.rb +0 -170
- data/spec/configliere/core_ext/hash_spec.rb +0 -78
- data/spec/configliere/core_ext/sash_spec.rb +0 -312
data/lib/configliere/commands.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
Configliere.use :commandline
|
2
1
|
module Configliere
|
3
2
|
|
4
3
|
#
|
@@ -6,54 +5,39 @@ module Configliere
|
|
6
5
|
#
|
7
6
|
# To include, specify
|
8
7
|
#
|
9
|
-
#
|
8
|
+
# Settings.use :commands
|
10
9
|
#
|
11
10
|
module Commands
|
12
|
-
|
13
11
|
# The name of the command.
|
14
12
|
attr_accessor :command_name
|
15
13
|
|
14
|
+
#
|
15
|
+
# FIXME: this will be refactored to look like Configliere::Define
|
16
|
+
#
|
17
|
+
|
16
18
|
# Add a command, along with a description of its predicates and the command itself.
|
17
19
|
def define_command cmd, options={}, &block
|
18
|
-
|
20
|
+
cmd = cmd.to_sym
|
21
|
+
command_configuration = Configliere::Param.new
|
22
|
+
command_configuration.use :commandline, :env_var
|
19
23
|
yield command_configuration if block_given?
|
20
24
|
commands[cmd] = options.merge(:config => command_configuration)
|
21
25
|
end
|
22
26
|
|
23
|
-
# Define a help command.
|
24
|
-
def define_help_command!
|
25
|
-
define_command :help, :description => "Print detailed help on each command"
|
26
|
-
end
|
27
|
-
|
28
|
-
# Are there any commands that have been defined?
|
29
|
-
def commands?
|
30
|
-
(! commands.empty?)
|
31
|
-
end
|
32
|
-
|
33
|
-
# Is +cmd+ the name of a known command?
|
34
|
-
def command? cmd
|
35
|
-
return false if cmd.blank?
|
36
|
-
commands.include?(cmd) || commands.include?(cmd.to_s)
|
37
|
-
end
|
38
|
-
|
39
27
|
def commands
|
40
|
-
@commands ||=
|
41
|
-
end
|
42
|
-
|
43
|
-
def command
|
44
|
-
command_name && commands[command_name]
|
28
|
+
@commands ||= DeepHash.new
|
45
29
|
end
|
46
30
|
|
47
|
-
|
48
|
-
|
49
|
-
command && command[:config]
|
31
|
+
def command_info
|
32
|
+
commands[command_name]
|
50
33
|
end
|
51
34
|
|
52
35
|
def resolve!
|
53
36
|
super()
|
54
|
-
commands.each_value do |
|
55
|
-
|
37
|
+
commands.each_value do |cmd_info|
|
38
|
+
cmd_info[:config].resolve!
|
56
39
|
end
|
40
|
+
self
|
57
41
|
end
|
58
42
|
|
59
43
|
#
|
@@ -68,41 +52,41 @@ module Configliere
|
|
68
52
|
#
|
69
53
|
def process_argv!
|
70
54
|
super()
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
55
|
+
base, cmd = script_base_and_command
|
56
|
+
if cmd
|
57
|
+
self.command_name = cmd.to_sym
|
58
|
+
elsif (not rest.empty?) && commands.include?(rest.first.to_sym)
|
59
|
+
self.command_name = rest.shift.to_sym
|
75
60
|
end
|
76
61
|
end
|
77
62
|
|
78
|
-
# The script name without command appendix if any: For $0 equal to any of
|
79
|
-
# 'git', 'git-reset', or 'git-cherry-pick', base_script_name is 'git'
|
80
|
-
#
|
81
|
-
def base_script_name
|
82
|
-
raw_script_name.gsub(/-.*/, '')
|
83
|
-
end
|
84
|
-
|
85
63
|
# Usage line
|
86
64
|
def usage
|
87
|
-
%Q{usage: #{
|
65
|
+
%Q{usage: #{script_base_and_command.first} [command] [...--param=val...]}
|
88
66
|
end
|
89
67
|
|
68
|
+
protected
|
69
|
+
|
90
70
|
# Return help on commands.
|
91
|
-
def
|
92
|
-
help = ["
|
93
|
-
|
94
|
-
" %-27s %s" % [
|
71
|
+
def commands_help
|
72
|
+
help = ["\nAvailable commands:"]
|
73
|
+
commands.sort_by(&:to_s).each do |cmd, info|
|
74
|
+
help << (" %-27s %s" % [cmd, info[:description]]) unless info[:internal]
|
95
75
|
end
|
96
|
-
help
|
97
|
-
|
76
|
+
help << "\nRun `#{script_base_and_command.first} help COMMAND' for more help on COMMAND" if commands.include?(:help)
|
77
|
+
help.flatten.join("\n")
|
78
|
+
end
|
79
|
+
|
80
|
+
# The script name without command appendix if any: For $0 equal to any of
|
81
|
+
# 'git', 'git-reset', or 'git-cherry-pick', base_script_name is 'git'
|
82
|
+
#
|
83
|
+
def script_base_and_command
|
84
|
+
raw_script_name.split('-', 2)
|
98
85
|
end
|
99
|
-
|
100
86
|
end
|
101
87
|
|
102
|
-
Param.
|
103
|
-
|
104
|
-
|
105
|
-
# Commandline methods in the superclass chain.
|
106
|
-
include Commands
|
88
|
+
Param.on_use(:commands) do
|
89
|
+
use :commandline
|
90
|
+
extend Configliere::Commands
|
107
91
|
end
|
108
92
|
end
|
@@ -1,4 +1,3 @@
|
|
1
|
-
Configliere.use :define
|
2
1
|
module Configliere
|
3
2
|
#
|
4
3
|
# ConfigBlock lets you use pure ruby to change and define settings. Call
|
@@ -8,6 +7,8 @@ module Configliere
|
|
8
7
|
# Settings.finally{|c| c.your_mom[:college] = 'went' unless (! c.mom_jokes_allowed) }
|
9
8
|
#
|
10
9
|
module ConfigBlock
|
10
|
+
#
|
11
|
+
# Define a block of code to run after all other settings are in place.
|
11
12
|
#
|
12
13
|
# @param &block each +finally+ block is called once, in the order it was
|
13
14
|
# defined, when the resolve! method is invoked. +config_block+ resolution
|
@@ -43,10 +44,11 @@ module Configliere
|
|
43
44
|
def final_blocks
|
44
45
|
@final_blocks ||= []
|
45
46
|
end
|
47
|
+
|
46
48
|
# call each +finally+ config block in the order it was defined
|
47
49
|
def resolve_finally_blocks!
|
48
50
|
final_blocks.each do |block|
|
49
|
-
block.call()
|
51
|
+
(block.arity == 1) ? block.call(self) : block.call()
|
50
52
|
end
|
51
53
|
end
|
52
54
|
end
|
@@ -1,8 +1,4 @@
|
|
1
|
-
require 'yaml'
|
2
|
-
require 'fileutils'
|
3
1
|
module Configliere
|
4
|
-
# Where to load params given only a symbol
|
5
|
-
DEFAULT_CONFIG_FILE = ENV['HOME'].to_s+'/.configliere.yaml' unless defined?(DEFAULT_CONFIG_FILE)
|
6
2
|
# Where to load params given a bare filename
|
7
3
|
DEFAULT_CONFIG_DIR = ENV['HOME'].to_s+'/.configliere' unless defined?(DEFAULT_CONFIG_DIR)
|
8
4
|
|
@@ -10,84 +6,66 @@ module Configliere
|
|
10
6
|
# ConfigFile -- load configuration from a simple YAML file
|
11
7
|
#
|
12
8
|
module ConfigFile
|
13
|
-
# Load params from
|
14
|
-
# * file is in YAML format, as a hash of handle => param_hash pairs
|
15
|
-
# * filename defaults to Configliere::DEFAULT_CONFIG_FILE (~/.configliere, probably)
|
9
|
+
# Load params from a YAML file, as a hash of handle => param_hash pairs
|
16
10
|
#
|
17
|
-
# @
|
11
|
+
# @param filename [String] the file to read. If it does not contain a '/',
|
12
|
+
# the filename is expanded relative to Configliere::DEFAULT_CONFIG_DIR
|
13
|
+
# @param options [Hash]
|
14
|
+
# @option options :env [String]
|
18
15
|
# If an :env option is given, only the indicated subhash is merged. This
|
19
16
|
# lets you for example specify production / environment / test settings
|
20
17
|
#
|
21
|
-
# @
|
18
|
+
# @return [Configliere::Params] the Settings object
|
22
19
|
#
|
23
20
|
# @example
|
24
|
-
# # Read from
|
25
|
-
# Settings.read(
|
21
|
+
# # Read from ~/.configliere/foo.yaml
|
22
|
+
# Settings.read(foo.yaml)
|
26
23
|
#
|
27
|
-
|
28
|
-
|
24
|
+
# @example
|
25
|
+
# # Read from config/foo.yaml and use settings appropriate for development/staging/production
|
26
|
+
# Settings.read(App.root.join('config', 'environment.yaml'), :env => ENV['RACK_ENV'])
|
27
|
+
#
|
28
|
+
# The env option is *not* coerced to_sym, so make sure your key type matches the file's
|
29
|
+
def read filename, options={}
|
30
|
+
if filename.is_a?(Symbol) then raise Configliere::DeprecatedError, "Loading from a default config file is no longer provided" ; end
|
31
|
+
filename = expand_filename(filename)
|
29
32
|
begin
|
30
|
-
|
33
|
+
new_data = YAML.load(File.open(filename)) || {}
|
31
34
|
rescue Errno::ENOENT => e
|
32
35
|
warn "Loading empty configliere settings file #{filename}"
|
33
|
-
|
36
|
+
new_data = {}
|
34
37
|
end
|
35
|
-
params = params[handle] if handle.is_a?(Symbol)
|
36
38
|
# Extract the :env (production/development/etc)
|
37
39
|
if options[:env]
|
38
|
-
|
40
|
+
new_data = new_data[options[:env]] || {}
|
39
41
|
end
|
40
|
-
deep_merge!
|
42
|
+
deep_merge! new_data
|
41
43
|
self
|
42
44
|
end
|
43
45
|
|
44
46
|
# save to disk.
|
45
47
|
# * file is in YAML format, as a hash of handle => param_hash pairs
|
46
48
|
# * filename defaults to Configliere::DEFAULT_CONFIG_FILE (~/.configliere, probably)
|
47
|
-
def save!
|
48
|
-
filename =
|
49
|
-
|
50
|
-
ConfigFile.merge_into_yaml_file filename, handle, self.export
|
51
|
-
else
|
52
|
-
ConfigFile.write_yaml_file filename, self.export
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
protected
|
57
|
-
|
58
|
-
# form suitable for serialization to disk
|
59
|
-
# (e.g. the encryption done in configliere/encrypted)
|
60
|
-
def export
|
61
|
-
super.to_hash
|
62
|
-
end
|
63
|
-
|
64
|
-
def self.write_yaml_file filename, hsh
|
49
|
+
def save! filename
|
50
|
+
filename = expand_filename(filename)
|
51
|
+
hsh = self.export.to_hash
|
65
52
|
FileUtils.mkdir_p(File.dirname(filename))
|
66
53
|
File.open(filename, 'w'){|f| f << YAML.dump(hsh) }
|
67
54
|
end
|
68
55
|
|
69
|
-
|
70
|
-
begin
|
71
|
-
all_settings = YAML.load(File.open(filename)) || {}
|
72
|
-
rescue Errno::ENOENT => e;
|
73
|
-
all_settings = {}
|
74
|
-
end
|
75
|
-
all_settings[handle] = params
|
76
|
-
write_yaml_file filename, all_settings
|
77
|
-
end
|
56
|
+
protected
|
78
57
|
|
79
|
-
def
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
58
|
+
def expand_filename filename
|
59
|
+
if filename.to_s.include?('/')
|
60
|
+
File.expand_path(filename)
|
61
|
+
else
|
62
|
+
File.expand_path(File.join(Configliere::DEFAULT_CONFIG_DIR, filename))
|
84
63
|
end
|
85
64
|
end
|
86
|
-
|
87
65
|
end
|
88
66
|
|
67
|
+
# ConfigFile is included by default
|
89
68
|
Param.class_eval do
|
90
|
-
# include read / save operations
|
91
69
|
include ConfigFile
|
92
70
|
end
|
93
71
|
end
|
data/lib/configliere/crypter.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
|
-
require 'openssl'
|
2
|
-
require 'digest/sha2'
|
1
|
+
require 'openssl' # for encryption
|
2
|
+
require 'digest/sha2' # for encryption
|
3
|
+
require "base64" # base64-encode the binary encrypted string
|
3
4
|
module Configliere
|
4
5
|
#
|
5
6
|
# Encrypt and decrypt values in configliere stores
|
@@ -22,7 +23,7 @@ module Configliere
|
|
22
23
|
cipher.iv = iv = cipher.random_iv
|
23
24
|
ciphertext = cipher.update(plaintext)
|
24
25
|
ciphertext << cipher.final
|
25
|
-
combine_iv_and_ciphertext(iv, ciphertext)
|
26
|
+
Base64.encode64(combine_iv_and_ciphertext(iv, ciphertext))
|
26
27
|
end
|
27
28
|
#
|
28
29
|
# Decrypt the given string, using the key and iv supplied
|
@@ -31,7 +32,8 @@ module Configliere
|
|
31
32
|
# @param [String] encrypt_pass secret passphrase to decrypt with
|
32
33
|
# @return [String] the decrypted plaintext
|
33
34
|
#
|
34
|
-
def self.decrypt
|
35
|
+
def self.decrypt enc_ciphertext, encrypt_pass, options={}
|
36
|
+
iv_and_ciphertext = Base64.decode64(enc_ciphertext)
|
35
37
|
cipher = new_cipher :decrypt, encrypt_pass, options
|
36
38
|
cipher.iv, ciphertext = separate_iv_and_ciphertext(cipher, iv_and_ciphertext)
|
37
39
|
plaintext = cipher.update(ciphertext)
|
@@ -64,7 +66,8 @@ module Configliere
|
|
64
66
|
|
65
67
|
# Convert the encrypt_pass passphrase into the key used for encryption
|
66
68
|
def self.encrypt_key encrypt_pass, options={}
|
67
|
-
|
69
|
+
encrypt_pass = encrypt_pass.to_s
|
70
|
+
raise 'Missing encryption password!' if encrypt_pass.empty?
|
68
71
|
# this provides the required 256 bits of key for the aes-256-cbc cipher
|
69
72
|
Digest::SHA256.digest(encrypt_pass)
|
70
73
|
end
|
@@ -0,0 +1,368 @@
|
|
1
|
+
#
|
2
|
+
# A magical hash: allows deep-nested access and proper merging of settings from
|
3
|
+
# different sources
|
4
|
+
#
|
5
|
+
class DeepHash < Hash
|
6
|
+
|
7
|
+
# @param constructor<Object>
|
8
|
+
# The default value for the DeepHash. Defaults to an empty hash.
|
9
|
+
# If constructor is a Hash, adopt its values.
|
10
|
+
def initialize(constructor = {})
|
11
|
+
if constructor.is_a?(Hash)
|
12
|
+
super()
|
13
|
+
update(constructor) unless constructor.empty?
|
14
|
+
else
|
15
|
+
super(constructor)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
unless method_defined?(:regular_writer) then alias_method :regular_writer, :[]= ; end
|
20
|
+
unless method_defined?(:regular_update) then alias_method :regular_update, :update ; end
|
21
|
+
|
22
|
+
# @param key<Object> The key to check for. This will be run through convert_key.
|
23
|
+
#
|
24
|
+
# @return [Boolean] True if the key exists in the mash.
|
25
|
+
def key?(key)
|
26
|
+
attr = convert_key(key)
|
27
|
+
if attr.is_a?(Array)
|
28
|
+
fk = attr.shift
|
29
|
+
attr = attr.first if attr.length == 1
|
30
|
+
super(fk) && (self[fk].key?(attr)) rescue nil
|
31
|
+
else
|
32
|
+
super(attr)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# def include? def has_key? def member?
|
37
|
+
alias_method :include?, :key?
|
38
|
+
alias_method :has_key?, :key?
|
39
|
+
alias_method :member?, :key?
|
40
|
+
|
41
|
+
# @param key<Object> The key to fetch. This will be run through convert_key.
|
42
|
+
# @param *extras<Array> Default value.
|
43
|
+
#
|
44
|
+
# @return [Object] The value at key or the default value.
|
45
|
+
def fetch(key, *extras)
|
46
|
+
super(convert_key(key), *extras)
|
47
|
+
end
|
48
|
+
|
49
|
+
# @param *indices<Array>
|
50
|
+
# The keys to retrieve values for. These will be run through +convert_key+.
|
51
|
+
#
|
52
|
+
# @return [Array] The values at each of the provided keys
|
53
|
+
def values_at(*indices)
|
54
|
+
indices.collect{|key| self[convert_key(key)]}
|
55
|
+
end
|
56
|
+
|
57
|
+
# @param key<Object>
|
58
|
+
# The key to delete from the DeepHash.
|
59
|
+
def delete attr
|
60
|
+
attr = convert_key(attr)
|
61
|
+
attr.is_a?(Array) ? deep_delete(*attr) : super(attr)
|
62
|
+
end
|
63
|
+
|
64
|
+
# @return [Hash] converts to a plain hash.
|
65
|
+
def to_hash
|
66
|
+
Hash.new(default).merge(self)
|
67
|
+
end
|
68
|
+
|
69
|
+
# @param hash<Hash> The hash to merge with the deep_hash.
|
70
|
+
#
|
71
|
+
# @return [DeepHash] A new deep_hash with the hash values merged in.
|
72
|
+
def merge(hash, &block)
|
73
|
+
self.dup.update(hash, &block)
|
74
|
+
end
|
75
|
+
|
76
|
+
alias_method :merge!, :update
|
77
|
+
|
78
|
+
# @param other_hash<Hash>
|
79
|
+
# A hash to update values in the deep_hash with. The keys and the values will be
|
80
|
+
# converted to DeepHash format.
|
81
|
+
#
|
82
|
+
# @return [DeepHash] The updated deep_hash.
|
83
|
+
def update(other_hash, &block)
|
84
|
+
deep_hash = self.class.new
|
85
|
+
other_hash.each_pair do |key, value|
|
86
|
+
val = convert_value(value)
|
87
|
+
deep_hash[key] = val
|
88
|
+
end
|
89
|
+
regular_update(deep_hash, &block)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Return a new hash with all keys converted to symbols, as long as
|
93
|
+
# they respond to +to_sym+.
|
94
|
+
def symbolize_keys
|
95
|
+
dup.symbolize_keys!
|
96
|
+
end unless method_defined?(:symbolize_keys)
|
97
|
+
|
98
|
+
# Used to provide the same interface as Hash.
|
99
|
+
#
|
100
|
+
# @return [DeepHash] This deep_hash unchanged.
|
101
|
+
def symbolize_keys!; self end
|
102
|
+
|
103
|
+
#
|
104
|
+
# remove all key-value pairs where the value is nil
|
105
|
+
#
|
106
|
+
def compact
|
107
|
+
reject{|key,val| val.nil? }
|
108
|
+
end
|
109
|
+
#
|
110
|
+
# Replace the hash with its compacted self
|
111
|
+
#
|
112
|
+
def compact!
|
113
|
+
replace(compact)
|
114
|
+
end
|
115
|
+
# Slice a hash to include only the given keys. This is useful for
|
116
|
+
# limiting an options hash to valid keys before passing to a method:
|
117
|
+
#
|
118
|
+
# def search(criteria = {})
|
119
|
+
# assert_valid_keys(:mass, :velocity, :time)
|
120
|
+
# end
|
121
|
+
#
|
122
|
+
# search(options.slice(:mass, :velocity, :time))
|
123
|
+
#
|
124
|
+
# If you have an array of keys you want to limit to, you should splat them:
|
125
|
+
#
|
126
|
+
# valid_keys = [:mass, :velocity, :time]
|
127
|
+
# search(options.slice(*valid_keys))
|
128
|
+
def slice(*keys)
|
129
|
+
keys = keys.map!{|key| convert_key(key) } if respond_to?(:convert_key)
|
130
|
+
hash = self.class.new
|
131
|
+
keys.each { |k| hash[k] = self[k] if has_key?(k) }
|
132
|
+
hash
|
133
|
+
end unless method_defined?(:slice)
|
134
|
+
|
135
|
+
# Replaces the hash with only the given keys.
|
136
|
+
# Returns a hash containing the removed key/value pairs
|
137
|
+
# @example
|
138
|
+
# hsh = {:a => 1, :b => 2, :c => 3, :d => 4}
|
139
|
+
# hsh.slice!(:a, :b)
|
140
|
+
# # => {:c => 3, :d =>4}
|
141
|
+
# hsh
|
142
|
+
# # => {:a => 1, :b => 2}
|
143
|
+
def slice!(*keys)
|
144
|
+
keys = keys.map!{|key| convert_key(key) } if respond_to?(:convert_key)
|
145
|
+
omit = slice(*self.keys - keys)
|
146
|
+
hash = slice(*keys)
|
147
|
+
replace(hash)
|
148
|
+
omit
|
149
|
+
end unless method_defined?(:slice!)
|
150
|
+
|
151
|
+
# Removes the given keys from the hash
|
152
|
+
# Returns a hash containing the removed key/value pairs
|
153
|
+
#
|
154
|
+
# @example
|
155
|
+
# hsh = {:a => 1, :b => 2, :c => 3, :d => 4}
|
156
|
+
# hsh.extract!(:a, :b)
|
157
|
+
# # => {:a => 1, :b => 2}
|
158
|
+
# hsh
|
159
|
+
# # => {:c => 3, :d =>4}
|
160
|
+
def extract!(*keys)
|
161
|
+
result = self.class.new
|
162
|
+
keys.each{|key| result[key] = delete(key) }
|
163
|
+
result
|
164
|
+
end unless method_defined?(:extract!)
|
165
|
+
|
166
|
+
# Allows for reverse merging two hashes where the keys in the calling hash take precedence over those
|
167
|
+
# in the <tt>other_hash</tt>. This is particularly useful for initializing an option hash with default values:
|
168
|
+
#
|
169
|
+
# def setup(options = {})
|
170
|
+
# options.reverse_merge! :size => 25, :velocity => 10
|
171
|
+
# end
|
172
|
+
#
|
173
|
+
# Using <tt>merge</tt>, the above example would look as follows:
|
174
|
+
#
|
175
|
+
# def setup(options = {})
|
176
|
+
# { :size => 25, :velocity => 10 }.merge(options)
|
177
|
+
# end
|
178
|
+
#
|
179
|
+
# The default <tt>:size</tt> and <tt>:velocity</tt> are only set if the +options+ hash passed in doesn't already
|
180
|
+
# have the respective key.
|
181
|
+
def reverse_merge(other_hash)
|
182
|
+
other_hash.merge(self)
|
183
|
+
end unless method_defined?(:reverse_merge)
|
184
|
+
|
185
|
+
# Performs the opposite of <tt>merge</tt>, with the keys and values from the first hash taking precedence over the second.
|
186
|
+
# Modifies the receiver in place.
|
187
|
+
def reverse_merge!(other_hash)
|
188
|
+
merge!( other_hash ){|k,o,n| o }
|
189
|
+
end unless method_defined?(:reverse_merge!)
|
190
|
+
|
191
|
+
# Validate all keys in a hash match *valid keys, raising ArgumentError on a mismatch.
|
192
|
+
# Note that keys are NOT treated indifferently, meaning if you use strings for keys but assert symbols
|
193
|
+
# as keys, this will fail.
|
194
|
+
#
|
195
|
+
# ==== Examples
|
196
|
+
# { :name => "Rob", :years => "28" }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key(s): years"
|
197
|
+
# { :name => "Rob", :age => "28" }.assert_valid_keys("name", "age") # => raises "ArgumentError: Unknown key(s): name, age"
|
198
|
+
# { :name => "Rob", :age => "28" }.assert_valid_keys(:name, :age) # => passes, raises nothing
|
199
|
+
def assert_valid_keys(*valid_keys)
|
200
|
+
unknown_keys = keys - [valid_keys].flatten
|
201
|
+
raise(ArgumentError, "Unknown key(s): #{unknown_keys.join(", ")}") unless unknown_keys.empty?
|
202
|
+
end unless method_defined?(:assert_valid_keys)
|
203
|
+
|
204
|
+
# Sets a member value.
|
205
|
+
#
|
206
|
+
# Given a deep key (one that contains '.'), uses it as a chain of hash
|
207
|
+
# memberships. Otherwise calls the normal hash member setter
|
208
|
+
#
|
209
|
+
# @example
|
210
|
+
# foo = DeepHash.new :hi => 'there'
|
211
|
+
# foo['howdy.doody'] = 3
|
212
|
+
# foo # => { :hi => 'there', :howdy => { :doody => 3 } }
|
213
|
+
#
|
214
|
+
def []= attr, val
|
215
|
+
attr = convert_key(attr)
|
216
|
+
val = convert_value(val)
|
217
|
+
attr.is_a?(Array) ? deep_set(*(attr | [val])) : super(attr, val)
|
218
|
+
end
|
219
|
+
|
220
|
+
|
221
|
+
# Gets a member value.
|
222
|
+
#
|
223
|
+
# Given a deep key (one that contains '.'), uses it as a chain of hash
|
224
|
+
# memberships. Otherwise calls the normal hash member getter
|
225
|
+
#
|
226
|
+
# @example
|
227
|
+
# foo = DeepHash.new({ :hi => 'there', :howdy => { :doody => 3 } })
|
228
|
+
# foo['howdy.doody'] # => 3
|
229
|
+
# foo['hi'] # => 'there'
|
230
|
+
# foo[:hi] # => 'there'
|
231
|
+
#
|
232
|
+
def [] attr
|
233
|
+
attr = convert_key(attr)
|
234
|
+
raise if (attr == [:made])
|
235
|
+
attr.is_a?(Array) ? deep_get(*attr) : super(attr)
|
236
|
+
end
|
237
|
+
|
238
|
+
# lambda for recursive merges
|
239
|
+
::DeepHash::DEEP_MERGER = proc do |key,v1,v2|
|
240
|
+
if (v1.respond_to?(:update) && v2.respond_to?(:update))
|
241
|
+
v1.update(v2.reject{|kk,vv| vv.nil? }, &DeepHash::DEEP_MERGER)
|
242
|
+
elsif v2.nil?
|
243
|
+
v1
|
244
|
+
else
|
245
|
+
v2
|
246
|
+
end
|
247
|
+
end unless defined?(::DeepHash::DEEP_MERGER)
|
248
|
+
|
249
|
+
#
|
250
|
+
# Merge hashes recursively.
|
251
|
+
# Nothing special happens to array values
|
252
|
+
#
|
253
|
+
# x = { :subhash => { 1 => :val_from_x, 222 => :only_in_x, 333 => :only_in_x }, :scalar => :scalar_from_x}
|
254
|
+
# y = { :subhash => { 1 => :val_from_y, 999 => :only_in_y }, :scalar => :scalar_from_y }
|
255
|
+
# x.deep_merge y
|
256
|
+
# => {:subhash=>{1=>:val_from_y, 222=>:only_in_x, 333=>:only_in_x, 999=>:only_in_y}, :scalar=>:scalar_from_y}
|
257
|
+
# y.deep_merge x
|
258
|
+
# => {:subhash=>{1=>:val_from_x, 222=>:only_in_x, 333=>:only_in_x, 999=>:only_in_y}, :scalar=>:scalar_from_x}
|
259
|
+
#
|
260
|
+
# Nil values always lose.
|
261
|
+
#
|
262
|
+
# x = {:subhash=>{:nil_in_x=>nil, 1=>:val1,}, :nil_in_x=>nil}
|
263
|
+
# y = {:subhash=>{:nil_in_x=>5}, :nil_in_x=>5}
|
264
|
+
# y.deep_merge x
|
265
|
+
# => {:subhash=>{1=>:val1, :nil_in_x=>5}, :nil_in_x=>5}
|
266
|
+
# x.deep_merge y
|
267
|
+
# => {:subhash=>{1=>:val1, :nil_in_x=>5}, :nil_in_x=>5}
|
268
|
+
#
|
269
|
+
def deep_merge hsh2
|
270
|
+
merge hsh2, &DeepHash::DEEP_MERGER
|
271
|
+
end
|
272
|
+
|
273
|
+
def deep_merge! hsh2
|
274
|
+
update hsh2, &DeepHash::DEEP_MERGER
|
275
|
+
self
|
276
|
+
end
|
277
|
+
|
278
|
+
#
|
279
|
+
# Treat hash as tree of hashes:
|
280
|
+
#
|
281
|
+
# x = { 1 => :val, :subhash => { 1 => :val1 } }
|
282
|
+
# x.deep_set(:subhash, :cat, :hat)
|
283
|
+
# # => { 1 => :val, :subhash => { 1 => :val1, :cat => :hat } }
|
284
|
+
# x.deep_set(:subhash, 1, :newval)
|
285
|
+
# # => { 1 => :val, :subhash => { 1 => :newval, :cat => :hat } }
|
286
|
+
#
|
287
|
+
#
|
288
|
+
def deep_set *args
|
289
|
+
val = args.pop
|
290
|
+
last_key = args.pop
|
291
|
+
# dig down to last subtree (building out if necessary)
|
292
|
+
hsh = self
|
293
|
+
args.each do |key|
|
294
|
+
hsh.regular_writer(key, self.class.new) unless hsh.has_key?(key)
|
295
|
+
hsh = hsh[key]
|
296
|
+
end
|
297
|
+
# set leaf value
|
298
|
+
hsh[last_key] = val
|
299
|
+
end
|
300
|
+
|
301
|
+
#
|
302
|
+
# Treat hash as tree of hashes:
|
303
|
+
#
|
304
|
+
# x = { 1 => :val, :subhash => { 1 => :val1 } }
|
305
|
+
# x.deep_get(:subhash, 1)
|
306
|
+
# # => :val
|
307
|
+
# x.deep_get(:subhash, 2)
|
308
|
+
# # => nil
|
309
|
+
# x.deep_get(:subhash, 2, 3)
|
310
|
+
# # => nil
|
311
|
+
# x.deep_get(:subhash, 2)
|
312
|
+
# # => nil
|
313
|
+
#
|
314
|
+
def deep_get *args
|
315
|
+
last_key = args.pop
|
316
|
+
# dig down to last subtree (building out if necessary)
|
317
|
+
hsh = args.inject(self){|h, k| h[k] || self.class.new }
|
318
|
+
# get leaf value
|
319
|
+
hsh[last_key]
|
320
|
+
end
|
321
|
+
|
322
|
+
#
|
323
|
+
# Treat hash as tree of hashes:
|
324
|
+
#
|
325
|
+
# x = { 1 => :val, :subhash => { 1 => :val1, 2 => :val2 } }
|
326
|
+
# x.deep_delete(:subhash, 1)
|
327
|
+
# #=> :val
|
328
|
+
# x
|
329
|
+
# #=> { 1 => :val, :subhash => { 2 => :val2 } }
|
330
|
+
#
|
331
|
+
def deep_delete *args
|
332
|
+
last_key = args.pop
|
333
|
+
last_hsh = args.empty? ? self : (deep_get(*args)||{})
|
334
|
+
last_hsh.delete(last_key)
|
335
|
+
end
|
336
|
+
|
337
|
+
protected
|
338
|
+
# @attr key<Object> The key to convert.
|
339
|
+
#
|
340
|
+
# @attr [Object]
|
341
|
+
# The converted key. A dotted attr ('moon.cheese.type') becomes
|
342
|
+
# an array of sequential keys for deep_set and deep_get
|
343
|
+
#
|
344
|
+
# @private
|
345
|
+
def convert_key(attr)
|
346
|
+
case
|
347
|
+
when attr.to_s.include?('.') then attr.to_s.split(".").map{|sub_attr| sub_attr.to_sym }
|
348
|
+
when attr.is_a?(String) then attr.to_sym
|
349
|
+
else attr
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
|
354
|
+
# @param value<Object> The value to convert.
|
355
|
+
#
|
356
|
+
# @return [Object]
|
357
|
+
# The converted value. A Hash or an Array of hashes, will be converted to
|
358
|
+
# their DeepHash equivalents.
|
359
|
+
#
|
360
|
+
# @private
|
361
|
+
def convert_value(value)
|
362
|
+
if value.class == Hash then self.class.new(value)
|
363
|
+
elsif value.is_a?(Array) then value.collect{|e| convert_value(e) }
|
364
|
+
else value
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
end
|