chef_backup 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +5 -5
  2. data/lib/chef_backup.rb +8 -8
  3. data/lib/chef_backup/config.rb +17 -17
  4. data/lib/chef_backup/data_map.rb +18 -12
  5. data/lib/chef_backup/deep_merge.rb +145 -0
  6. data/lib/chef_backup/helpers.rb +46 -10
  7. data/lib/chef_backup/logger.rb +2 -2
  8. data/lib/chef_backup/mash.rb +226 -0
  9. data/lib/chef_backup/runner.rb +11 -13
  10. data/lib/chef_backup/strategy.rb +10 -10
  11. data/lib/chef_backup/strategy/backup/custom.rb +1 -2
  12. data/lib/chef_backup/strategy/backup/ebs.rb +3 -6
  13. data/lib/chef_backup/strategy/backup/lvm.rb +2 -4
  14. data/lib/chef_backup/strategy/backup/object.rb +2 -4
  15. data/lib/chef_backup/strategy/backup/tar.rb +23 -7
  16. data/lib/chef_backup/strategy/restore/tar.rb +69 -43
  17. data/lib/chef_backup/version.rb +1 -1
  18. metadata +20 -168
  19. data/.gitignore +0 -23
  20. data/.kitchen.yml +0 -30
  21. data/.rubocop.yml +0 -21
  22. data/.travis.yml +0 -6
  23. data/Gemfile +0 -4
  24. data/Guardfile +0 -22
  25. data/README.md +0 -21
  26. data/Rakefile +0 -44
  27. data/chef_backup.gemspec +0 -33
  28. data/spec/fixtures/chef-server-running.json +0 -589
  29. data/spec/spec_helper.rb +0 -98
  30. data/spec/unit/data_map_spec.rb +0 -59
  31. data/spec/unit/helpers_spec.rb +0 -88
  32. data/spec/unit/runner_spec.rb +0 -185
  33. data/spec/unit/shared_examples/helpers.rb +0 -20
  34. data/spec/unit/strategy/backup/lvm_spec.rb +0 -0
  35. data/spec/unit/strategy/backup/shared_examples/backup.rb +0 -92
  36. data/spec/unit/strategy/backup/tar_spec.rb +0 -327
  37. data/spec/unit/strategy/restore/lvm_spec.rb +0 -0
  38. data/spec/unit/strategy/restore/shared_examples/restore.rb +0 -84
  39. data/spec/unit/strategy/restore/tar_spec.rb +0 -255
  40. data/spec/unit/strategy_spec.rb +0 -36
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 3aaeca0a04fa4fe4ee8443b47d5f9295929d201d
4
- data.tar.gz: 82f06bbc30bff3a57c235d98012f0a251498af9c
2
+ SHA256:
3
+ metadata.gz: b4e824870ac3e10eb6ffaa5bedd55483721a1af6b15cb61c7cb58cc2ccbcf7d9
4
+ data.tar.gz: 73be0c4d4911cd7f2f238aa0c9bd93b67a5fe7fdbfeb90294a8bfbffdcc42d7d
5
5
  SHA512:
6
- metadata.gz: 688f33e0046368ac2c35dcf7dce9ae9f474cf5e8d8628651c105af4c709769fe7728e5e4a8fd66d530f0e2ad82577039bd42874ffdb34b4a168b563045460b1f
7
- data.tar.gz: 3c965dbc203fd45876c38d051cc2427ba67e9a8a97c38db7b947f85d2d4e277de620a06dd0fa9a7bf29f04eec327787006e17679e2b6140e84445f36e1e9dac0
6
+ metadata.gz: 718ef11067a9e995f10bef3ccee17ac176b42a9a08fd9c3eebad64856dd5fda41f63c3e75e82d5e22fbb37dff521c0dd8e231436a580ace946366fb7dc9f1bc3
7
+ data.tar.gz: 03d05f2b1fd933cbd1842fc3563392b63f91127697ab17ae066eaf7c19f094c9c14aaeb083b9bbcbd580db0eb6ed0a71a67b20371821466308a2e1103a8fcbfb
@@ -2,11 +2,11 @@
2
2
  #
