stubb 0.1.rc.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +7 -0
- data/README.markdown +159 -0
- data/bin/stubb +27 -0
- data/lib/stubb/combined_logger.rb +30 -0
- data/lib/stubb/counter.rb +33 -0
- data/lib/stubb/finder.rb +71 -0
- data/lib/stubb/match_finder.rb +62 -0
- data/lib/stubb/naive_finder.rb +18 -0
- data/lib/stubb/request.rb +55 -0
- data/lib/stubb/response.rb +46 -0
- data/lib/stubb/sequence_finder.rb +45 -0
- data/lib/stubb/sequence_match_finder.rb +58 -0
- data/lib/stubb.rb +51 -0
- metadata +125 -0
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
|
data/lib/stubb/finder.rb
ADDED
@@ -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
|
+
|