right_develop 2.1.5 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (28) hide show
  1. data/VERSION +1 -1
  2. data/bin/right_develop +4 -0
  3. data/lib/right_develop.rb +1 -0
  4. data/lib/right_develop/commands.rb +2 -1
  5. data/lib/right_develop/commands/server.rb +194 -0
  6. data/lib/right_develop/testing.rb +31 -0
  7. data/lib/right_develop/testing/clients.rb +36 -0
  8. data/lib/right_develop/testing/clients/rest.rb +34 -0
  9. data/lib/right_develop/testing/clients/rest/requests.rb +38 -0
  10. data/lib/right_develop/testing/clients/rest/requests/base.rb +305 -0
  11. data/lib/right_develop/testing/clients/rest/requests/playback.rb +293 -0
  12. data/lib/right_develop/testing/clients/rest/requests/record.rb +175 -0
  13. data/lib/right_develop/testing/recording.rb +33 -0
  14. data/lib/right_develop/testing/recording/config.rb +571 -0
  15. data/lib/right_develop/testing/recording/metadata.rb +805 -0
  16. data/lib/right_develop/testing/servers/might_api/.gitignore +3 -0
  17. data/lib/right_develop/testing/servers/might_api/Gemfile +6 -0
  18. data/lib/right_develop/testing/servers/might_api/Gemfile.lock +18 -0
  19. data/lib/right_develop/testing/servers/might_api/app/base.rb +323 -0
  20. data/lib/right_develop/testing/servers/might_api/app/echo.rb +73 -0
  21. data/lib/right_develop/testing/servers/might_api/app/playback.rb +46 -0
  22. data/lib/right_develop/testing/servers/might_api/app/record.rb +45 -0
  23. data/lib/right_develop/testing/servers/might_api/config.ru +8 -0
  24. data/lib/right_develop/testing/servers/might_api/config/init.rb +47 -0
  25. data/lib/right_develop/testing/servers/might_api/lib/config.rb +204 -0
  26. data/lib/right_develop/testing/servers/might_api/lib/logger.rb +68 -0
  27. data/right_develop.gemspec +30 -2
  28. metadata +84 -34