3
3
  # All Rights Reserved
4
4
 
5
- require 'chef_backup/version'
6
- require 'chef_backup/exceptions'
7
- require 'chef_backup/config'
8
- require 'chef_backup/logger'
9
- require 'chef_backup/data_map'
10
- require 'chef_backup/helpers'
11
- require 'chef_backup/runner'
12
- require 'chef_backup/strategy'
5
+ require "chef_backup/version"
6
+ require "chef_backup/exceptions"
7
+ require "chef_backup/config"
8
+ require "chef_backup/logger"
9
+ require "chef_backup/data_map"
10
+ require "chef_backup/helpers"
11
+ require "chef_backup/runner"
12
+ require "chef_backup/strategy"
@@ -1,23 +1,23 @@
1
- require 'fileutils'
2
- require 'json'
3
- require 'forwardable'
1
+ require "fileutils"
2
+ require "json"
3
+ require "forwardable"
4
4
 
5
5
  module ChefBackup
6
6
  # ChefBackup Global Config
7
7
  class Config
8
8
  extend Forwardable
9
9
 
10
- DEFAULT_BASE = 'private_chef'.freeze
10
+ DEFAULT_BASE = "private_chef".freeze
11
11
  DEFAULT_CONFIG = {
12
- 'backup' => {
13
- 'always_dump_db' => true,
14
- 'strategy' => 'none',
15
- 'export_dir' => '/var/opt/chef-backup',
16
- 'project_name' => 'opscode',
17
- 'ctl-command' => 'chef-server-ctl',
18
- 'running_filepath' => '/etc/opscode/chef-server-running.json',
19
- 'database_name' => 'opscode_chef'
20
- }
12
+ "backup" => {
13
+ "always_dump_db" => true,
14
+ "strategy" => "none",
15
+ "export_dir" => "/var/opt/chef-backup",
16
+ "project_name" => "opscode",
17
+ "ctl-command" => "chef-server-ctl",
18
+ "running_filepath" => "/etc/opscode/chef-server-running.json",
19
+ "database_name" => "opscode_chef",
20
+ },
21
21
  }.freeze
22
22
 
23
23
  class << self
@@ -50,11 +50,11 @@ module ChefBackup
50
50
  # @param config [Hash] a Hash of the private-chef-running.json
51
51
  #
52
52
  def initialize(config = {})
53
- config['config_base'] ||= DEFAULT_BASE
54
- base = config['config_base']
53
+ config["config_base"] ||= DEFAULT_BASE
54
+ base = config["config_base"]
55
55
  config[base] ||= {}
56
- config[base]['backup'] ||= {}
57
- config[base]['backup'] = DEFAULT_CONFIG['backup'].merge(config[base]['backup'])
56
+ config[base]["backup"] ||= {}
57
+ config[base]["backup"] = DEFAULT_CONFIG["backup"].merge(config[base]["backup"])
58
58
  @config = config
59
59
  end
60
60
 
@@ -1,4 +1,4 @@
1
- require 'time'
1
+ require "time"
2
2
 
3
3
  module ChefBackup
4
4
  # DataMap class to store data about the data we're backing up
@@ -11,27 +11,32 @@ module ChefBackup
11
11
  attr_writer :data_map
12
12
  end
13
13
 
14
- attr_accessor :strategy, :backup_time, :topology, :configs, :services, :ha
14
+ attr_accessor :strategy, :backup_time, :topology, :configs, :services, :ha, :versions
15
15
 
16
16
  def initialize
17
17
  @services = {}
18
18
  @configs = {}
19
+ @versions = {}
19
20
  @ha = {}
20
21
  yield self if block_given?
21
22
 
22
23
  @backup_time ||= Time.now.iso8601
