stubb 0.1.rc.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2011 Johannes Emerich
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,159 @@
1
+ Stubb
2
+ =====
3
+
4
+ Stubb allows to **set up a REST API stub by putting responses in files** organized in a directory tree. Which file is picked in response to a particular HTTP request is primarily determined by the request's **method**, **path** and **accept header**. Thus adding a response for a certain type of request is as easy as adding a file with a matching name. For example, the file
5
+
6
+ whales/narwhal.GET.json
7
+
8
+ in your base directory will be picked to deliver the response to the request
9
+
10
+ GET /whales/narwhal.json HTTP/1.1
11
+
12
+ or alternatively
13
+
14
+ GET /whales/narwhal HTTP/1.1
15
+ Accept: application/json
16
+
17
+ .
18
+
19
+ Additionally, **sequences of responses** to repeated identical requests can be defined through infix numerals in file names.
20
+
21
+ Directory Structure and Response Files
22
+ --------------------------------------
23
+
24
+ All requests are served from the *base directory*, that is the directory Stubb was started from. The directory tree in your base directory determines the path hierarchy of your stubbed REST API. Request paths are mapped to relative paths within the base directory to locate a response file.
25
+
26
+ ### Response Files
27
+
28
+ A *response file* is a file containing an API response. There are two kinds of response files, member response files and collection response files. They differ only in concept and naming.
29
+
30
+ #### Member Response Files
31
+
32
+ A *member response file* is a file containing an API response for a member resource, named after the scheme
33
+
34
+ REQUEST_PATH_WITHOUT_EXTENSION.HTTP_METHOD[.SEQUENCE_NUMBER][.FILE_TYPE]
35
+
36
+ , where `SEQUENCE_NUMBER` is optional and only needed when defining response sequences, and `FILE_TYPE` is also optional and only needed when a file type is implied by request path or accept header.
37
+
38
+ Examples:
39
+
40
+ whales/narwhal.GET
41
+ whales/narwhal.GET.json
42
+ whales/narwhal.GET.1
43
+ whales/narwhal.GET.1.json
44
+
45
+ #### Collection Response Files
46
+
47
+ A *collection response file* is a file containing an API response for a collection resource, named after the scheme
48
+
49
+ REQUEST_PATH_WITHOUT_EXTENSION/HTTP_METHOD[.SEQUENCE_NUMBER][.FILE_TYPE]
50
+
51
+ , where `SEQUENCE_NUMBER` is optional and only needed when defining response sequences, and `FILE_TYPE` is also optional and only needed when a file type is implied by request path or accept header.
52
+
53
+ Examples:
54
+
55
+ whales/GET
56
+ whales/GET.json
57
+ whales/GET.1
58
+ whales/GET.1.json
59
+
60
+ ### Response Files as ERB Templates
61
+
62
+ Any matching response file will be evaluated as an ERB template, with `GET` or `POST` parameters available in a `params` hash. This can come in handy when stubbing `POST` and `PUT` requests or serving JSONP.
63
+
64
+ ### YAML Frontmatter
65
+
66
+ Response files may contain YAML frontmatter before the response text, allowing to set custom values for response status and header:
67
+
68
+ ---
69
+ status: 201
70
+ header:
71
+ Cache-Control: no-cache
72
+ ---
73
+ {"name":"Stubb"}
74
+
75
+ ### Missing Responses
76
+
77
+ If no matching response file is found, Stubb replies with a status of `404`. You can customize error responses for types of requests by creating a matching response file that contains your custom response.
78
+
79
+ Path Matching
80
+ -------------
81
+
82
+ Paths in the base directory may include wildcards to allow one response file to match for a whole range of request paths instead of just one. Both Directory names and file names may be wildcards. Wildcards are marked by starting and ending in an underscore (`_`). A wildcard segment matches any equally positioned segment of a request path.
83
+
84
+ For example
85
+
86
+ whales/_default_whale_.GET.json
87
+
88
+ matches
89
+
90
+ GET /whales/pygmy_sperm_whale.json HTTP/1.1
91
+
92
+ as well as
93
+
94
+ GET /whales/blackfish.json HTTP/1.1
95
+
96
+ .
97
+
98
+ If a literal match exists, it will be chosen over a wildcard match.
99
+
100
+ Response Sequences
101
+ ------------------
102
+
103
+ A *response sequence* is a sequence of response files whose members are being used as responses to a sequence of requests of the same type. This allows for controlled stubbing of changes in the API.
104
+
105
+ ### Stalling Sequences (1..._n_)
106
+
107
+ A *stalling sequence* keeps responding with the last response file in the response sequence after _n_ requests of the same type. Stalling sequences are specified by adding response files with indices 1 through _n_.
108
+
109
+ GET.1.format, GET.2.format, ..., GET._n-1_.format, GET._n_.format
110
+
111
+ Example:
112
+
113
+ whales/GET.1.json
114
+ whales/GET.2.json
115
+ whales/GET.3.json
116
+
117
+ From the third request on, the response to
118
+
119
+ GET /whales.json HTTP/1.1
120
+
121
+ will be the one given in `whales/GET.3.json`.
122
+
123
+ ### Looping Sequences (0..._n-1_)
124
+
125
+ A *stalling sequence* starts from the first response file in the response sequence after _n_ requests of the same type. Looping sequences are specified by adding response files with indices 0 through _n_-1.
126
+
127
+ GET.0.format, GET.1.format, ..., GET._n-2_.format, GET._n-1_.format
128
+
129
+ Example:
130
+
131
+ whales/GET.0.json
132
+ whales/GET.1.json
133
+ whales/GET.2.json
134
+
135
+ The response to the fourth request to
136
+
137
+ GET /whales.json HTTP/1.1
138
+
139
+ will again be the one given in `whales/GET.0.json`, and so forth.
140
+
141
+ Dependencies
142
+ ------------
143
+
144
+ Stubb depends on
145
+
146
+ - <a href="http://github.com/rack/rack">Rack</a> for processing and serving requests, and
147
+ - <a href="https://github.com/wycats/thor">Thor</a> for adding a CLI executable.
148
+
149
+ License
150
+ -------
151
+
152
+ Copyright (c) 2011 Johannes Emerich
153
+
154
+ MIT-style licensing, for details see file `LICENSE`.
155
+
156
+ <hr>
157
+
158
+ _'Why,' thinks I, 'what's the row? It's not a real leg, only a false leg.'_
159
+ --Stubb in _Moby Dick_
data/bin/stubb ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
4
+
5
+ require 'thor'
6
+ require 'stubb'
7
+
8
+ module Stubb
9
+ class CLI < Thor
10
+ map '-v' => :version
11
+
12
+ desc 'server', 'Starts the server'
13
+ method_option :port, :type => :numeric, :default => 4040, :aliases => '-p', :desc => 'Specifies the port for the server to use'
14
+ method_option :root, :type => :string, :default => '', :aliases => '-r', :desc => 'Specifies the root directory to serve from'
15
+ method_option :verbose, :type => :boolean, :aliases => '-v', :desc => 'Print debug messages'
16
+ def server
17
+ Stubb.run :Port => options[:port], :root => options[:root], :verbose => options[:verbose]
18
+ end
19
+
20
+ desc 'version', 'Print the version of Stubb'
21
+ def version
22
+ puts Stubb::VERSION
23
+ end
24
+ end
25
+ end
26
+
27
+ Stubb::CLI.start
@@ -0,0 +1,30 @@
1
+ module Stubb
2
+
3
+ class CombinedLogger < Rack::CommonLogger
4
+ FORMAT = %{%s - %s [%s] "%s %s%s %s" %d %s %0.4f "%s" "%s"\n}
5
+
6
+ def log(env, status, header, began_at)
7
+ now = Time.now
8
+ length = extract_content_length(header)
9
+
10
+ logger = @logger || env['rack.errors']
11
+ logger.write FORMAT % [
12
+ env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_ADDR'] || '-',
13
+ env['REMOTE_USER'] || '-',
14
+ now.strftime('%d/%b/%Y %H:%M:%S'),
15
+ env['REQUEST_METHOD'],
16
+ env['PATH_INFO'],
17
+ env['QUERY_STRING'].empty? ? '' : '?'+env['QUERY_STRING'],
18
+ env['HTTP_VERSION'],
19
+ status.to_s[0..3],
20
+ length,
21
+ now - began_at,
22
+ header.delete('stubb.response_file') || 'NONE',
23
+ "YAML Frontmatter: #{header.delete('stubb.yaml_frontmatter') || 'No'}"
24
+ ]
25
+
26
+ end
27
+
28
+ end
29
+
30
+ end
@@ -0,0 +1,33 @@
1
+ module Stubb
2
+
3
+ class Counter
4
+
5
+ def initialize(app)
6
+ @app = app
7
+ @request_history = {}
8
+ trap(:INT) { |signal| reset_or_quit signal }
9
+ end
10
+
11
+ def call(env)
12
+ env['stubb.request_sequence_index'] = count(env['REQUEST_METHOD'], env['PATH_INFO'], env['HTTP_ACCEPT'])
13
+ @app.call(env)
14
+ end
15
+
16
+ private
17
+ def count(method, path, accept)
18
+ fingerprint = "#{method}-#{path}-#{accept}"
19
+ @request_history[fingerprint] = (@request_history[fingerprint] || 0) + 1
20
+ end
21
+
22
+ def reset_or_quit(signal)
23
+ if @request_history.empty?
24
+ exit! signal
25
+ else
26
+ @request_history.clear
27
+ puts "\n\nReset request history. Interrupt again to quit.\n\n"
28
+ end
29
+ end
30
+
31
+ end
32
+
33
+ end
@@ -0,0 +1,71 @@
1
+ module Stubb
2
+
3
+ class NotFound < Exception; end
4
+
5
+ class Finder
6
+
7
+ attr_accessor :request, :root
8
+
9
+ def initialize(options = {})
10
+ @root = File.expand_path options[:root] || ''
11
+ @verbose = options[:verbose] || false
12
+ end
13
+
14
+ def call(env)
15
+ @request = Request.new env
16
+
17
+ respond
18
+
19
+ rescue Errno::ENOENT, Errno::ELOOP
20
+ [404, {"Content-Type" => "text/plain"}, ["Not found."]]
21
+ rescue Exception => e
22
+ debug e.message, e.backtrace.join("\n")
23
+ [500, {'Content-Type' => 'text/plain'}, ['Internal server error.']]
24
+ end
25
+
26
+ private
27
+ def respond
28
+ response_file_path = projected_path
29
+ response_body = File.open(response_file_path, 'r') {|f| f.read }
30
+ Response.new(
31
+ response_body,
32
+ request.params,
33
+ 200,
34
+ {'Content-Type' => content_type, 'stubb.response_file' => response_file_path}
35
+ ).finish
36
+ rescue NotFound => e
37
+ debug e.message
38
+ [404, {}, e.message]
39
+ end
40
+
41
+ def request_options_as_file_ending
42
+ "#{request.request_method}#{request.extension}"
43
+ end
44
+
45
+ def exists?(relative_path)
46
+ File.exists? local_path_for(relative_path)
47
+ end
48
+
49
+ def local_path_for(relative_path)
50
+ File.join root, relative_path
51
+ end
52
+
53
+ def content_type
54
+ Rack::Mime.mime_type(request.extension) || "text/html"
55
+ end
56
+
57
+ def debug(*messages)
58
+ log(*messages) if @verbose
59
+ end
60
+
61
+ def log(*messages)
62
+ if request.env['rack.errors'] && request.env['rack.errors'].respond_to?('write')
63
+ request.env['rack.errors'].write messages.join(" ") << "\n"
64
+ else
65
+ puts messages.join(" ")
66
+ end
67
+ end
68
+
69
+ end
70
+
71
+ end
@@ -0,0 +1,62 @@
1
+ module Stubb
2
+
3
+ class NoMatch < NotFound; end
4
+
5
+ class MatchFinder < Finder
6
+ private
7
+ def projected_path
8
+ built_path = []
9
+ last_is_dir = false
10
+ request.path_parts.each_with_index do |level, index|
11
+ if match = literal_directory(built_path, level)
12
+ last_is_dir = true
13
+ elsif match = literal_file(built_path, level)
14
+ last_is_dir = false
15
+ elsif match = matching_directory(built_path)
16
+ last_is_dir = true
17
+ elsif match = matching_file(built_path)
18
+ last_is_dir = false
19
+ else
20
+ raise NoMatch
21
+ end
22
+
23
+ built_path << match
24
+ end
25
+
26
+ if last_is_dir
27
+ File.join local_path_for(built_path), request_options_as_file_ending
28
+ else
29
+ local_path_for built_path
30
+ end
31
+ end
32
+
33
+ def literal_directory(current_path, level)
34
+ File.directory?(local_path_for(current_path + [level])) ? level: nil
35
+ end
36
+
37
+ def literal_file(current_path, level)
38
+ parts = level.split('.')
39
+ level = parts.size > 1 ? parts[0..-2].join('.') : parts.first
40
+ filename = "#{level}.#{request_options_as_file_ending}"
41
+ File.exists?(local_path_for(current_path + [filename])) ? filename : nil
42
+ end
43
+
44
+ def matching_directory(current_path)
45
+ matches = Dir.glob local_path_for(current_path + [Stubb.matcher_pattern])
46
+ for match in matches
47
+ continue unless File.directory? match
48
+ return File.split(match).last
49
+ end
50
+ nil
51
+ end
52
+
53
+ def matching_file(current_path)
54
+ matches = Dir.glob local_path_for(current_path + ["#{Stubb.matcher_pattern}.#{request_options_as_file_ending}"])
55
+
56
+ matches.empty? ? nil : File.split(matches.first).last
57
+ end
58
+
59
+ end
60
+
61
+
62
+ end
@@ -0,0 +1,18 @@
1
+ module Stubb
2
+
3
+ class NaiveFinder < Finder
4
+
5
+ private
6
+ def projected_path
7
+ relative_path = if File.directory? local_path_for(request.resource_path)
8
+ File.join request.resource_path, request_options_as_file_ending
9
+ else
10
+ "#{request.resource_path}.#{request_options_as_file_ending}"
11
+ end
12
+
13
+ local_path_for relative_path
14
+ end
15
+
16
+ end
17
+
18
+ end
@@ -0,0 +1,55 @@
1
+ module Stubb
2
+
3
+ class Request < Rack::Request
4
+
5
+ def path_parts
6
+ relative_path.split '/'
7
+ end
8
+
9
+ def path_dir_parts
10
+ parts = path_parts
11
+ parts.size > 1 ? parts[0..-2] : []
12
+ end
13
+
14
+ def file_name
15
+ path_parts.last
16
+ end
17
+
18
+ def resource_name
19
+ parts = file_name.split('.')
20
+ parts.size > 1 ? parts[0..-2].join('.') : parts.first
21
+ end
22
+
23
+ def resource_path
24
+ File.join((path_dir_parts << resource_name).compact)
25
+ end
26
+
27
+ def extension
28
+ extension_by_path.empty? ? extension_by_header : extension_by_path
29
+ end
30
+
31
+ def extension_by_path
32
+ File.extname(relative_path)
33
+ end
34
+
35
+ def extension_by_header
36
+ Rack::Mime::MIME_TYPES.invert[accept]
37
+ end
38
+
39
+ # TODO parse, sort
40
+ def accept
41
+ @env['HTTP_ACCEPT'].to_s.split(',').first
42
+ end
43
+
44
+ def relative_path
45
+ # Strip slashes at string end and start
46
+ path_info.gsub /(\A\/|\/\Z)/, ''
47
+ end
48
+
49
+ def sequence_index
50
+ @env['stubb.request_sequence_index'] || 1
51
+ end
52
+
53
+ end
54
+
55
+ end
@@ -0,0 +1,46 @@
1
+ require 'erb'
2
+
3
+ module Stubb
4
+
5
+ class Response < Rack::Response
6
+
7
+ attr_accessor :body, :params, :status, :header
8
+
9
+ def initialize(body=[], params={}, status=200, header={})
10
+ @body = body
11
+ @params = params
12
+ @status = status
13
+ @header = header
14
+
15
+ process_yaml
16
+ render_template
17
+
18
+ super self.body, self.status, self.header
19
+ end
20
+
21
+ private
22
+ def process_yaml
23
+ if self.body =~ /^(---\s*\n.*?\n?)^(---\s*$\n?)/m
24
+ self.body = self.body[($1.size + $2.size)..-1]
25
+ begin
26
+ data = YAML.load($1)
27
+
28
+ # Use specified HTTP status
29
+ self.status = data['status'] if data['status']
30
+ # Fill header information
31
+ data['header'].each { |field, value| self.header[field] = value } if data['header'].kind_of? Hash
32
+ self.header['stubb.yaml_frontmatter'] = 'Yes'
33
+ rescue => e
34
+ self.header['stubb.yaml_frontmatter'] = 'Error'
35
+ end
36
+ end
37
+ end
38
+
39
+ def render_template
40
+ erb = ERB.new @body
41
+ @body = erb.result binding
42
+ end
43
+
44
+ end
45
+
46
+ end
@@ -0,0 +1,45 @@
1
+ module Stubb
2
+
3
+ class NoSuchSequence < NotFound; end
4
+
5
+ class SequenceFinder < Finder
6
+ private
7
+ def projected_path
8
+ sequence_members = Dir.glob local_path_for(sequenced_path_pattern)
9
+ raise NoSuchSequence.new("Nothing found for sequence pattern `#{sequenced_path_pattern}`.") if sequence_members.empty?
10
+
11
+ loop? ? pick_loop_member(sequence_members) : pick_stall_member(sequence_members)
12
+ end
13
+
14
+ def pick_loop_member(sequence_members)
15
+ sequence_members[(request.sequence_index - 1) % sequence_members.size]
16
+ end
17
+
18
+ def pick_stall_member(sequence_members)
19
+ request.sequence_index > sequence_members.size ? sequence_members.last : sequence_members[request.sequence_index - 1]
20
+ end
21
+
22
+ def sequenced_path(index)
23
+ if File.directory? local_path_for(request.relative_path)
24
+ File.join request.relative_path, request_options_as_file_ending(index)
25
+ else
26
+ "#{request.relative_path}.#{request_options_as_file_ending(index)}"
27
+ end
28
+ end
29
+
30
+ def sequenced_path_pattern
31
+ sequenced_path('[0-9]')
32
+ end
33
+
34
+ def request_options_as_file_ending(index)
35
+ "#{request.request_method}.#{index}#{request.extension}"
36
+ end
37
+
38
+ def loop?
39
+ exists? sequenced_path(0)
40
+ end
41
+
42
+ end
43
+
44
+
45
+ end
@@ -0,0 +1,58 @@
1
+ module Stubb
2
+
3
+ class SequenceMatchFinder < SequenceFinder
4
+ private
5
+ def sequenced_path(sequence_index)
6
+ built_path = []
7
+ last_is_dir = false
8
+ request.path_parts.each_with_index do |level, index|
9
+ if match = literal_directory(built_path, level)
10
+ last_is_dir = true
11
+ elsif match = literal_file(built_path, level, sequence_index)
12
+ last_is_dir = false
13
+ elsif match = matching_directory(built_path)
14
+ last_is_dir = true
15
+ elsif match = matching_file(built_path, sequence_index)
16
+ last_is_dir = false
17
+ else
18
+ return 'NOT FOUND'
19
+ end
20
+
21
+ built_path << match
22
+ end
23
+
24
+ if last_is_dir
25
+ File.join built_path, request_options_as_file_ending(sequence_index)
26
+ else
27
+ File.join built_path
28
+ end
29
+ end
30
+
31
+ def literal_directory(current_path, level)
32
+ File.directory?(local_path_for(current_path + [level])) ? level: nil
33
+ end
34
+
35
+ def literal_file(current_path, level, index)
36
+ filename = "#{level}.#{request_options_as_file_ending(index)}"
37
+ sequence_members = Dir.glob local_path_for(current_path + [filename])
38
+ sequence_members.empty? ? nil : filename
39
+ end
40
+
41
+ def matching_directory(current_path)
42
+ matches = Dir.glob local_path_for(current_path + [Stubb.matcher_pattern])
43
+ for match in matches
44
+ continue unless File.directory? match
45
+ return File.split(match).last
46
+ end
47
+ nil
48
+ end
49
+
50
+ def matching_file(current_path, index)
51
+ matches = Dir.glob local_path_for(current_path + ["#{Stubb.matcher_pattern}.#{request_options_as_file_ending(index)}"])
52
+
53
+ matches.empty? ? nil : File.split(matches.first).last
54
+ end
55
+
56
+ end
57
+
58
+ end
data/lib/stubb.rb ADDED
@@ -0,0 +1,51 @@
1
+ require 'rack'
2
+
3
+ module Stubb
4
+
5
+ VERSION = '0.1.rc.1'
6
+
7
+ class ResponseNotFound < Exception
8
+ end
9
+
10
+ @config = {
11
+ :matcher_pattern => '_*_'
12
+ }
13
+
14
+ def self.method_missing(m, *attrs)
15
+ # Ease access to @config
16
+ if @config[m.to_sym]
17
+ @config[m.to_sym]
18
+ elsif @config[m.to_s.chomp('=').to_sym]
19
+ @config[m.to_s.chomp('=').to_sym] = attrs[0]
20
+ else
21
+ super
22
+ end
23
+ end
24
+
25
+ def self.app(options = {})
26
+ Rack::Builder.new {
27
+ use CombinedLogger
28
+ use Counter
29
+
30
+ run Rack::Cascade.new([SequenceFinder.new(options), NaiveFinder.new(options), SequenceMatchFinder.new(options), MatchFinder.new(options)])
31
+ }.to_app
32
+ end
33
+
34
+ def self.run(options = {})
35
+ Rack::Handler.default.run(
36
+ app({:root => ''}.update(options)),
37
+ {:Port => 4040}.update(options)
38
+ )
39
+ end
40
+
41
+ end
42
+
43
+ require 'stubb/request'
44
+ require 'stubb/response'
45
+ require 'stubb/counter'
46
+ require 'stubb/combined_logger'
47
+ require 'stubb/finder'
48
+ require 'stubb/naive_finder'
49
+ require 'stubb/sequence_finder'
50
+ require 'stubb/match_finder'
51
+ require 'stubb/sequence_match_finder'
metadata ADDED
@@ -0,0 +1,125 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: stubb
3
+ version: !ruby/object:Gem::Version
4
+ hash: 15424239
5
+ prerelease: true
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - rc
10
+ - 1
11
+ version: 0.1.rc.1
12
+ platform: ruby
13
+ authors:
14
+ - Johannes Emerich
15
+ autorequire:
16
+ bindir: bin
17
+ cert_chain: []
18
+
19
+ date: 2012-02-24 00:00:00 +01:00
20
+ default_executable:
21
+ dependencies:
22
+ - !ruby/object:Gem::Dependency
23
+ version_requirements: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 3
29
+ segments:
30
+ - 0
31
+ version: "0"
32
+ requirement: *id001
33
+ name: rake
34
+ type: :development
35
+ prerelease: false
36
+ - !ruby/object:Gem::Dependency
37
+ version_requirements: &id002 !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ hash: 31
43
+ segments:
44
+ - 1
45
+ - 2
46
+ - 0
47
+ version: 1.2.0
48
+ requirement: *id002
49
+ name: rack
50
+ type: :runtime
51
+ prerelease: false
52
+ - !ruby/object:Gem::Dependency
53
+ version_requirements: &id003 !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ hash: 3
59
+ segments:
60
+ - 0
61
+ version: "0"
62
+ requirement: *id003
63
+ name: thor
64
+ type: :runtime
65
+ prerelease: false
66
+ description: Stubb is the second mate.
67
+ email: johannes@emerich.de
68
+ executables:
69
+ - stubb
70
+ extensions: []
71
+
72
+ extra_rdoc_files: []
73
+
74
+ files:
75
+ - lib/stubb.rb
76
+ - lib/stubb/request.rb
77
+ - lib/stubb/response.rb
78
+ - lib/stubb/counter.rb
79
+ - lib/stubb/combined_logger.rb
80
+ - lib/stubb/finder.rb
81
+ - lib/stubb/naive_finder.rb
82
+ - lib/stubb/sequence_finder.rb
83
+ - lib/stubb/match_finder.rb
84
+ - lib/stubb/sequence_match_finder.rb
85
+ - bin/stubb
86
+ - LICENSE
87
+ - README.markdown
88
+ has_rdoc: true
89
+ homepage: http://github.com/knuton/stubb
90
+ licenses: []
91
+
92
+ post_install_message:
93
+ rdoc_options: []
94
+
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ none: false
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ hash: 3
103
+ segments:
104
+ - 0
105
+ version: "0"
106
+ required_rubygems_version: !ruby/object:Gem::Requirement
107
+ none: false
108
+ requirements:
109
+ - - ">"
110
+ - !ruby/object:Gem::Version
111
+ hash: 25
112
+ segments:
113
+ - 1
114
+ - 3
115
+ - 1
116
+ version: 1.3.1
117
+ requirements: []
118
+
119
+ rubyforge_project:
120
+ rubygems_version: 1.3.7
121
+ signing_key:
122
+ specification_version: 3
123
+ summary: Specify REST API stubs using your file system
124
+ test_files: []
125
+