ktheory-juicer 1.0.0.ktheory1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. data/History.txt +30 -0
  2. data/Manifest.txt +58 -0
  3. data/Rakefile +96 -0
  4. data/Readme.rdoc +312 -0
  5. data/VERSION +1 -0
  6. data/bin/juicer +8 -0
  7. data/lib/juicer.rb +70 -0
  8. data/lib/juicer/asset/path.rb +275 -0
  9. data/lib/juicer/asset/path_resolver.rb +79 -0
  10. data/lib/juicer/binary.rb +171 -0
  11. data/lib/juicer/cache_buster.rb +130 -0
  12. data/lib/juicer/chainable.rb +106 -0
  13. data/lib/juicer/cli.rb +56 -0
  14. data/lib/juicer/command/install.rb +61 -0
  15. data/lib/juicer/command/list.rb +57 -0
  16. data/lib/juicer/command/merge.rb +205 -0
  17. data/lib/juicer/command/util.rb +32 -0
  18. data/lib/juicer/command/verify.rb +60 -0
  19. data/lib/juicer/css_cache_buster.rb +80 -0
  20. data/lib/juicer/datafy/datafy.rb +20 -0
  21. data/lib/juicer/dependency_resolver/css_dependency_resolver.rb +29 -0
  22. data/lib/juicer/dependency_resolver/dependency_resolver.rb +101 -0
  23. data/lib/juicer/dependency_resolver/javascript_dependency_resolver.rb +23 -0
  24. data/lib/juicer/ext/logger.rb +5 -0
  25. data/lib/juicer/ext/string.rb +47 -0
  26. data/lib/juicer/ext/symbol.rb +15 -0
  27. data/lib/juicer/image_embed.rb +136 -0
  28. data/lib/juicer/install/base.rb +186 -0
  29. data/lib/juicer/install/closure_compiler_installer.rb +69 -0
  30. data/lib/juicer/install/jslint_installer.rb +51 -0
  31. data/lib/juicer/install/rhino_installer.rb +53 -0
  32. data/lib/juicer/install/yui_compressor_installer.rb +67 -0
  33. data/lib/juicer/jslint.rb +90 -0
  34. data/lib/juicer/merger/base.rb +74 -0
  35. data/lib/juicer/merger/javascript_merger.rb +29 -0
  36. data/lib/juicer/merger/stylesheet_merger.rb +110 -0
  37. data/lib/juicer/minifyer/closure_compiler.rb +90 -0
  38. data/lib/juicer/minifyer/java_base.rb +77 -0
  39. data/lib/juicer/minifyer/yui_compressor.rb +96 -0
  40. data/test/bin/jslint-1.0.js +523 -0
  41. data/test/bin/jslint.js +523 -0
  42. data/test/bin/rhino1_7R1.zip +0 -0
  43. data/test/bin/rhino1_7R2-RC1.jar +0 -0
  44. data/test/bin/rhino1_7R2-RC1.zip +0 -0
  45. data/test/bin/yuicompressor +0 -0
  46. data/test/bin/yuicompressor-2.3.5.zip +0 -0
  47. data/test/bin/yuicompressor-2.4.2.jar +0 -0
  48. data/test/bin/yuicompressor-2.4.2.zip +0 -0
  49. data/test/data/Changelog.txt +10 -0
  50. data/test/data/a.css +3 -0
  51. data/test/data/a.js +5 -0
  52. data/test/data/a1.css +5 -0
  53. data/test/data/b.css +1 -0
  54. data/test/data/b.js +5 -0
  55. data/test/data/b1.css +5 -0
  56. data/test/data/c1.css +3 -0
  57. data/test/data/css/2.gif +1 -0
  58. data/test/data/css/test.css +11 -0
  59. data/test/data/css/test2.css +1 -0
  60. data/test/data/d1.css +3 -0
  61. data/test/data/images/1.png +1 -0
  62. data/test/data/my_app.js +2 -0
  63. data/test/data/not-ok.js +2 -0
  64. data/test/data/ok.js +3 -0
  65. data/test/data/path_test.css +5 -0
  66. data/test/data/path_test2.css +14 -0
  67. data/test/data/pkg/module/moda.js +2 -0
  68. data/test/data/pkg/module/modb.js +3 -0
  69. data/test/data/pkg/pkg.js +1 -0
  70. data/test/test_helper.rb +169 -0
  71. data/test/unit/juicer/asset/path_resolver_test.rb +76 -0
  72. data/test/unit/juicer/asset/path_test.rb +370 -0
  73. data/test/unit/juicer/cache_buster_test.rb +104 -0
  74. data/test/unit/juicer/chainable_test.rb +94 -0
  75. data/test/unit/juicer/command/install_test.rb +58 -0
  76. data/test/unit/juicer/command/list_test.rb +81 -0
  77. data/test/unit/juicer/command/merge_test.rb +162 -0
  78. data/test/unit/juicer/command/util_test.rb +58 -0
  79. data/test/unit/juicer/command/verify_test.rb +48 -0
  80. data/test/unit/juicer/css_cache_buster_test.rb +71 -0
  81. data/test/unit/juicer/datafy_test.rb +37 -0
  82. data/test/unit/juicer/dependency_resolver/css_dependency_resolver_test.rb +36 -0
  83. data/test/unit/juicer/dependency_resolver/javascript_dependency_resolver_test.rb +50 -0
  84. data/test/unit/juicer/ext/string_test.rb +59 -0
  85. data/test/unit/juicer/ext/symbol_test.rb +27 -0
  86. data/test/unit/juicer/image_embed_test.rb +271 -0
  87. data/test/unit/juicer/install/installer_base_test.rb +214 -0
  88. data/test/unit/juicer/install/jslint_installer_test.rb +54 -0
  89. data/test/unit/juicer/install/rhino_installer_test.rb +57 -0
  90. data/test/unit/juicer/install/yui_compressor_test.rb +56 -0
  91. data/test/unit/juicer/jslint_test.rb +60 -0
  92. data/test/unit/juicer/merger/base_test.rb +122 -0
  93. data/test/unit/juicer/merger/javascript_merger_test.rb +74 -0
  94. data/test/unit/juicer/merger/stylesheet_merger_test.rb +180 -0
  95. data/test/unit/juicer/minifyer/closure_compressor_test.rb +107 -0
  96. data/test/unit/juicer/minifyer/yui_compressor_test.rb +116 -0
  97. data/test/unit/juicer_test.rb +1 -0
  98. metadata +265 -0