@@ -0,0 +1,45 @@
1
+ #
2
+ # Copyright (c) 2014 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ require ::File.expand_path('../base', __FILE__)
24
+
25
+ module RightDevelop::Testing::Server::MightApi::App
26
+ class Record < ::RightDevelop::Testing::Server::MightApi::App::Base
27
+
28
+ STATE_FILE_NAME = 'record_state.yml'
29
+
30
+ def initialize
31
+ super(STATE_FILE_NAME)
32
+ end
33
+
34
+ # @see RightDevelop::Testing::Server::MightApi::App::Base#handle_request
35
+ def handle_request(env, verb, uri, headers, body)
36
+ proxy(
37
+ ::RightDevelop::Testing::Client::Rest::Request::Record,
38
+ verb,
39
+ uri,
40
+ headers,
41
+ body)
42
+ end
43
+
44
+ end
45
+ end
@@ -0,0 +1,8 @@
1
+ lib_dir = ::File.expand_path('../../../../../../lib', __FILE__)
2
+ $:.unshift(lib_dir) unless $:.include?(lib_dir)
3
+
4
+ require ::File.expand_path('../config/init', __FILE__)
5
+ require ::File.expand_path("../app/#{::RightDevelop::Testing::Server::MightApi::Config.mode}", __FILE__)
6
+
7
+ run ::RightDevelop::Testing::Server::MightApi::App.const_get(
8
+ ::RightDevelop::Testing::Server::MightApi::Config.mode.capitalize).new
@@ -0,0 +1,47 @@
1
+ #
2
+ # Copyright (c) 2014 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ # fixup RACK_ENV
24
+ require 'right_develop'
25
+
26
+ require ::File.expand_path('../../lib/config', __FILE__)
27
+ require ::File.expand_path('../../lib/logger', __FILE__)
28
+
29
+ module RightDevelop::Testing::Server::MightApi
30
+
31
+ # attempt to read stdin for configuration or else expect relative file path.
32
+ # note the following .fcntl call returns zero when data is available on $stdin
33
+ config_yaml = ($stdin.tty? || 0 != $stdin.fcntl(::Fcntl::F_GETFL, 0)) ? '' : $stdin.read
34
+ config_hash = config_yaml.empty? ? nil : ::YAML.load(config_yaml)
35
+ if config_hash
36
+ Config.from_hash(config_hash)
37
+ else
38
+ Config.from_file(Config::DEFAULT_CONFIG_PATH)
39
+ end
40
+
41
+ # ensure fixture dir exists as result of configuration for better
42
+ # synchronization of any state file locking.
43
+ ::FileUtils.mkdir_p(Config.fixtures_dir)
44
+
45
+ # ready.
46
+ logger.info("MightApi initialized in #{Config.mode} mode.")
47
+ end
@@ -0,0 +1,204 @@
1
+ #
2
+ # Copyright (c) 2014 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ if ::ENV['RACK_ENV'].to_s.empty?
24
+ ::ENV['RACK_ENV'] = 'development'
25
+ end
26
+
27
+ require 'right_develop/testing/recording/config'
28
+ require 'uri'
29
+
30
+ # define the module hierarchy once so that it can be on a single line hereafter.
31
+ module RightDevelop
32
+ module Testing
33
+ module Server
34
+ module MightApi
35
+ # if only ruby could consume a single line module declaration...
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ module RightDevelop::Testing::Server::MightApi
42
+ class Config
43
+
44
+ # default config path.
45
+ CONFIG_DIR_NAME = 'config'.freeze
46
+ DEFAULT_CONFIG_PATH = ::File.join(CONFIG_DIR_NAME, 'might_deploy.yml').freeze
47
+
48
+ METADATA_CLASS = ::RightDevelop::Testing::Recording::Metadata
49
+ CONFIG_CLASS = ::RightDevelop::Testing::Recording::Config
50
+
51
+ # Loads the config hash from given path or a relative location.
52
+ #
53
+ # @param [String] path to configuration
54
+ #
55
+ # @return [Mash] configuration hash
56
+ #
57
+ # @raise [ArgumentError] on failure to load
58
+ def self.from_file(path, options = nil)
59
+ @config = CONFIG_CLASS.from_file(path, options)
60
+ self
61
+ end
62
+
63
+ # Setup configuration. Defaults to using environment variables for setup due
64
+ # to rackup not allowing custom arguments to be passed on command line.
65
+ #
66
+ # @param [Hash] config as raw configuration data
67
+ #
68
+ # @return [Config] self
69
+ #
70
+ # @raise [ArgumentError] on failure to load
71
+ def self.from_hash(config_hash)
72
+ @config = CONFIG_CLASS.new(config_hash)
73
+ self
74
+ end
75
+
76
+ # @see Object#method_missing
77
+ def self.method_missing(methud, *args, &block)
78
+ if @config && @config.respond_to?(methud)
79
+ @config.__send__(methud, *args, &block)
80
+ else
81
+ super
82
+ end
83
+ end
84
+
85
+ # @see Object#respond_to?
86
+ def self.respond_to?(methud)
87
+ super(methud) || (@config && @config.respond_to?(methud))
88
+ end
89
+
90
+ # @see Class.const_missing
91
+ def self.const_missing(konst)
92
+ if CONFIG_CLASS.const_defined?(konst)
93
+ CONFIG_CLASS.const_get(konst)
94
+ else
95
+ super
96
+ end
97
+ end
98
+
99
+ # @see Class.const_defined?
100
+ def self.const_defined?(konst)
101
+ super(konst) || CONFIG_CLASS.const_defined?(konst)
102
+ end
103
+
104
+ # @return [String] environment configuration string
105
+ def self.environment
106
+ @environment ||= ::ENV['RACK_ENV']
107
+ end
108
+
109
+ def self.normalize_fixtures_dir(logger)
110
+ # remove any residual state files at root of fixtures directory.
111
+ logger.info("Normalizing fixtures directory: #{fixtures_dir.inspect} ...")
112
+ ::Dir[::File.join(fixtures_dir, '*.yml')].each do |path|
113
+ ::File.unlink(path) if ::File.file?(path)
114
+ end
115
+
116
+ # recursively iterate requests/responses ensuring that both files exist
117
+ # and that they are MD5-named. if not, then supply the MD5 by renaming
118
+ # both files. this allows a user to write custom request/responses without
119
+ # having to supply the MD5 checksum for the significant request parts.
120
+ ::Dir[::File.join(fixtures_dir, '*/*')].sort.each do |epoch_api_dir|
121
+ if ::File.directory?(epoch_api_dir)
122
+ # request/response pairs must be identical whether or not using human-
123
+ # readable file names.
124
+ route_subdir_name = ::File.basename(epoch_api_dir)
125
+ if route = routes.find { |prefix, data| data[:subdir] == route_subdir_name }
126
+ route_path, route_data = route
127
+ requests_dir = epoch_api_dir + '/request/'
128
+ responses_dir = epoch_api_dir + '/response/'
129
+ request_files = ::Dir[requests_dir + '**/*.yml'].sort.map { |path| path[requests_dir.length..-1] }
130
+ response_files = ::Dir[responses_dir + '**/*.yml'].sort.map { |path| path[responses_dir.length..-1] }
131
+ if request_files != response_files
132
+ difference = ((request_files - response_files) | (response_files - request_files)).sort
133
+ message = 'Mismatched request/response file pairs under ' +
134
+ "#{epoch_api_dir.inspect}: #{difference.inspect}"
135
+ raise ::ArgumentError, message
136
+ end
137
+
138
+ # convert filename prefix to MD5 wherever necessary.
139
+ request_files.each do |path|
140
+ # load request/response pair to validate.
141
+ request_file_path = ::File.join(requests_dir, path)
142
+ response_file_path = ::File.join(responses_dir, path)
143
+ request_data = ::Mash.new(::YAML.load_file(request_file_path))
144
+ response_data = ::Mash.new(::YAML.load_file(response_file_path))
145
+
146
+ # if confing contains unreachable (i.e. no available route) files
147
+ # then that is ignorable.
148
+ query_string = request_data[:query]
149
+ uri = METADATA_CLASS.normalize_uri(
150
+ URI::HTTP.build(
151
+ host: 'none',
152
+ path: (route_path + ::File.dirname(path)),
153
+ query: request_data[:query]).to_s)
154
+
155
+ # compute checksum from recorded request metadata.
156
+ request_metadata = METADATA_CLASS.new(
157
+ mode: :echo,
158
+ kind: :request,
159
+ logger: logger,
160
+ route_data: route_data,
161
+ uri: uri,
162
+ verb: request_data[:verb],
163
+ headers: request_data[:headers],
164
+ body: request_data[:body],
165
+ variables: {})
166
+
167
+ # rename fixure file prefix only if given custom name by user.
168
+ name = ::File.basename(path)
169
+
170
+ if matched = FIXTURE_FILE_NAME_REGEX.match(name)
171
+ # verify correct MD5 for body.
172
+ file_checksum = matched[1]
173
+ if file_checksum.casecmp(request_metadata.checksum) != 0
174
+ message = "Checksum from fixture file name (#{file_checksum}) " +
175
+ "does not match the request body checksum " +
176
+ "(#{request_metadata.checksum}): #{request_file_path.inspect}"
177
+ raise ::ArgumentError, message
178
+ end
179
+ else
180
+ # compute checksum from loaded request body.
181
+ checksum_file_name = request_metadata.checksum + '.yml'
182
+
183
+ # rename file pair.
184
+ to_request_file_path = ::File.join(::File.dirname(request_file_path), checksum_file_name)
185
+ to_response_file_path = ::File.join(::File.dirname(response_file_path), checksum_file_name)
186
+ logger.debug("Renaming #{request_file_path.inspect} to #{to_request_file_path.inspect}.")
187
+ ::File.rename(request_file_path, to_request_file_path)
188
+ logger.debug("Renaming #{response_file_path.inspect} to #{to_response_file_path.inspect}.")
189
+ ::File.rename(response_file_path, to_response_file_path)
190
+ end
191
+ end
192
+ else
193
+ # unknown route fixture directories will cause playback engine to
194
+ # spin forever.
195
+ unrecognized_routes << epoch_api_dir
196
+ message = "Cannot find a route for fixtures directory: #{epoch_api_dir.inspect}"
197
+ raise ::ArgumentError, message
198
+ end
199
+ end
200
+ end
201
+ end
202
+
203
+ end # Config
204
+ end # RightDevelop::Testing::Server::MightApi
@@ -0,0 +1,68 @@
1
+ #
2
+ # Copyright (c) 2014 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ require 'logger'
24
+ require 'right_support'
25
+
26
+ module RightDevelop::Testing::Server::MightApi
27
+
28
+ def self.logger
29
+ @logger ||= lambda do
30
+ l = nil
31
+ case Config.environment
32
+ when 'development'
33
+ # file
34
+ log_dir = Config.log_dir
35
+ log_file_path = ::File.join(log_dir, 'development.log')
36
+ ::FileUtils.mkdir_p(log_dir)
37
+ log_file = ::File.open(log_file_path, 'a')
38
+ log_file.sync = true
39
+ file_logger = Logger.new(log_file)
40
+
41
+ # console
42
+ STDOUT.sync = true
43
+ console_logger = ::Logger.new(STDOUT)
44
+
45
+ # multiplexer
46
+ l = ::RightSupport::Log::Multiplexer.new(file_logger, console_logger)
47
+ when 'test'
48
+ # any tests should mock logger to verify output.
49
+ l = ::RightSupport::Log::NullLogger.new
50
+ else
51
+ l = ::RightSupport::Log::SystemLogger.new('might_api', facility: 'local0')
52
+ end
53
+ l.formatter = DateTimeLoggerFormatter.new
54
+ l.level = Config.log_level
55
+ l
56
+ end.call
57
+ end
58
+
59
+ class DateTimeLoggerFormatter < ::Logger::Formatter
60
+ def call(severity, time, progname, msg)
61
+ sprintf("%s (%s) %s: %s\n",
62
+ ::Time.now,
63
+ ::Thread.current.object_id,
64
+ severity,
65
+ msg2str(msg))
66
+ end
67
+ end
68
+ end
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{right_develop}
8
- s.version = "2.1.5"
8
+ s.version = "2.2.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Tony Spataro"]
12
- s.date = %q{2014-04-14}
12
+ s.date = %q{2014-04-28}
13
13
  s.default_executable = %q{right_develop}
