dockerize 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.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ Njg4NjU1MjBhNDkyMmFlMzMwYjhiODIzNDM3MzEwY2RiMzM2NDg2Mg==
5
+ data.tar.gz: !binary |-
6
+ Nzg0ZjUwNmE4YWUwZWY0MmFjYjczZDlkZmI2NTQwYzZkNGUwOTczMQ==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ ZTg2OTdjMGJiZDJmYWRlYWJmNTMyYTgzNjczNjU0YjcyMzJmZTNlNzY0YmY2
10
+ MTVkMjVlZDRjMGQwN2EzNGU4YTNhMDIxZWNiZmVhZDdmMjY0YWY2ZTJiZTZl
11
+ YmVlMTViOWU0NThlNmMxOTkxMjczYjYyZjFhZGEzYTE0YzYyZDE=
12
+ data.tar.gz: !binary |-
13
+ ZTVmMTQ3ZmFhNGM5YWFjZjE2MzNmMGNkZWJmM2IyNjlkNzJkMGUzOTJhYjg1
14
+ OTE2YTdkYjY4MTU5YjExYzhiMGNiYzc1YzdlNjA0ZGNhODcwYWVmMzU3N2I5
15
+ MmFmNjRkZWU0Y2M2ODgxYTQxYTU1NDIxNWIyNjc2YTVhN2I5Mjc=
data/.gitignore ADDED
@@ -0,0 +1,20 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ coverage
6
+ InstalledFiles
7
+ lib/bundler/man
8
+ pkg
9
+ rdoc
10
+ spec/reports
11
+ test/tmp
12
+ test/version_tmp
13
+ tmp
14
+ Gemfile.lock
15
+
16
+ # YARD artifacts
17
+ .yardoc
18
+ _yardoc
19
+ doc/
20
+ .ruby-version
data/.jrubyrc ADDED
@@ -0,0 +1,5 @@
1
+ compat.version=1.9
2
+ ctext.enabled=false
3
+ errno.backtrace=true
4
+
5
+ # vim:filetype=jproperties
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --require spec_helper
3
+ --require English
data/.rubocop.yml ADDED
@@ -0,0 +1,10 @@
1
+ ---
2
+ AllCops:
3
+ Excludes:
4
+ - vendor/**
5
+
6
+ Documentation:
7
+ Enabled: false
8
+
9
+ GlobalVars:
10
+ Enabled: false
data/.simplecov ADDED
@@ -0,0 +1,6 @@
1
+ # vim:fileencoding=utf-8
2
+ if ENV['COVERAGE']
3
+ SimpleCov.start do
4
+ add_filter '/spec/'
5
+ end
6
+ end
data/.travis.yml ADDED
@@ -0,0 +1,13 @@
1
+ ---
2
+ language: ruby
3
+ matrix:
4
+ allow_failures:
5
+ - rvm: jruby-19mode
6
+ rvm:
7
+ - 1.9.3
8
+ - 2.0.0
9
+ - jruby-19mode
10
+ notifications:
11
+ email:
12
+ recipients:
13
+ - r.colton@modcloth.com
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in dockerize.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Rafe Colton
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # Dockerize
2
+
3
+ [![Build Status](https://travis-ci.org/modcloth-labs/dockerize.png?branch=master)](https://travis-ci.org/modcloth-labs/dockerize)
4
+
5
+ Dockerizes your project.
6
+
7
+ ## About
8
+
9
+ Dockerize helps 'dockerize' your application by providing you an easy
10
+ way to add Docker integration to your project. Once a project has been
11
+ 'dockerized,' you will have some useful files in place that will guide
12
+ you through the process of deploying your application with Docker.
13
+
14
+ In particular, `dockerize` creates the `.run` directory in your project.
15
+ Use this directory to configure important files that need to be placed
16
+ on the host machine *outside* your container at deploy time. These
17
+ files might include an [Upstart](http://upstart.ubuntu.com/cookbook/)
18
+ config file, for example. Follow the comments in the files for more
19
+ details
20
+
21
+ ## Installation
22
+
23
+ Install with:
24
+
25
+ ```bash
26
+ > gem install dockerize
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ The simplest use case is dockerizing the current project. Example
32
+
33
+ ```bash
34
+ > dockerize .
35
+ ```
36
+
37
+ Dockerize is also very configurable, and allows many options to be set
38
+ through the command line.
39
+
40
+ To see what options are available, run `dockerize` with the help flag:
41
+
42
+ ```bash
43
+ > dockerize --help
44
+
45
+ # Usage: dockerize <project directory> [options]
46
+ # Options:
47
+ # --quiet, -q: Silence output
48
+ # --dry-run, -d: Dry run, do not write any files
49
+ # --force, -f: Force existing files to be overwritten
50
+ # --backup, --no-backup, -b: Creates .bak version of files before overwriting them
51
+ # --registry, -r <s>: The Docker registry to use when writing files
52
+ # --template-dir, -t <s>: The directory containing the templates to be written
53
+ # --maintainer, -m <s>: The default MAINTAINER to use for any Dockerfiles written
54
+ # --from, -F <s>: The default base image to use for any Dockerfiles written
55
+ # --version, -v: Print version and exit
56
+ # --help, -h: Show this message
57
+ ```
58
+
59
+ If you want to use `dockerize` to dockerize multiple projects, it may be
60
+ useful to set some defaults in the environment. Options currently
61
+ configurable in the environment include:
62
+
63
+ * default registry - `DOCKERIZE_REGISTRY`
64
+ * template dir - `DOCKERIZE_TEMPLATE_DIR`
65
+ * default maintainer - `DOCKERIZE_MAINTAINER`
66
+ * default base image - `DOCKERIZE_FROM`
67
+
68
+ ## Deploying
69
+
70
+ This gem also provides a handy script for unpacking the `.run` directory
71
+ at deploy time. To run this script, use the following command:
72
+
73
+ ```bash
74
+ # retrieve the script
75
+ > curl -s -O https://raw.github.com/modcloth-labs/dockerize/master/bin/dockerize-unpack
76
+
77
+ # make it executable
78
+ > chmod +x dockerize-unpack
79
+
80
+ # run the script
81
+ > dockerize-unpack quay.io/yourorg/example-image:tag
82
+
83
+ # or, run with the help flag for more info
84
+ > dockerize-unpack --help
85
+ ```
86
+
87
+
88
+ ## Contributing
89
+
90
+ 1. Fork it
91
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
92
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
93
+ 4. Push to the branch (`git push origin my-new-feature`)
94
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env rake
2
+ # vim:fileencoding=utf-8
3
+
4
+ require 'bundler/gem_tasks'
5
+ require 'rspec/core/rake_task'
6
+
7
+ desc 'Run rubocop'
8
+ task :rubocop do
9
+ sh('rubocop --format simple'){ |ok, _| ok || abort }
10
+ end
11
+
12
+ RSpec::Core::RakeTask.new(:spec) do |t|
13
+ t.rspec_opts = '--format documentation'
14
+ end
15
+
16
+ task default: [:spec, :rubocop]
data/bin/dockerize ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # coding: utf-8
3
+
4
+ lib = File.expand_path('../../lib', __FILE__)
5
+ vendor = File.expand_path('../../vendor', __FILE__)
6
+
7
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
8
+ $LOAD_PATH.unshift(vendor) unless $LOAD_PATH.include?(vendor)
9
+
10
+ require 'dockerize'
11
+ require 'dockerize/cli'
12
+ require 'colored'
13
+
14
+ Dockerize::Cli.run(ARGV)
15
+ exit 0
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -x
4
+ set -e
5
+
6
+ function usage() {
7
+ cat <<EOB
8
+
9
+ Usage: dockerize-unpack <image> [docker args]
10
+
11
+ Options:
12
+ -h/--help Display this message
13
+
14
+ Unpacks the files shipped with a docker container (via the \`dockerize\` gem)
15
+ and puts them in place by calling the \`deploy\` target in the
16
+ \`Makefile.run\`
17
+ EOB
18
+ }
19
+
20
+ function main() {
21
+ case "$1" in
22
+ -h|--help)
23
+ usage
24
+ exit 0
25
+ ;;
26
+ esac
27
+
28
+ export DOCKER="${DOCKER:-sudo docker}"
29
+ export IMAGE="$1"
30
+ if [[ -z "$IMAGE" ]] ; then
31
+ usage
32
+ exit 1
33
+ fi
34
+ export ARGS="$2"
35
+
36
+ mkdir -p /tmp/docker-bridge
37
+ pushd /tmp/docker-bridge
38
+ fetch
39
+ unpack
40
+ popd
41
+ }
42
+
43
+ function fetch() {
44
+ $DOCKER run -v /tmp/docker-bridge:/bridge "$ARGS" "$IMAGE" /docker/run/bridge
45
+ tar -xzf /tmp/docker-bridge/run.tar.gz
46
+ }
47
+
48
+ function unpack() {
49
+ cd ./docker/run
50
+ make -f Makefile.run deploy
51
+ }
52
+
53
+ main "$@"
data/dockerize.gemspec ADDED
@@ -0,0 +1,36 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ vendor = File.expand_path('../vendor', __FILE__)
4
+
5
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
+ $LOAD_PATH.unshift(vendor) unless $LOAD_PATH.include?(vendor)
7
+
8
+ require 'dockerize/version'
9
+
10
+ Gem::Specification.new do |gem|
11
+ gem.name = 'dockerize'
12
+ gem.version = Dockerize::VERSION
13
+ gem.authors = ['Rafe Colton']
14
+ gem.email = ['r.colton@modcloth.com']
15
+ gem.description = 'Dockerizes your application'
16
+ gem.summary = 'Creates a templated Dockerfile and corresponding ' <<
17
+ 'support files for easy deployment with docker.'
18
+ gem.homepage = 'https://github.com/modcloth-labs/dockerize'
19
+ gem.license = 'MIT'
20
+
21
+ gem.files = `git ls-files`.split($/)
22
+ gem.executables = gem.files.grep(%r{^bin/}) { |f| File.basename(f) }
23
+ gem.bindir = 'bin'
24
+ gem.test_files = gem.files.grep(%r{^spec/})
25
+ gem.require_paths = %w(lib vendor)
26
+ gem.required_ruby_version = '>= 1.9.3'
27
+
28
+ gem.add_development_dependency 'rake'
29
+ gem.add_development_dependency 'rspec'
30
+ gem.add_development_dependency 'rubocop'
31
+
32
+ gem.add_development_dependency 'pry' unless RUBY_PLATFORM == 'java'
33
+ gem.add_development_dependency 'simplecov' unless RUBY_PLATFORM == 'java'
34
+
35
+ gem.add_runtime_dependency 'syck' if RUBY_VERSION.split('.').first.to_i >= 2
36
+ end
@@ -0,0 +1,52 @@
1
+ # coding: utf-8
2
+
3
+ require 'dockerize/template_parser'
4
+ require 'dockerize/document_writer'
5
+
6
+ module Dockerize
7
+ class Cli
8
+ class << self
9
+ attr_reader :args
10
+
11
+ def run(argv = [])
12
+ @args = argv
13
+ ensure_project_dir
14
+ parse_args
15
+ set_out_stream
16
+ handle_templates
17
+ end
18
+
19
+ private
20
+
21
+ attr_writer :args
22
+
23
+ def handle_templates
24
+ all_templates.map do |template|
25
+ Dockerize::TemplateParser.new(File.read(template))
26
+ .write_with Dockerize::DocumentWriter.new
27
+ end
28
+ end
29
+
30
+ def all_templates
31
+ Dir["#{Dockerize::Config.template_dir}/**/*.erb"] |
32
+ Dir["#{Dockerize::Config.template_dir}/**/*.erb"]
33
+ end
34
+
35
+ def set_out_stream
36
+ $out = $stdout
37
+ $out = File.open('/dev/null', 'w') if Dockerize::Config.quiet?
38
+ end
39
+
40
+ def parse_args
41
+ Dockerize::Config.parse(args)
42
+ end
43
+
44
+ def ensure_project_dir
45
+ if args.count < 1 && !%w(-h --help).include?(args[0])
46
+ fail Dockerize::Error::MissingRequiredArgument,
47
+ 'You must specify a project directory to dockerize'
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,141 @@
1
+ # coding: utf-8
2
+ # rubocop:disable MethodLength, ClassLength, CyclomaticComplexity
3
+
4
+ require 'trollop'
5
+
6
+ module Dockerize
7
+ class Config
8
+ class << self
9
+ attr_reader :project_dir
10
+ attr_accessor :opts
11
+
12
+ def parse(args)
13
+ config = self
14
+
15
+ Trollop.options(args) do
16
+ text "Usage: dockerize <project directory> [options]\nOptions:\n"
17
+
18
+ # -q/--quiet
19
+ opt :quiet, 'Silence output', type: :flag, short: 'q', default: false
20
+
21
+ # -d/--dry-run
22
+ opt :dry_run, 'Dry run, do not write any files',
23
+ type: :flag,
24
+ short: 'd',
25
+ default: false
26
+
27
+ # -f/--force
28
+ opt :force, 'Force existing files to be overwritten',
29
+ type: :flag,
30
+ short: 'f',
31
+ default: false
32
+
33
+ # -b/--backup
34
+ opt :backup, 'Creates .bak version of files before overwriting them',
35
+ type: :flag,
36
+ short: 'b',
37
+ default: true
38
+
39
+ # -r/--registry
40
+ opt :registry, 'The Docker registry to use when writing files',
41
+ type: :string,
42
+ short: 'r',
43
+ default: ENV['DOCKERIZE_REGISTRY'] || 'quay.io/modcloth'
44
+
45
+ # -t/--template-dir
46
+ opt :template_dir,
47
+ 'The directory containing the templates to be written',
48
+ type: :string,
49
+ short: 't',
50
+ default: ENV['DOCKERIZE_TEMPLATE_DIR'] ||
51
+ "#{config.top}/templates"
52
+
53
+ # -m/--maintainer
54
+ opt :maintainer,
55
+ 'The default MAINTAINER to use for any Dockerfiles written',
56
+ type: :string,
57
+ short: 'm',
58
+ default: ENV['DOCKERIZE_MAINTAINER'] ||
59
+ "#{ENV['USER']} <#{ENV['USER']}@example.com>"
60
+
61
+ # -F/--from
62
+ opt :from,
63
+ 'The default base image to use for any Dockerfiles written',
64
+ type: :string,
65
+ short: 'F',
66
+ default: ENV['DOCKERIZE_FROM'] || 'ubuntu:12.04'
67
+
68
+ version "dockerize #{Dockerize::VERSION}"
69
+
70
+ begin
71
+ config.send(:opts=, parse(args))
72
+ rescue Trollop::CommandlineError => e
73
+ $stderr.puts "Error: #{e.message}."
74
+ $stderr.puts 'Try --help for help.'
75
+ exit 1
76
+ rescue Trollop::HelpNeeded
77
+ educate
78
+ exit
79
+ rescue Trollop::VersionNeeded
80
+ $stderr.puts version
81
+ exit
82
+ end
83
+
84
+ config.send(:opts)[:top] = config.top
85
+ config.send(:generate_accessor_methods, self)
86
+ end
87
+
88
+ self.project_dir = args[0]
89
+ set_project_name unless opts[:project_name]
90
+ end
91
+
92
+ def project_dir=(dir)
93
+ unless dir
94
+ fail Dockerize::Error::UnspecifiedProjectDirectory,
95
+ 'You must specify a project directory'
96
+ end
97
+
98
+ expanded_dir = File.expand_path(dir)
99
+
100
+ if !File.exists?(expanded_dir)
101
+ fail Dockerize::Error::NonexistentProjectDirectory,
102
+ "Project directory '#{expanded_dir}' does not exist"
103
+ elsif !File.directory?(expanded_dir)
104
+ fail Dockerize::Error::InvalidProjectDirectory,
105
+ "Project directory '#{expanded_dir}' is not a directory"
106
+ else
107
+ @project_dir = expanded_dir
108
+ end
109
+ end
110
+
111
+ def set_project_name
112
+ opts[:project_name] ||= File.basename(project_dir)
113
+ end
114
+
115
+ def top
116
+ @top ||= Gem::Specification.find_by_name('dockerize').gem_dir
117
+ end
118
+
119
+ private
120
+
121
+ def klass
122
+ @klass ||= class << self ; self ; end
123
+ end
124
+
125
+ def add_method(name, &block)
126
+ klass.send(:define_method, name.to_sym, &block)
127
+ end
128
+
129
+ def generate_accessor_methods(parser)
130
+ parser.specs.map do |k, v|
131
+ case v[:type]
132
+ when *Trollop::Parser::FLAG_TYPES
133
+ add_method("#{k}?") { @opts[k] }
134
+ when :string
135
+ add_method("#{k}") { @opts[k] }
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,91 @@
1
+ # coding: utf-8
2
+
3
+ require 'fileutils'
4
+ require 'colored'
5
+
6
+ module Dockerize
7
+ class DocumentWriter
8
+ CREATE_WORD = 'created '.green
9
+ REPLACE_WORD = 'replaced '.red
10
+ IGNORE_WORD = 'ignored '.yellow
11
+
12
+ attr_writer :document_name
13
+
14
+ def initialize(document_name = nil, stream = $out)
15
+ @stream = stream
16
+ @document_name = document_name
17
+ end
18
+
19
+ def write(contents = nil, executable = false)
20
+ @invalid_content = true unless contents
21
+ ensure_containing_dir
22
+ do_backup! if should_backup?
23
+ inform_of_write(status_word)
24
+ do_write!(contents, executable) if should_write?
25
+ end
26
+
27
+ def output_target
28
+ "#{Dockerize::Config.project_dir}/#{document_name}"
29
+ end
30
+
31
+ def inform_of_write(type)
32
+ $out.puts ' ' << type << document_name
33
+ end
34
+
35
+ protected
36
+
37
+ def invalid_content?
38
+ @invalid_content.nil? ? false : @invalid_content
39
+ end
40
+
41
+ def invalid_word
42
+ 'The template provided contains invalid content: '.blue
43
+ end
44
+
45
+ def status_word
46
+ if invalid_content?
47
+ invalid_word
48
+ elsif !should_write?
49
+ IGNORE_WORD
50
+ elsif preexisting_file?
51
+ REPLACE_WORD
52
+ else
53
+ CREATE_WORD
54
+ end
55
+ end
56
+
57
+ def should_backup?
58
+ Dockerize::Config.backup? && preexisting_file?
59
+ end
60
+
61
+ def should_write?
62
+ (Dockerize::Config.force? || !preexisting_file?) && !invalid_content?
63
+ end
64
+
65
+ def preexisting_file?
66
+ File.exists?(output_target)
67
+ end
68
+
69
+ def ensure_containing_dir(target = output_target)
70
+ FileUtils.mkdir_p(File.dirname(target))
71
+ end
72
+
73
+ def do_write!(contents, executable)
74
+ @stream = File.open(output_target, 'w') unless Dockerize::Config.dry_run?
75
+ @stream.print contents
76
+ FileUtils.chmod('+x', output_target) if executable && @stream != $out
77
+ ensure
78
+ @stream.close unless @stream == $out
79
+ end
80
+
81
+ def do_backup!
82
+ FileUtils.cp(output_target, "#{output_target}.bak")
83
+ end
84
+
85
+ def document_name
86
+ return @document_name if @document_name
87
+ fail Dockerize::Error::DocumentNameNotSpecified,
88
+ "Document name not specified for class #{self.class.name}"
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,15 @@
1
+ # coding: utf-8
2
+
3
+ module Dockerize
4
+ module Error
5
+ # Invalid Config Errors
6
+ InvalidConfig = Class.new(StandardError)
7
+ NonexistentProjectDirectory = Class.new(InvalidConfig)
8
+ InvalidProjectDirectory = Class.new(InvalidConfig)
9
+ UnspecifiedProjectDirectory = Class.new(InvalidConfig)
10
+
11
+ # General Errors
12
+ MissingRequiredArgument = Class.new(ArgumentError)
13
+ DocumentNameNotSpecified = Class.new(NoMethodError)
14
+ end
15
+ end
@@ -0,0 +1,63 @@
1
+ # coding: utf-8
2
+
3
+ require 'ostruct'
4
+ require 'erb'
5
+
6
+ module Dockerize
7
+ class TemplateParser
8
+ attr_reader :raw_text
9
+ attr_reader :metadata
10
+
11
+ def initialize(contents)
12
+ @raw_text = contents
13
+ @metadata = []
14
+ end
15
+
16
+ def document_name
17
+ metadata[:filename]
18
+ end
19
+
20
+ def executable?
21
+ metadata[:executable] == true ? true : false
22
+ end
23
+
24
+ def write_with(writer)
25
+ text = parsed_erb
26
+ writer.document_name = document_name
27
+ writer.write(text, executable?)
28
+ end
29
+
30
+ def parsed_erb
31
+ return @parsed_erb if @parsed_erb
32
+ begin
33
+ @parsed_erb = parse_erb(raw_text, config_vars)
34
+ rescue SyntaxError
35
+ @parsed_erb = nil
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def config_vars
42
+ Dockerize::Config.opts
43
+ end
44
+
45
+ def parse_erb(raw, hash)
46
+ os = OpenStruct.new(hash)
47
+ os_before = os.clone
48
+ result = ERB.new(raw, nil, '%<>>-').result(
49
+ os.instance_eval { binding }
50
+ )
51
+ @metadata = hash_diff(os, os_before)
52
+ result
53
+ end
54
+
55
+ def hash_diff(os1, os2)
56
+ h1 = os1.marshal_dump
57
+ h2 = os2.marshal_dump
58
+ h1.dup.delete_if { |k, v| h2[k] == v }.merge!(
59
+ h2.dup.delete_if { |k, v| h1.key?(k) }
60
+ )
61
+ end
62
+ end
63
+ end