@@ -0,0 +1,186 @@
1
+ require 'nokogiri'
2
+ require 'open-uri'
3
+ require 'fileutils'
4
+ require "juicer"
5
+
6
+ module Juicer
7
+ module Install
8
+ #
9
+ # Installer skeleton. Provides basic functionality like figuring out where
10
+ # to install, create base directories, remove unneeded directories and more
11
+ # housekeeping.
12
+ #
13
+ class Base
14
+ attr_reader :install_dir
15
+
16
+ #
17
+ # Create new installer
18
+ #
19
+ def initialize(install_dir = Juicer.home)
20
+ @install_dir = install_dir
21
+ @path = nil
22
+ @bin_path = nil
23
+ @name = nil
24
+ @dependencies = {}
25
+ end
26
+
27
+ #
28
+ # Returns the latest available version number. Must be implemented in
29
+ # subclasses. Raises an exception when called directly.
30
+ #
31
+ def latest
32
+ raise NotImplementedError.new("Implement in subclasses")
33
+ end
34
+
35
+ # Returns the path relative to installation path this installer will
36
+ # install to
37
+ def path
38
+ return @path if @path
39
+ @path = "lib/" + self.class.to_s.split("::").pop.sub(/Installer$/, "").underscore
40
+ end
41
+
42
+ # Returns the path to search for binaries from
43
+ #
44
+ def bin_path
45
+ return @bin_path if @bin_path
46
+ @bin_path = File.join(path, "bin")
47
+ end
48
+
49
+ #
50
+ # Returns name of component. Default implementation returns class name
51
+ # with "Installer" removed
52
+ #
53
+ def name
54
+ return @name if @name
55
+ @name = File.basename(path).split("_").inject("") { |str, word| (str + " #{word.capitalize}").strip }
56
+ end
57
+
58
+ #
59
+ # Checks if the component is currently installed.
60
+ #
61
+ # If no version is provided the most recent version is assumed.
62
+ #
63
+ def installed?(version = nil)
64
+ installed = File.exists?(File.join(@install_dir, path, "#{version || latest}"))
65
+ deps = @dependencies.length == 0 || dependencies.all? { |d, v| d.installed?(v) }
66
+ installed && deps
67
+ end
68
+
69
+ #
70
+ # Install the component. Creates basic directory structure.
71
+ #
72
+ def install(version = nil)
73
+ raise "#{name} #{version} is already installed in #{File.join(@install_dir, path)}" if installed?(version)
74
+ version ||= latest
75
+ log "Installing #{name} #{version} in #{File.join(@install_dir, path)}"
76
+
77
+ if @dependencies.length > 0
78
+ log "Installing dependencies"
79
+ dependencies { |dependency, ver| dependency.install(ver) unless dependency.installed?(ver) }
80
+ end
81
+
82
+ # Create directories
83
+ FileUtils.mkdir_p(File.join(@install_dir, path, "bin"))
84
+ FileUtils.mkdir_p(File.join(@install_dir, path, version))
85
+
86
+ # Return resolved version for subclass to use
87
+ version
88
+ end
89
+
90
+ #
91
+ # Uninstalls the given version of the component.
92
+ #
93
+ # If no version is provided the most recent version is assumed.
94
+ #
95
+ # If there are no more files left in INSTALLATION_PATH/<path>, the
96
+ # whole directory is removed.
97
+ #
98
+ # This method takes a block and can be used from subclasses like so:
99
+ #
100
+ # def self.uninstall(install_dir = nil, version = nil)
101
+ # super do |home_dir, version|
102
+ # # Custom uninstall logic
103
+ # end
104
+ # end
105
+ #
106
+ #
107
+ def uninstall(version = nil)
108
+ version ||= self.latest
109
+ install_dir = File.join(@install_dir, path, version)
110
+ raise "#{name} #{version} is not installed" if !File.exists?(install_dir)
111
+
112
+ FileUtils.rm_rf(install_dir)
113
+
114
+ yield(File.join(@install_dir, path), version) if block_given?
115
+
116
+ files = Dir.glob(File.join(@install_dir, path, "**", "*")).find_all { |f| File.file?(f) }
117
+ FileUtils.rm_rf(File.join(@install_dir, path)) if files.length == 0
118
+ end
119
+
120
+ #
121
+ # Download a file to Juicer temporary directory. The file will be kept
122
+ # until #purge is called to wipe it. If the installer receives a request
123
+ # to download the same file again, the disk cache will be used unless the
124
+ # force argument is true (default false)
125
+ #
126
+ def download(url, force = false)
127
+ filename = File.join(@install_dir, "download", path.sub("lib/", ""), File.basename(url))
128
+ return filename if File.exists?(filename) && !force
129
+ FileUtils.mkdir_p(File.dirname(filename))
130
+ File.delete(filename) if File.exists?(filename) && force
131
+
132
+ log "Downloading #{url}"
133
+ File.open(filename, "wb") do |file|
134
+ webpage = open(url)
135
+ file.write(webpage.read)
136
+ webpage.close
137
+ end
138
+
139
+ filename
140
+ end
141
+
142
+ #
143
+ # Display a message to the user through Juicer::LOGGER
144
+ #
145
+ def log(str)
146
+ Juicer::LOGGER.info str
147
+ end
148
+
149
+ #
150
+ # Add a dependency. Dependency should be a Juicer::Install::Base installer
151
+ # class (not instance) OR a symbol/string like :rhino/"rhino" (which will
152
+ # be expanded unto Juicer::Install::RhinoInstaller). Version is optional
153
+ # and defaults to latest and greatest.
154
+ #
155
+ def dependency(dependency, version = nil)
156
+ dependency = Juicer::Install.get(dependency) if [String, Symbol].include?(dependency.class)
157
+
158
+ @dependencies[dependency.to_s + (version || "")] = [dependency, version]
159
+ end
160
+
161
+ #
162
+ # Yields depencies one at a time: class and version and returns an array
163
+ # of arrays: [dependency, version] where dependency is an instance and
164
+ # version a string.
165
+ #
166
+ def dependencies(&block)
167
+ @dependencies.collect do |name, dependency|
168
+ version = dependency[1]
169
+ dependency = dependency[0].new(@install_dir)
170
+ block.call(dependency, version) if block
171
+ [dependency, version]
172
+ end
173
+ end
174
+ end
175
+
176
+ #
177
+ # Returns the installer. Accepts installer classes (which are returned
178
+ # directly), strings or symbols. Strings and symbols may be on the form
179
+ # :my_module which is expanded to Juicer::Install::MyModuleInstaller
180
+ #
181
+ def self.get(nameOrClass)
182
+ return nameOrClass if nameOrClass.is_a? Class
183
+ (nameOrClass.to_s + "_installer").classify(Juicer::Install)
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,69 @@
1
+ require "juicer"
2
+ require "juicer/install/base"
3
+ require "zip/zip"
4
+
5
+ module Juicer
6
+ module Install
7
+ #
8
+ # Install and uninstall routines for the Google Closure Compiler.
9
+ # Installation downloads the Closure Compiler distribution, unzips it and
10
+ # storesthe jar file on disk along with the README.
11
+ #
12
+ class ClosureCompilerInstaller < Base
13
+ def initialize(install_dir = Juicer.home)
14
+ super(install_dir)
15
+ @latest = nil
16
+ @website = "http://code.google.com/p/closure-compiler/downloads/list"
17
+ @download_link = "http://closure-compiler.googlecode.com/files/compiler-%s.zip"
18
+ end
19
+
20
+ #
21
+ # Install the Closure Compiler. Downloads the distribution and keeps the jar
22
+ # file inside PATH/closure_compiler/bin and the README in
23
+ # PATH/closere_compiler/yyyymmdd/ where yyyymmdd is the version, most recent if
24
+ # not specified otherwise.
25
+ #
26
+ # Path defaults to environment variable $JUICER_HOME or default Juicer
27
+ # home
28
+ #
29
+ def install(version = nil)
30
+ version = super(version)
31
+ base = "closure-compiler-#{version}"
32
+ filename = download(@download_link % version)
33
+ target = File.join(@install_dir, path)
34
+
35
+ Zip::ZipFile.open(filename) do |file|
36
+ file.extract("README", File.join(target, version, "README"))
37
+ file.extract("compiler.jar", File.join(target, "bin", "#{base}.jar"))
38
+ end
39
+ end
40
+
41
+ #
42
+ # Uninstalls the given version of Closure Compiler. If no location is
43
+ # provided the environment variable $JUICER_HOME or Juicers default home
44
+ # directory is used.
45
+ #
46
+ # If no version is provided the most recent version is assumed.
47
+ #
48
+ # If there are no more files left in INSTALLATION_PATH/closure_compiler, the
49
+ # whole directory is removed.
50
+ #
51
+ def uninstall(version = nil)
52
+ super(version) do |dir, version|
53
+ File.delete(File.join(dir, "bin/closure-compiler-#{version}.jar"))
54
+ end
55
+ end
56
+
57
+ #
58
+ # Check which version is the most recent
59
+ #
60
+ def latest
61
+ return @latest if @latest
62
+ webpage = Nokogiri::HTML(open(@website))
63
+ @latest = (webpage / "//table[@id='resultstable']//td/a[contains(@href, 'compiler')]").map{|link|
64
+ link.get_attribute('href')[/\d{8}/].to_i
65
+ }.sort.last.to_s
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,51 @@
1
+ require "juicer"
2
+ require "juicer/install/base"
3
+ require "zip/zip"
4
+
5
+ module Juicer
6
+ module Install
7
+ #
8
+ # Install and uninstall routines for the JSLint library by Douglas Crockford.
9
+ # Installation downloads the jslintfull.js and rhino.js files and stores
10
+ # them in the Juicer installation directory.
11
+ #
12
+ class JSLintInstaller < Base
13
+ attr_reader :latest
14
+
15
+ def initialize(install_dir = Juicer.home)
16
+ super(install_dir)
17
+ @latest = "1.0"
18
+ @website = "http://www.jslint.com/"
19
+ @path = "lib/jslint"
20
+ @name = "JsLint"
21
+ dependency :rhino
22
+ end
23
+
24
+ #
25
+ # Install JSLint. Downloads the two js files and stores them in the
26
+ # installation directory.
27
+ #
28
+ def install(version = nil)
29
+ version = super(version)
30
+ filename = download(File.join(@website, "rhino/jslint.js"))
31
+ FileUtils.copy(filename, File.join(@install_dir, path, "bin", "jslint-#{version}.js"))
32
+ end
33
+
34
+ #
35
+ # Uninstalls JSLint
36
+ #
37
+ def uninstall(version = nil)
38
+ super(version) do |dir, version|
39
+ File.delete(File.join(dir, "bin", "jslint-#{version}.js"))
40
+ end
41
+ end
42
+ end
43
+
44
+ #
45
+ # This class makes it possible to do Juicer.install("jslint") instead of
46
+ # Juicer.install("j_s_lint"). Sugar, sugar...
47
+ #
48
+ class JslintInstaller < JSLintInstaller
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,53 @@
1
+ require "juicer"
2
+ require "juicer/install/base"
3
+ require "zip/zip"
4
+
5
+ module Juicer
6
+ module Install
7
+ #
8
+ # Install and uninstall routines for the Mozilla Rhino jar.
9
+ #
10
+ class RhinoInstaller < Base
11
+ attr_reader :latest
12
+
13
+ def initialize(install_dir = Juicer.home)
14
+ super(install_dir)
15
+ @latest = "1_7R2-RC1"
16
+ @website = "http://ftp.mozilla.org/pub/mozilla.org/js/"
17
+ end
18
+
19
+ #
20
+ # Install Rhino. Downloads the jar file and stores it in the installation
21
+ # directory along with the License text.
22
+ #
23
+ def install(version = nil)
24
+ version = super((version || latest).gsub(/\./, "_"))
25
+ base = "rhino#{version}"
26
+ filename = download(File.join(@website, "#{base}.zip"))
27
+ target = File.join(@install_dir, path)
28
+
29
+ Zip::ZipFile.open(filename) do |file|
30
+ FileUtils.mkdir_p(File.join(target, version))
31
+
32
+ begin
33
+ file.extract("#{base.sub(/-RC\d/, "")}/LICENSE.txt", File.join(target, version, "LICENSE.txt"))
34
+ rescue Exception
35
+ # Fail silently, some releases don't carry the license
36
+ end
37
+
38
+ file.extract("#{base.sub(/-RC\d/, "")}/js.jar", File.join(target, "bin", "#{base}.jar"))
39
+ end
40
+ end
41
+
42
+ #
43
+ # Uninstalls Rhino
44
+ #
45
+ def uninstall(version = nil)
46
+ super((version || latest).gsub(/\./, "_")) do |dir, version|
47
+ base = "rhino#{version}"
48
+ File.delete(File.join(dir, "bin/", "#{base}.jar"))
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,67 @@
1
+ require "juicer"
2
+ require "juicer/install/base"
3
+ require "zip/zip"
4
+
5
+ module Juicer
6
+ module Install
7
+ #
8
+ # Install and uninstall routines for the YUI Compressor.
9
+ # Installation downloads the YUI Compressor distribution, unzips it and
10
+ # storesthe jar file on disk along with the license.
11
+ #
12
+ class YuiCompressorInstaller < Base
13
+ def initialize(install_dir = Juicer.home)
14
+ super(install_dir)
15
+ @latest = nil
16
+ @website = "http://yuilibrary.com/downloads/"
17
+ end
18
+
19
+ #
20
+ # Install the Yui Compressor. Downloads the distribution and keeps the jar
21
+ # file inside PATH/yui_compressor/bin and the README and CHANGELOG in
22
+ # PATH/yui_compressor/x.y.z/ where x.y.z is the version, most recent if
23
+ # not specified otherwise.
24
+ #
25
+ # Path defaults to environment variable $JUICER_HOME or default Juicer
26
+ # home
27
+ #
28
+ def install(version = nil)
29
+ version = super(version)
30
+ base = "yuicompressor-#{version}"
31
+ filename = download(File.join(@website, "yuicompressor", "#{base}.zip"))
32
+ target = File.join(@install_dir, path)
33
+
34
+ Zip::ZipFile.open(filename) do |file|
35
+ file.extract("#{base}/doc/README", File.join(target, version, "README"))
36
+ file.extract("#{base}/doc/CHANGELOG", File.join(target, version, "CHANGELOG"))
37
+ file.extract("#{base}/build/#{base}.jar", File.join(target, "bin", "#{base}.jar"))
38
+ end
39
+ end
40
+
41
+ #
42
+ # Uninstalls the given version of YUI Compressor. If no location is
43
+ # provided the environment variable $JUICER_HOME or Juicers default home
44
+ # directory is used.
45
+ #
46
+ # If no version is provided the most recent version is assumed.
47
+ #
48
+ # If there are no more files left in INSTALLATION_PATH/yui_compressor, the
49
+ # whole directory is removed.
50
+ #
51
+ def uninstall(version = nil)
52
+ super(version) do |dir, version|
53
+ File.delete(File.join(dir, "bin/yuicompressor-#{version}.jar"))
54
+ end
55
+ end
56
+
57
+ #
58
+ # Check which version is the most recent
59
+ #
60
+ def latest
61
+ return @latest if @latest
62
+ webpage = Nokogiri::HTML(open(@website))
63
+ @latest = (webpage / "//h2[@id='yuicompressor']/../../../..//a")[0].get_attribute("href").match(/(\d\.\d\.\d)/)[1]
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,90 @@
1
+ require "juicer/binary"
2
+
3
+ module Juicer
4
+ #
5
+ # A Ruby API to Douglas Crockfords genious JsLint program
6
+ # http://www.jslint.com/
7
+ #
8
+ # JsLint parses JavaScript code and identifies (potential) problems.
9
+ # Effectively, JsLint defines a subset of JavaScript which is safe to use, and
10
+ # among other things make code minification a substantially less dangerous
11
+ # task.
12
+ #
13
+ class JsLint
14
+ include Juicer::Binary
15
+
16
+ def initialize(options = {})
17
+ super(options[:java] || "java")
18
+ path << options[:bin_path] if options[:bin_path]
19
+ end
20
+
21
+ #
22
+ # Checks if a files has problems. Also includes experimental support for CSS
23
+ # files. CSS files should begin with the line @charset "UTF-8";
24
+ #
25
+ # Returns a Juicer::JsLint::Report object
26
+ #
27
+ def check(file)
28
+ rhino_jar = rhino
29
+ js_file = locate_lib
30
+
31
+ raise FileNotFoundError.new("Unable to locate Rhino jar '#{rhino_jar}'") if !rhino_jar || !File.exists?(rhino_jar)
32
+ raise FileNotFoundError.new("Unable to locate JsLint '#{js_file}'") if !js_file || !File.exists?(js_file)
33
+ raise FileNotFoundError.new("Unable to locate input file '#{file}'") unless File.exists?(file)
34
+
35
+ lines = execute(%Q{-jar "#{rhino}" "#{locate_lib}" "#{file}"}).split("\n")
36
+ return Report.new if lines.length == 1 && lines[0] =~ /jslint: No problems/
37
+
38
+ report = Report.new
39
+ lines = lines.reject { |line| !line || "#{line}".strip == "" }
40
+ report.add_error(lines.shift, lines.shift) while lines.length > 0
41
+
42
+ return report
43
+ end
44
+
45
+ def rhino
46
+ files = locate("**/rhino*.jar", "RHINO_HOME")
47
+ !files || files.empty? ? nil : files.sort.last
48
+ end
49
+
50
+ def locate_lib
51
+ files = locate("**/jslint-*.js", "JSLINT_HOME")
52
+ !files || files.empty? ? nil : files.sort.last
53
+ end
54
+
55
+ #
56
+ # Represents the results of a JsLint run
57
+ #
58
+ class Report
59
+ attr_accessor :errors
60
+
61
+ def initialize(errors = [])
62
+ @errors = errors
63
+ end
64
+
65
+ def add_error(message, code)
66
+ @errors << JsLint::Error.new(message, code)
67
+ end
68
+
69
+ def ok?
70
+ @errors.nil? || @errors.length == 0
71
+ end
72
+ end
73
+
74
+ #
75
+ # A JsLint error
76
+ #
77
+ class Error
78
+ attr_accessor :message, :code
79
+
80
+ def initialize(message, code)
81
+ @message = message
82
+ @code = code
83
+ end
84
+
85
+ def to_s
86
+ "#@message\n#@code"
87
+ end
88
+ end
89
+ end
90
+ end