chef_backup 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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