14
14
  s.description = %q{A toolkit of development tools created by RightScale.}
15
15
  s.email = %q{support@rightscale.com}
@@ -33,6 +33,7 @@ Gem::Specification.new do |s|
33
33
  "lib/right_develop/ci/util.rb",
34
34
  "lib/right_develop/commands.rb",
35
35
  "lib/right_develop/commands/git.rb",
36
+ "lib/right_develop/commands/server.rb",
36
37
  "lib/right_develop/git.rb",
37
38
  "lib/right_develop/git/rake_task.rb",
38
39
  "lib/right_develop/net.rb",
@@ -42,6 +43,27 @@ Gem::Specification.new do |s|
42
43
  "lib/right_develop/s3.rb",
43
44
  "lib/right_develop/s3/interface.rb",
44
45
  "lib/right_develop/s3/rake_task.rb",
46
+ "lib/right_develop/testing.rb",
47
+ "lib/right_develop/testing/clients.rb",
48
+ "lib/right_develop/testing/clients/rest.rb",
49
+ "lib/right_develop/testing/clients/rest/requests.rb",
50
+ "lib/right_develop/testing/clients/rest/requests/base.rb",
51
+ "lib/right_develop/testing/clients/rest/requests/playback.rb",
52
+ "lib/right_develop/testing/clients/rest/requests/record.rb",
53
+ "lib/right_develop/testing/recording.rb",
54
+ "lib/right_develop/testing/recording/config.rb",
55
+ "lib/right_develop/testing/recording/metadata.rb",
56
+ "lib/right_develop/testing/servers/might_api/.gitignore",
57
+ "lib/right_develop/testing/servers/might_api/Gemfile",
58
+ "lib/right_develop/testing/servers/might_api/Gemfile.lock",
59
+ "lib/right_develop/testing/servers/might_api/app/base.rb",
60
+ "lib/right_develop/testing/servers/might_api/app/echo.rb",
61
+ "lib/right_develop/testing/servers/might_api/app/playback.rb",
62
+ "lib/right_develop/testing/servers/might_api/app/record.rb",
63
+ "lib/right_develop/testing/servers/might_api/config.ru",
64
+ "lib/right_develop/testing/servers/might_api/config/init.rb",
65
+ "lib/right_develop/testing/servers/might_api/lib/config.rb",
66
+ "lib/right_develop/testing/servers/might_api/lib/logger.rb",
45
67
  "lib/right_develop/utility.rb",