23
- @strategy ||= 'none'
24
- @toplogy ||= 'idontknow'
24
+ @strategy ||= "none"
25
+ @toplogy ||= "idontknow"
25
26
  end
26
27
 
27
28
  def add_service(service, data_dir)
28
29
  @services[service] ||= {}
29
- @services[service]['data_dir'] = data_dir
30
+ @services[service]["data_dir"] = data_dir
30
31
  end
31
32
 
32
33
  def add_config(config, path)
33
34
  @configs[config] ||= {}
34
- @configs[config]['data_dir'] = path
35
+ @configs[config]["data_dir"] = path
36
+ end
37
+
38
+ def add_version(project_name, data)
39
+ @versions[project_name] = data
35
40
  end
36
41
 
37
42
  def add_ha_info(k, v)
@@ -40,12 +45,13 @@ module ChefBackup
40
45
 
41
46
  def manifest
42
47
  {
43
- 'strategy' => strategy,
44
- 'backup_time' => backup_time,
45
- 'topology' => topology,
46
- 'ha' => ha,
47
- 'services' => services,
48
- 'configs' => configs
48
+ "strategy" => strategy,
49
+ "backup_time" => backup_time,
50
+ "topology" => topology,
51
+ "ha" => ha,
52
+ "services" => services,
53
+ "configs" => configs,
54
+ "versions" => versions,
49
55
  }
50
56
  end
51
57
  end
@@ -0,0 +1,145 @@
1
+ #
2
+ # Author:: Adam Jacob (<adam@chef.io>)
3
+ # Author:: Steve Midgley (http://www.misuse.org/science)
4
+ # Copyright:: Copyright 2009-2016, Chef Software Inc.
5
+ # Copyright:: Copyright 2008-2016, Steve Midgley
6
+ # License:: Apache License, Version 2.0
7
+ #
8
+ # Licensed under the Apache License, Version 2.0 (the "License");
9
+ # you may not use this file except in compliance with the License.
10
+ # You may obtain a copy of the License at
11
+ #
12
+ # http://www.apache.org/licenses/LICENSE-2.0
13
+ #
14
+ # Unless required by applicable law or agreed to in writing, software
15
+ # distributed under the License is distributed on an "AS IS" BASIS,
16
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ # See the License for the specific language governing permissions and
18
+ # limitations under the License.
19
+ require 'chef_backup/mash'
20
+
21
+ #
22
+ # DANGER! THIS FILE WAS VENDORDED FROM CHEF. IF YOU ARE
23
+ # MAKING A CHANGE HERE, CONSIDER WHETHER IT IS REQUIRED IN CHEF ALSO?
24
+ #
25
+ module ChefBackup
26
+ module Mixin
27
+ # == Chef::Mixin::DeepMerge
28
+ # Implements a deep merging algorithm for nested data structures.
29
+ # ==== Notice:
30
+ # This code was originally imported from deep_merge by Steve Midgley.
31
+ # deep_merge is available under the MIT license from
32
+ # http://trac.misuse.org/science/wiki/DeepMerge
33
+ module DeepMerge
34
+
35
+ extend self
36
+
37
+ def merge(first, second)
38
+ first = Mash.new(first) unless first.kind_of?(Mash)
39
+ second = Mash.new(second) unless second.kind_of?(Mash)
40
+
41
+ DeepMerge.deep_merge(second, first)
42
+ end
43
+
44
+ class InvalidParameter < StandardError; end
45
+
46
+ # Deep Merge core documentation.
47
+ # deep_merge! method permits merging of arbitrary child elements. The two top level
48
+ # elements must be hashes. These hashes can contain unlimited (to stack limit) levels
49
+ # of child elements. These child elements to not have to be of the same types.
50
+ # Where child elements are of the same type, deep_merge will attempt to merge them together.
51
+ # Where child elements are not of the same type, deep_merge will skip or optionally overwrite
52
+ # the destination element with the contents of the source element at that level.
53
+ # So if you have two hashes like this:
54
+ # source = {:x => [1,2,3], :y => 2}
55
+ # dest = {:x => [4,5,'6'], :y => [7,8,9]}
56
+ # dest.deep_merge!(source)
57
+ # Results: {:x => [1,2,3,4,5,'6'], :y => 2}
58
+ # By default, "deep_merge!" will overwrite any unmergeables and merge everything else.
59
+ # To avoid this, use "deep_merge" (no bang/exclamation mark)
60
+ def deep_merge!(source, dest)
61
+ # if dest doesn't exist, then simply copy source to it
62
+ if dest.nil?
63
+ dest = source; return dest
64
+ end
65
+
66
+ case source
67
+ when nil
68
+ dest
69
+ when Hash
70
+ if dest.kind_of?(Hash)
71
+ source.each do |src_key, src_value|
72
+ if dest[src_key]
73
+ dest[src_key] = deep_merge!(src_value, dest[src_key])
74
+ else # dest[src_key] doesn't exist so we take whatever source has
75
+ dest[src_key] = src_value
76
+ end
77
+ end
78
+ else # dest isn't a hash, so we overwrite it completely
79
+ dest = source
80
+ end
81
+ when Array
82
+ if dest.kind_of?(Array)
83
+ dest = dest | source
84
+ else
85
+ dest = source
86
+ end
87
+ when String
88
+ dest = source
89
+ else # src_hash is not an array or hash, so we'll have to overwrite dest
90
+ dest = source
91
+ end
92
+ dest
93
+ end # deep_merge!
94
+
95
+ def hash_only_merge(merge_onto, merge_with)
96
+ hash_only_merge!(safe_dup(merge_onto), safe_dup(merge_with))
97
+ end
98
+
99
+ def safe_dup(thing)
100
+ thing.dup
101
+ rescue TypeError
102
+ thing
103
+ end
104
+
105
+ # Deep merge without Array merge.
106
+ # `merge_onto` is the object that will "lose" in case of conflict.
107
+ # `merge_with` is the object whose values will replace `merge_onto`s
108
+ # values when there is a conflict.
109
+ def hash_only_merge!(merge_onto, merge_with)
110
+ # If there are two Hashes, recursively merge.
111
+ if merge_onto.kind_of?(Hash) && merge_with.kind_of?(Hash)
112
+ merge_with.each do |key, merge_with_value|
113
+ value =
114
+ if merge_onto.has_key?(key)
115
+ hash_only_merge(merge_onto[key], merge_with_value)
116
+ else
117
+ merge_with_value
118
+ end
119
+
120
+ if merge_onto.respond_to?(:public_method_that_only_deep_merge_should_use)
121
+ # we can't call ImmutableMash#[]= because its immutable, but we need to mutate it to build it in-place
122
+ merge_onto.public_method_that_only_deep_merge_should_use(key, value)
123
+ else
124
+ merge_onto[key] = value
125
+ end
126
+ end
127
+ merge_onto
128
+
129
+ # If merge_with is nil, don't replace merge_onto
130
+ elsif merge_with.nil?
131
+ merge_onto
132
+
133
+ # In all other cases, replace merge_onto with merge_with
134
+ else
135
+ merge_with
136
+ end
137
+ end
138
+
139
+ def deep_merge(source, dest)
140
+ deep_merge!(safe_dup(source), safe_dup(dest))
141
+ end
142
+
143
+ end
144
+ end
145
+ end
@@ -1,8 +1,10 @@
1
1
  require 'fileutils'
2
+ require 'json'
2
3
  require 'mixlib/shellout'
3
4
  require 'chef_backup/config'
4
5
  require 'chef_backup/logger'
5
6
 
7
+ # rubocop:disable ModuleLength
6
8
  # rubocop:disable IndentationWidth
7
9
  module ChefBackup
8
10
  # Common helper methods that are usefull in many classes
@@ -27,7 +29,7 @@ module Helpers
27
29
  'ctl_command' => 'opscode-analytics-ctl'