46
68
  "lib/right_develop/utility/git.rb",
47
69
  "lib/right_develop/utility/shell.rb",
@@ -68,6 +90,8 @@ Gem::Specification.new do |s|
68
90
  s.add_runtime_dependency(%q<trollop>, [">= 1.0", "< 3.0"])
69
91
  s.add_runtime_dependency(%q<right_git>, ["~> 0.1.0"])
70
92
  s.add_runtime_dependency(%q<right_aws>, [">= 2.1.0"])
93
+ s.add_runtime_dependency(%q<extlib>, [">= 0"])
94
+ s.add_runtime_dependency(%q<rack>, [">= 0"])
71
95
  s.add_development_dependency(%q<jeweler>, ["~> 1.8.3"])
72
96
  s.add_development_dependency(%q<rdoc>, [">= 2.4.2"])
73
97
  else
@@ -79,6 +103,8 @@ Gem::Specification.new do |s|
79
103
  s.add_dependency(%q<trollop>, [">= 1.0", "< 3.0"])
80
104
  s.add_dependency(%q<right_git>, ["~> 0.1.0"])
81
105
  s.add_dependency(%q<right_aws>, [">= 2.1.0"])
106
+ s.add_dependency(%q<extlib>, [">= 0"])
107
+ s.add_dependency(%q<rack>, [">= 0"])
82
108
  s.add_dependency(%q<jeweler>, ["~> 1.8.3"])
83
109
  s.add_dependency(%q<rdoc>, [">= 2.4.2"])
84
110
  end
@@ -91,6 +117,8 @@ Gem::Specification.new do |s|
91
117
  s.add_dependency(%q<trollop>, [">= 1.0", "< 3.0"])
92
118
  s.add_dependency(%q<right_git>, ["~> 0.1.0"])
93
119
  s.add_dependency(%q<right_aws>, [">= 2.1.0"])
120
+ s.add_dependency(%q<extlib>, [">= 0"])
121
+ s.add_dependency(%q<rack>, [">= 0"])
94
122
  s.add_dependency(%q<jeweler>, ["~> 1.8.3"])
95
123
  s.add_dependency(%q<rdoc>, [">= 2.4.2"])
96
124
  end