28
30
  },
29
31
  'chef-ha' => {
30
- 'config_file' => 'etc/opscode/chef-server.rb'
32
+ 'config_file' => '/etc/opscode/chef-server.rb'
31
33
  },
32
34
  'chef-sync' => {
33
35
  'config_file' => '/etc/chef-sync/chef-sync.rb',
@@ -69,6 +71,19 @@ module Helpers
69
71
  ChefBackup::Logger.logger.log(message, level)
70
72
  end
71
73
 
74
+ # Note that when we are in the backup codepath, we have access to a running
75
+ # chef server and hence, the ctl command puts all our flags under the current
76
+ # running service namespace. The lets the default configuration of the server
77
+ # provide flags that the user doesn't necessarily provide on the command line.
78
+ #
79
+ # During the restore codepath, there may be no running chef server. This means
80
+ # that we need to be paranoid about the existence of the service_config hash.
81
+ def shell_timeout
82
+ option = config['shell_out_timeout'] ||
83
+ (service_config && service_config['backup']['shell_out_timeout'])
84
+ option.to_f unless option.nil?
85
+ end
86
+
72
87
  #
73
88
  # @param file [String] A path to a file on disk
74
89
  # @param exception [Exception] An exception to raise if file is not present
@@ -81,7 +96,9 @@ module Helpers
81
96
  end
82
97
 
83
98
  def shell_out(*command)
84
- cmd = Mixlib::ShellOut.new(*command)
99
+ options = command.last.is_a?(Hash) ? command.pop : {}
100
+ opts_with_defaults = { 'timeout' => shell_timeout }.merge(options)
101
+ cmd = Mixlib::ShellOut.new(*command, opts_with_defaults)
85
102
  cmd.live_stream ||= $stdout.tty? ? $stdout : nil
86
103
  cmd.run_command
87
104
  cmd
@@ -101,6 +118,11 @@ module Helpers
101
118
  "/opt/#{project_name}"
102
119
  end
103
120
 
121
+ def addon_install_dir(name)
122
+ # can use extra field in SERVER_ADD_ONS to extend if someone isn't following this pattern.
123
+ "/opt/#{name}"
124
+ end
125
+
104
126
  def base_config_dir
105
127
  "/etc/#{project_name}"
106
128
  end
@@ -114,7 +136,9 @@ module Helpers
114
136
  end
115
137
 
116
138
  def pg_options
117
- config['pg_options'] || DEFAULT_PG_OPTIONS
139
+ config['pg_options'] ||
140
+ (service_config && service_config['backup']['pg_options']) ||
141
+ DEFAULT_PG_OPTIONS
118
142
  end
119
143
 
120
144
  def all_services
@@ -126,7 +150,7 @@ module Helpers
126
150
  end
127
151
 
128
152
  def disabled_services
129
- all_services.select { |sv| !service_enabled?(sv) }
153
+ all_services.reject { |sv| service_enabled?(sv) }
130
154
  end
131
155
 
132
156
  def service_enabled?(service)
@@ -177,12 +201,10 @@ module Helpers
177
201
  end
178
202
 
179
203
  def enabled_addons
180
- SERVER_ADD_ONS.select do |_name, config|
181
- begin
182
- File.directory?(File.dirname(config['config_file']))
183
- rescue
184
- false
185
- end
204
+ SERVER_ADD_ONS.select do |name, config|
205
+ !config['config_file'].nil? &&
206
+ File.directory?(File.dirname(config['config_file'])) &&
207
+ File.directory?(addon_install_dir(name))
186
208
  end
187
209
  end
188
210
 
@@ -242,6 +264,20 @@ module Helpers
242
264
  true
243
265
  end
244
266
 
267
+ def version_from_manifest_file(file)
268
+ return :no_version if file.nil?
269
+
270
+ path = File.expand_path(file)
271
+ if File.exist?(path)
272
+ config = JSON.parse(File.read(path))
273
+ { 'version' => config['build_version'],
274
+ 'revision' => config['build_git_revision'],
275
+ 'path' => path }
276
+ else
277
+ :no_version
278
+ end
279
+ end
280
+
245
281
  private
246
282
 
247
283
  def safe_key
@@ -1,4 +1,4 @@
1
- require 'highline'
1
+ require "highline"
2
2
 
3
3
  module ChefBackup
4
4
  # Basic Logging Class
@@ -15,7 +15,7 @@ module ChefBackup
15
15
  attr_accessor :stdout
16
16
 
17
17
  def initialize(logfile = nil)
18
- $stdout = logfile ? File.open(logfile, 'ab') : $stdout
18
+ $stdout = logfile ? File.open(logfile, "ab") : $stdout
19
19
  @highline = HighLine.new($stdin, $stdout)
20
20
  end
21
21
 
@@ -0,0 +1,226 @@
1
+ # Copyright 2009-2016, Dan Kubb
2
+
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ # ---
23
+ # ---
24
+
25
+ # Some portions of blank.rb and mash.rb are verbatim copies of software
26
+ # licensed under the MIT license. That license is included below:
27
+
28
+ # Copyright 2005-2016, David Heinemeier Hansson
29
+
30
+ # Permission is hereby granted, free of charge, to any person obtaining
31
+ # a copy of this software and associated documentation files (the
32
+ # "Software"), to deal in the Software without restriction, including
33
+ # without limitation the rights to use, copy, modify, merge, publish,
34
+ # distribute, sublicense, and/or sell copies of the Software, and to
35
+ # permit persons to whom the Software is furnished to do so, subject to
36
+ # the following conditions:
37
+
38
+ # The above copyright notice and this permission notice shall be
39
+ # included in all copies or substantial portions of the Software.
40
+
41
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
42
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
43
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
44
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
45
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
46
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
47
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
48
+
49
+ # This class has dubious semantics and we only have it so that people can write
50
+ # params[:key] instead of params['key'].
51
+ class Mash < Hash
52
+
53
+ # @param constructor<Object>
54
+ # The default value for the mash. Defaults to an empty hash.
55
+ #
56
+ # @details [Alternatives]
57
+ # If constructor is a Hash, a new mash will be created based on the keys of
58
+ # the hash and no default value will be set.
59
+ def initialize(constructor = {})
60
+ if constructor.is_a?(Hash)
61
+ super()
62
+ update(constructor)
63
+ else
64
+ super(constructor)
65
+ end
66
+ end
67
+
68
+ # @param orig<Object> Mash being copied
69
+ #
70
+ # @return [Object] A new copied Mash
71
+ def initialize_copy(orig)
72
+ super
73
+ # Handle nested values
74
+ each do |k, v|
75
+ if v.kind_of?(Mash) || v.is_a?(Array)
76
+ self[k] = v.dup
77
+ end
78
+ end
79
+ self
80
+ end
81
+
82
+ # @param key<Object> The default value for the mash. Defaults to nil.
83
+ #
84
+ # @details [Alternatives]
85
+ # If key is a Symbol and it is a key in the mash, then the default value will
86
+ # be set to the value matching the key.
87
+ def default(key = nil)
88
+ if key.is_a?(Symbol) && include?(key = key.to_s)
89
+ self[key]
90
+ else
91
+ super
92
+ end
93
+ end
94
+
95
+ alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
96
+ alias_method :regular_update, :update unless method_defined?(:regular_update)
97
+
98
+ # @param key<Object> The key to set.
99
+ # @param value<Object>
100
+ # The value to set the key to.
101
+ #
102
+ # @see Mash#convert_key
103
+ # @see Mash#convert_value
104
+ def []=(key, value)
105
+ regular_writer(convert_key(key), convert_value(value))
106
+ end
107
+
108
+ # @param other_hash<Hash>
109
+ # A hash to update values in the mash with. The keys and the values will be
110
+ # converted to Mash format.
111
+ #
112
+ # @return [Mash] The updated mash.
113
+ def update(other_hash)
114
+ other_hash.each_pair { |key, value| regular_writer(convert_key(key), convert_value(value)) }
115
+ self
116
+ end
117
+
118
+ alias_method :merge!, :update
119
+
120
+ # @param key<Object> The key to check for. This will be run through convert_key.
121
+ #
122
+ # @return [Boolean] True if the key exists in the mash.
123
+ def key?(key)
124
+ super(convert_key(key))
125
+ end
126
+
127
+ # def include? def has_key? def member?
128
+ alias_method :include?, :key?
129
+ alias_method :has_key?, :key?
130
+ alias_method :member?, :key?
131
+
132
+ # @param key<Object> The key to fetch. This will be run through convert_key.
133
+ # @param *extras<Array> Default value.
134
+ #
135
+ # @return [Object] The value at key or the default value.
136
+ def fetch(key, *extras)
137
+ super(convert_key(key), *extras)
138
+ end
139
+
140
+ # @param *indices<Array>
141
+ # The keys to retrieve values for. These will be run through +convert_key+.
142
+ #
143
+ # @return [Array] The values at each of the provided keys
144
+ def values_at(*indices)
145
+ indices.collect { |key| self[convert_key(key)] }
146
+ end
147
+
148
+ # @param hash<Hash> The hash to merge with the mash.
149
+ #
150
+ # @return [Mash] A new mash with the hash values merged in.
151
+ def merge(hash)
152
+ self.dup.update(hash)
153
+ end
154
+
155
+ # @param key<Object>
156
+ # The key to delete from the mash.\
157
+ def delete(key)
158
+ super(convert_key(key))
159
+ end
160
+
161
+ # @param *rejected<Array[(String, Symbol)] The mash keys to exclude.
162
+ #
163
+ # @return [Mash] A new mash without the selected keys.
164
+ #
165
+ # @example
166
+ # { :one => 1, :two => 2, :three => 3 }.except(:one)
167
+ # #=> { "two" => 2, "three" => 3 }
168
+ def except(*keys)
169
+ super(*keys.map { |k| convert_key(k) })
170
+ end
171
+
172
+ # Used to provide the same interface as Hash.
173
+ #
174
+ # @return [Mash] This mash unchanged.
175
+ def stringify_keys!; self end
176
+
177
+ # @return [Hash] The mash as a Hash with symbolized keys.
178
+ def symbolize_keys
179
+ h = Hash.new(default)
180
+ each { |key, val| h[key.to_sym] = val }
181
+ h
182
+ end
183
+
184
+ # @return [Hash] The mash as a Hash with string keys.
185
+ def to_hash
186
+ Hash.new(default).merge(self)
187
+ end
188
+
189
+ # @return [Mash] Convert a Hash into a Mash
190
+ # The input Hash's default value is maintained
191
+ def self.from_hash(hash)
192
+ mash = Mash.new(hash)
193
+ mash.default = hash.default
194
+ mash
195
+ end
196
+
197
+ protected
198
+
199
+ # @param key<Object> The key to convert.
200
+ #
201
+ # @param [Object]
202
+ # The converted key. If the key was a symbol, it will be converted to a
203
+ # string.
204
+ #
205
+ # @api private
206
+ def convert_key(key)
207
+ key.kind_of?(Symbol) ? key.to_s : key
208
+ end
209
+
210
+ # @param value<Object> The value to convert.
211
+ #
212
+ # @return [Object]
213
+ # The converted value. A Hash or an Array of hashes, will be converted to
214
+ # their Mash equivalents.
215
+ #
216
+ # @api private
217
+ def convert_value(value)
218
+ if value.class == Hash
219
+ Mash.from_hash(value)
220
+ elsif value.is_a?(Array)
221
+ value.collect { |e| convert_value(e) }
222
+ else
223
+ value
224
+ end
225
+ end
226
+ end