holoserve 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.
- data/LICENSE +19 -0
- data/README.rdoc +131 -0
- data/Rakefile +53 -0
- data/bin/holoserve +30 -0
- data/lib/holoserve/bucket.rb +10 -0
- data/lib/holoserve/configuration.rb +48 -0
- data/lib/holoserve/history.rb +14 -0
- data/lib/holoserve/interface/control.rb +74 -0
- data/lib/holoserve/interface/fake.rb +47 -0
- data/lib/holoserve/interface.rb +7 -0
- data/lib/holoserve/pair/finder.rb +21 -0
- data/lib/holoserve/pair.rb +6 -0
- data/lib/holoserve/request/decomposer.rb +65 -0
- data/lib/holoserve/request/matcher.rb +61 -0
- data/lib/holoserve/request.rb +7 -0
- data/lib/holoserve/response/composer.rb +35 -0
- data/lib/holoserve/response.rb +6 -0
- data/lib/holoserve/runner.rb +68 -0
- data/lib/holoserve/tool/hash/key_symbolizer.rb +41 -0
- data/lib/holoserve/tool/hash.rb +6 -0
- data/lib/holoserve/tool/uploader.rb +36 -0
- data/lib/holoserve/tool.rb +7 -0
- data/lib/holoserve.rb +59 -0
- metadata +159 -0
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (C) 2011 Skrill Holdings Ltd
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
4
|
+
this software and associated documentation files (the "Software"), to deal in
|
5
|
+
the Software without restriction, including without limitation the rights to
|
6
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
7
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
8
|
+
so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
11
|
+
copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
19
|
+
SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
|
2
|
+
= HoloServe - Simple faking of HTTP APIs
|
3
|
+
|
4
|
+
This tool can be used to fake HTTP web APIs. It's meant to be used in a testing environment, to make the test suite
|
5
|
+
run faster and be independent from other API and network problems.
|
6
|
+
|
7
|
+
== Concept
|
8
|
+
|
9
|
+
HoloServe runs a rack application server, that matches any incoming request to a list of request profiles defined in
|
10
|
+
the server layout. If a match is found, the defined static response is returned. The name of the matched
|
11
|
+
request/response pair is saved in a request history. If no match is found, a 404 is returned and the
|
12
|
+
request data is stored in the bucket for unhandeled requests. These informations can be used to extend the server layout
|
13
|
+
with missing request handlers.
|
14
|
+
|
15
|
+
Layouts, history and bucket can be accessed via control routes, which are described below.
|
16
|
+
|
17
|
+
== Control routes
|
18
|
+
|
19
|
+
=== POST /_control/layouts
|
20
|
+
|
21
|
+
It should receive a parameter named <tt>file</tt> that contains an uploaded YML file with the server layouts. See
|
22
|
+
{YML server layouts file format}[rdoc-label:YML-server-layouts-file-format] below.
|
23
|
+
|
24
|
+
=== DELETE /_control/layouts
|
25
|
+
|
26
|
+
Removes all server layouts.
|
27
|
+
|
28
|
+
=== GET /_control/layouts/ids
|
29
|
+
|
30
|
+
Returns the list of ids of all server layouts.
|
31
|
+
|
32
|
+
==== Response example
|
33
|
+
|
34
|
+
[ "one", "two" ]
|
35
|
+
|
36
|
+
=== PUT /_control/layouts/:id/current
|
37
|
+
|
38
|
+
Makes the layout with id <tt>:id</tt> the currently active one.
|
39
|
+
|
40
|
+
=== GET /_control/layouts/current
|
41
|
+
|
42
|
+
Returns the id of the current layout.
|
43
|
+
|
44
|
+
==== Response example
|
45
|
+
|
46
|
+
one
|
47
|
+
|
48
|
+
=== GET /_control/bucket/requests
|
49
|
+
|
50
|
+
Returns a list of all requests that has been received, but couldn't be handled.
|
51
|
+
|
52
|
+
==== Response example
|
53
|
+
|
54
|
+
[
|
55
|
+
{
|
56
|
+
"method": "POST",
|
57
|
+
"path": "test",
|
58
|
+
"headers": {
|
59
|
+
"REMOTE_ADDR": "127.0.0.1",
|
60
|
+
"REQUEST_METHOD": "GET",
|
61
|
+
"REQUEST_PATH": "/test",
|
62
|
+
"PATH_INFO": "/test",
|
63
|
+
"REQUEST_URI": "/test",
|
64
|
+
"SERVER_PROTOCOL": "HTTP/1.1",
|
65
|
+
"HTTP_VERSION": "HTTP/1.1",
|
66
|
+
"HTTP_ACCEPT": "*/*",
|
67
|
+
"HTTP_USER_AGENT": "Ruby",
|
68
|
+
"HTTP_HOST": "localhost:8080",
|
69
|
+
"SERVER_NAME": "localhost",
|
70
|
+
"SERVER_PORT": "8080",
|
71
|
+
"QUERY_STRING": "",
|
72
|
+
"SCRIPT_NAME": "",
|
73
|
+
"SERVER_SOFTWARE": "Unicorn 4.1.1"
|
74
|
+
}
|
75
|
+
}
|
76
|
+
]
|
77
|
+
|
78
|
+
=== GET /_control/history
|
79
|
+
|
80
|
+
Returns a list of all names of pairs that has been triggered.
|
81
|
+
|
82
|
+
==== Response example
|
83
|
+
|
84
|
+
[ "create_received", "update_received" ]
|
85
|
+
|
86
|
+
=== DELETE /_control/history
|
87
|
+
|
88
|
+
Removes all entries from the history.
|
89
|
+
|
90
|
+
== YML server layouts file format
|
91
|
+
|
92
|
+
The server layouts file should have the following format.
|
93
|
+
|
94
|
+
one:
|
95
|
+
-
|
96
|
+
name: "test_received"
|
97
|
+
request:
|
98
|
+
method: "POST"
|
99
|
+
path: "/test"
|
100
|
+
headers:
|
101
|
+
HTTP_USER_AGENT: "Ruby"
|
102
|
+
HTTP_AUTHORIZATION: "OAuth oauth_token=12345"
|
103
|
+
body:
|
104
|
+
"test=value"
|
105
|
+
parameters:
|
106
|
+
test: "value"
|
107
|
+
oauth:
|
108
|
+
oauth_token: "12345"
|
109
|
+
response:
|
110
|
+
status: 200
|
111
|
+
body:
|
112
|
+
"ok"
|
113
|
+
-
|
114
|
+
request:
|
115
|
+
method: "GET"
|
116
|
+
path: "/test"
|
117
|
+
response:
|
118
|
+
status: 200
|
119
|
+
body:
|
120
|
+
"ok too"
|
121
|
+
|
122
|
+
This example would define a server layout named <tt>one</tt> that has two request/response pairs. The first pair would
|
123
|
+
have the name <tt>test_received</tt> and would match a <tt>POST</tt> request to the path <tt>/test</tt>.
|
124
|
+
|
125
|
+
Notice, that the given request attributes are the _minimal_ values that have to match the incomong one. An incoming
|
126
|
+
request may has much more attributes (see bucket example above). If a request is matched, the corresponding pair name is
|
127
|
+
placed in the history.
|
128
|
+
|
129
|
+
As the sections <tt>headers</tt> and <tt>body</tt> are raw values from the request, the sections <tt>parameters</tt> and
|
130
|
+
<tt>oauth</tt> contain high-level values. The <tt>parameters</tt> hash is taken from the request body or the query and
|
131
|
+
the hash containing the OAuth values is parsed from a fitting <tt>HTTP_AUTHORIZATION</tt> header.
|
data/Rakefile
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
|
4
|
+
desc "Runs the unicorn interface server."
|
5
|
+
task :server do
|
6
|
+
system "bundle exec unicorn"
|
7
|
+
end
|
8
|
+
|
9
|
+
desc "Runs the shotgun interface server."
|
10
|
+
task :shotgun do
|
11
|
+
system "bundle exec shotgun"
|
12
|
+
end
|
13
|
+
|
14
|
+
begin
|
15
|
+
require 'cucumber'
|
16
|
+
require 'cucumber/rake/task'
|
17
|
+
|
18
|
+
Cucumber::Rake::Task.new(:features) do |task|
|
19
|
+
task.cucumber_opts = "features --format pretty"
|
20
|
+
end
|
21
|
+
|
22
|
+
namespace :features do
|
23
|
+
Cucumber::Rake::Task.new(:wip) do |task|
|
24
|
+
task.cucumber_opts = "features --format pretty --tags @wip"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
rescue LoadError
|
28
|
+
end
|
29
|
+
|
30
|
+
begin
|
31
|
+
require 'rdoc'
|
32
|
+
require 'rdoc/task'
|
33
|
+
|
34
|
+
Rake::RDocTask.new do |rdoc|
|
35
|
+
rdoc.main = "README.rdoc"
|
36
|
+
rdoc.rdoc_files.include("README.rdoc", "lib/**/*.rb")
|
37
|
+
end
|
38
|
+
rescue LoadError
|
39
|
+
end
|
40
|
+
|
41
|
+
namespace :gem do
|
42
|
+
|
43
|
+
desc "Builds the gem"
|
44
|
+
task :build do
|
45
|
+
system "gem build *.gemspec && mkdir -p pkg/ && mv *.gem pkg/"
|
46
|
+
end
|
47
|
+
|
48
|
+
desc "Builds and installs the gem"
|
49
|
+
task :install => :build do
|
50
|
+
system "gem install pkg/"
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
data/bin/holoserve
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'optparse'
|
6
|
+
|
7
|
+
require File.expand_path(File.join(File.dirname(__FILE__), "..", "lib", "holoserve"))
|
8
|
+
|
9
|
+
options = { }
|
10
|
+
OptionParser.new do |parser|
|
11
|
+
parser.banner = "Usage: holoserve [options]"
|
12
|
+
parser.separator ""
|
13
|
+
parser.separator "Options:"
|
14
|
+
parser.on("-p", "--port PORT", Integer, "The port holoserve should listen to.") do |value|
|
15
|
+
options[:port] = value
|
16
|
+
end
|
17
|
+
parser.on("-f", "--layouts-file FILE", "Load the specified layouts file on startup.") do |value|
|
18
|
+
options[:layouts_filename] = value
|
19
|
+
end
|
20
|
+
parser.on("-l", "--layout LAYOUT", "Selects the specified layout as the current one.") do |value|
|
21
|
+
options[:layout] = value
|
22
|
+
end
|
23
|
+
parser.on_tail("-h", "--help", "Shows the help message.") do
|
24
|
+
puts parser
|
25
|
+
exit
|
26
|
+
end
|
27
|
+
end.parse!(ARGV)
|
28
|
+
|
29
|
+
runner = Holoserve::Runner.new options
|
30
|
+
runner.run
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
class Holoserve::Configuration
|
4
|
+
|
5
|
+
attr_reader :logger
|
6
|
+
|
7
|
+
attr_reader :layouts
|
8
|
+
attr_reader :layout_id
|
9
|
+
|
10
|
+
def initialize(logger)
|
11
|
+
@logger = logger
|
12
|
+
end
|
13
|
+
|
14
|
+
def layouts=(hash)
|
15
|
+
@layouts = Holoserve::Tool::Hash::KeySymbolizer.new(hash).hash
|
16
|
+
end
|
17
|
+
|
18
|
+
def layout_id=(value)
|
19
|
+
@layout_id = value.to_sym
|
20
|
+
logger.info "made '#{value}' the current layout"
|
21
|
+
end
|
22
|
+
|
23
|
+
def clear_layouts!
|
24
|
+
self.layouts = nil
|
25
|
+
end
|
26
|
+
|
27
|
+
def layout_ids
|
28
|
+
self.layouts ? self.layouts.keys : [ ]
|
29
|
+
end
|
30
|
+
|
31
|
+
def layout_id?(id)
|
32
|
+
self.layout_ids.include? id.to_sym
|
33
|
+
end
|
34
|
+
|
35
|
+
def layout
|
36
|
+
self.layouts ? self.layouts[self.layout_id] : nil
|
37
|
+
end
|
38
|
+
|
39
|
+
def load_layouts_from_yml_file(file)
|
40
|
+
self.layouts = YAML::load_file file
|
41
|
+
logger.info "loaded layouts from file #{file.path}"
|
42
|
+
logger.info "available layout(s): #{self.layout_ids.join(", ")}"
|
43
|
+
rescue Psych::SyntaxError => error
|
44
|
+
self.clear_layouts!
|
45
|
+
raise error
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'sinatra'
|
2
|
+
require 'yaml'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
class Holoserve::Interface::Control < Sinatra::Base
|
6
|
+
|
7
|
+
post "/_control/layouts" do
|
8
|
+
begin
|
9
|
+
configuration.load_layouts_from_yml_file params["file"][:tempfile]
|
10
|
+
respond_json_acknowledgement
|
11
|
+
rescue Psych::SyntaxError => error
|
12
|
+
error 400, error.inspect
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
delete "/_control/layouts" do
|
17
|
+
configuration.clear_layouts!
|
18
|
+
respond_json_acknowledgement
|
19
|
+
end
|
20
|
+
|
21
|
+
get "/_control/layouts/ids" do
|
22
|
+
respond_json configuration.layout_ids
|
23
|
+
end
|
24
|
+
|
25
|
+
put "/_control/layouts/:id/current" do |id|
|
26
|
+
if configuration.layout_id?(id)
|
27
|
+
configuration.layout_id = id
|
28
|
+
respond_json_acknowledgement
|
29
|
+
else
|
30
|
+
not_found
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
get "/_control/layouts/current" do
|
35
|
+
configuration.layout_id.to_s
|
36
|
+
end
|
37
|
+
|
38
|
+
get "/_control/bucket/requests" do
|
39
|
+
respond_json bucket.requests
|
40
|
+
end
|
41
|
+
|
42
|
+
get "/_control/history" do
|
43
|
+
respond_json history.pair_names
|
44
|
+
end
|
45
|
+
|
46
|
+
delete "/_control/history" do
|
47
|
+
history.clear!
|
48
|
+
respond_json_acknowledgement
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def respond_json_acknowledgement
|
54
|
+
respond_json :ok => true
|
55
|
+
end
|
56
|
+
|
57
|
+
def respond_json(object)
|
58
|
+
content_type "application/json"
|
59
|
+
JSON.dump object
|
60
|
+
end
|
61
|
+
|
62
|
+
def bucket
|
63
|
+
Holoserve.instance.bucket
|
64
|
+
end
|
65
|
+
|
66
|
+
def history
|
67
|
+
Holoserve.instance.history
|
68
|
+
end
|
69
|
+
|
70
|
+
def configuration
|
71
|
+
Holoserve.instance.configuration
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'pp'
|
2
|
+
|
3
|
+
class Holoserve::Interface::Fake
|
4
|
+
|
5
|
+
def call(env)
|
6
|
+
request = Holoserve::Request::Decomposer.new(env).hash
|
7
|
+
pair = Holoserve::Pair::Finder.new(configuration, request).pair
|
8
|
+
if pair
|
9
|
+
if name = pair[:name]
|
10
|
+
history.pair_names << name
|
11
|
+
logger.info "received handled request with name '#{name}'"
|
12
|
+
end
|
13
|
+
Holoserve::Response::Composer.new(pair[:response]).response_array
|
14
|
+
else
|
15
|
+
bucket.requests << request
|
16
|
+
logger.error "received unhandled request\n" + request.pretty_inspect
|
17
|
+
not_found
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def not_found
|
24
|
+
[ 404, { "Content-Type" => "text/plain" }, [ "no response found for this request" ] ]
|
25
|
+
end
|
26
|
+
|
27
|
+
def logger
|
28
|
+
configuration.logger
|
29
|
+
end
|
30
|
+
|
31
|
+
def layout
|
32
|
+
configuration.layout
|
33
|
+
end
|
34
|
+
|
35
|
+
def bucket
|
36
|
+
Holoserve.instance.bucket
|
37
|
+
end
|
38
|
+
|
39
|
+
def history
|
40
|
+
Holoserve.instance.history
|
41
|
+
end
|
42
|
+
|
43
|
+
def configuration
|
44
|
+
Holoserve.instance.configuration
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
|
2
|
+
class Holoserve::Pair::Finder
|
3
|
+
|
4
|
+
def initialize(configuration, request)
|
5
|
+
@configuration, @request = configuration, request
|
6
|
+
end
|
7
|
+
|
8
|
+
def pair
|
9
|
+
return nil unless layout
|
10
|
+
layout.detect do |pair|
|
11
|
+
Holoserve::Request::Matcher.new(@request, pair[:request]).match?
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def layout
|
18
|
+
@configuration.layout
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
|
2
|
+
class Holoserve::Request::Decomposer
|
3
|
+
|
4
|
+
def initialize(request)
|
5
|
+
@request = request
|
6
|
+
end
|
7
|
+
|
8
|
+
def hash
|
9
|
+
hash = {
|
10
|
+
:method => @request["REQUEST_METHOD"],
|
11
|
+
:path => @request["PATH_INFO"]
|
12
|
+
}
|
13
|
+
hash.merge! :headers => headers unless headers.empty?
|
14
|
+
hash.merge! :body => body unless body.nil?
|
15
|
+
hash.merge! :parameters => parameters unless parameters.empty?
|
16
|
+
hash.merge! :oauth => oauth unless oauth.empty?
|
17
|
+
hash
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def headers
|
23
|
+
headers = { }
|
24
|
+
@request.each do |key, value|
|
25
|
+
headers[ key.to_sym ] = value unless key =~ /^rack\./
|
26
|
+
end
|
27
|
+
headers
|
28
|
+
end
|
29
|
+
|
30
|
+
def body
|
31
|
+
@body ||= begin
|
32
|
+
body = @request["rack.input"].read
|
33
|
+
body && body != "" ? body : nil
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def parameters
|
38
|
+
Holoserve::Tool::Hash::KeySymbolizer.new(query_hash.merge(form_hash)).hash
|
39
|
+
end
|
40
|
+
|
41
|
+
def query_hash
|
42
|
+
@request["rack.request.query_hash"] || { }
|
43
|
+
end
|
44
|
+
|
45
|
+
def form_hash
|
46
|
+
@request["rack.request.form_hash"] || { }
|
47
|
+
end
|
48
|
+
|
49
|
+
def oauth
|
50
|
+
@oauth ||= begin
|
51
|
+
oauth = { }
|
52
|
+
http_authorization = @request["HTTP_AUTHORIZATION"]
|
53
|
+
if http_authorization && http_authorization =~ /^OAuth/
|
54
|
+
http_authorization = http_authorization.sub /^OAuth/, ""
|
55
|
+
pairs = http_authorization.split ","
|
56
|
+
pairs.each do |pair|
|
57
|
+
key, value = *pair.strip.split("=")
|
58
|
+
oauth[ key.to_sym ] = value.sub(/^\\"/, "").sub(/\\"$/, "").gsub(/"/, "")
|
59
|
+
end
|
60
|
+
end
|
61
|
+
oauth
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
|
2
|
+
class Holoserve::Request::Matcher
|
3
|
+
|
4
|
+
def initialize(request, request_subset)
|
5
|
+
@request, @request_subset = request, request_subset
|
6
|
+
end
|
7
|
+
|
8
|
+
def match?
|
9
|
+
match_method? &&
|
10
|
+
match_path? &&
|
11
|
+
match_headers? &&
|
12
|
+
match_body? &&
|
13
|
+
match_parameters? &&
|
14
|
+
match_oauth?
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def match_method?
|
20
|
+
@request_subset[:method] ?
|
21
|
+
@request[:method] == @request_subset[:method] :
|
22
|
+
true
|
23
|
+
end
|
24
|
+
|
25
|
+
def match_path?
|
26
|
+
@request_subset[:path] ?
|
27
|
+
@request[:path] == @request_subset[:path] :
|
28
|
+
true
|
29
|
+
end
|
30
|
+
|
31
|
+
def match_headers?
|
32
|
+
match = true
|
33
|
+
(@request_subset[:headers] || { }).each do |key, value|
|
34
|
+
match &&= @request[:headers][key] == value
|
35
|
+
end
|
36
|
+
match
|
37
|
+
end
|
38
|
+
|
39
|
+
def match_body?
|
40
|
+
@request_subset[:body] ?
|
41
|
+
@request[:body] == @request_subset[:body] :
|
42
|
+
true
|
43
|
+
end
|
44
|
+
|
45
|
+
def match_parameters?
|
46
|
+
match = true
|
47
|
+
(@request_subset[:parameters] || { }).each do |key, value|
|
48
|
+
match &&= @request[:parameters][key] == value
|
49
|
+
end
|
50
|
+
match
|
51
|
+
end
|
52
|
+
|
53
|
+
def match_oauth?
|
54
|
+
match = true
|
55
|
+
(@request_subset[:oauth] || { }).each do |key, value|
|
56
|
+
match &&= @request[:oauth][key] == value
|
57
|
+
end
|
58
|
+
match
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
class Holoserve::Response::Composer
|
4
|
+
|
5
|
+
def initialize(response)
|
6
|
+
@response = response
|
7
|
+
end
|
8
|
+
|
9
|
+
def response_array
|
10
|
+
[ status, headers, [ body ] ]
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def status
|
16
|
+
@response[:status]
|
17
|
+
end
|
18
|
+
|
19
|
+
def headers
|
20
|
+
@response[:headers] || { }
|
21
|
+
end
|
22
|
+
|
23
|
+
def body
|
24
|
+
@body ||= begin
|
25
|
+
if @response.has_key?(:body)
|
26
|
+
@response[:body]
|
27
|
+
elsif @response.has_key?(:json)
|
28
|
+
JSON.dump @response[:json]
|
29
|
+
else
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'unicorn'
|
2
|
+
require 'transport'
|
3
|
+
|
4
|
+
class Holoserve::Runner
|
5
|
+
|
6
|
+
attr_reader :port
|
7
|
+
|
8
|
+
def initialize(options = { })
|
9
|
+
@port = options[:port] || 4250
|
10
|
+
@layouts_filename = options[:layouts_filename]
|
11
|
+
@layout = options[:layout]
|
12
|
+
|
13
|
+
@rackup_options = Unicorn::Configurator::RACKUP
|
14
|
+
@rackup_options[:port] = @port
|
15
|
+
@rackup_options[:set_listener] = true
|
16
|
+
@options = @rackup_options[:options]
|
17
|
+
|
18
|
+
@unicorn = Unicorn::HttpServer.new rack, @options
|
19
|
+
end
|
20
|
+
|
21
|
+
def start
|
22
|
+
@unicorn.start
|
23
|
+
upload_layouts if @layouts_filename
|
24
|
+
set_layout if @layout
|
25
|
+
end
|
26
|
+
|
27
|
+
def join
|
28
|
+
return unless @unicorn
|
29
|
+
@unicorn.join
|
30
|
+
end
|
31
|
+
|
32
|
+
def stop
|
33
|
+
return unless @unicorn
|
34
|
+
@unicorn.stop
|
35
|
+
@unicorn = nil
|
36
|
+
end
|
37
|
+
|
38
|
+
def run
|
39
|
+
self.start
|
40
|
+
self.join
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def upload_layouts
|
46
|
+
Holoserve::Tool::Uploader.new(
|
47
|
+
@layouts_filename,
|
48
|
+
:post,
|
49
|
+
"http://localhost:#{port}/_control/layouts",
|
50
|
+
:expected_status_code => 200
|
51
|
+
).upload
|
52
|
+
nil
|
53
|
+
end
|
54
|
+
|
55
|
+
def set_layout
|
56
|
+
Transport::JSON.request :put, "http://localhost:#{port}/_control/layouts/#{@layout}/current", :expected_status_code => 200
|
57
|
+
nil
|
58
|
+
end
|
59
|
+
|
60
|
+
def rack
|
61
|
+
instance.rack
|
62
|
+
end
|
63
|
+
|
64
|
+
def instance
|
65
|
+
Holoserve.instance
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
|
2
|
+
class Holoserve::Tool::Hash::KeySymbolizer
|
3
|
+
|
4
|
+
def initialize(hash)
|
5
|
+
@hash = hash
|
6
|
+
end
|
7
|
+
|
8
|
+
def hash
|
9
|
+
symbolize_keys @hash
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def symbolize_keys(hash)
|
15
|
+
return nil unless hash.is_a?(Hash)
|
16
|
+
result = { }
|
17
|
+
hash.each do |key, value|
|
18
|
+
result[ key.to_sym ] = if value.is_a?(Hash)
|
19
|
+
symbolize_keys value
|
20
|
+
elsif value.is_a?(Array)
|
21
|
+
symbolize_keys_of_all value
|
22
|
+
else
|
23
|
+
value
|
24
|
+
end
|
25
|
+
end
|
26
|
+
result
|
27
|
+
end
|
28
|
+
|
29
|
+
def symbolize_keys_of_all(array)
|
30
|
+
array.map do |item|
|
31
|
+
if item.is_a?(Hash)
|
32
|
+
symbolize_keys item
|
33
|
+
elsif item.is_a?(Array)
|
34
|
+
symbolize_keys_of_all item
|
35
|
+
else
|
36
|
+
item
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'transport'
|
2
|
+
|
3
|
+
class Holoserve::Tool::Uploader
|
4
|
+
|
5
|
+
def initialize(filename, http_method, url, options = { })
|
6
|
+
@filename, @http_method, @url, @options = filename, http_method, url, options
|
7
|
+
end
|
8
|
+
|
9
|
+
def upload
|
10
|
+
options = @options
|
11
|
+
options[:headers] = (@options[:headers] || { }).merge(headers)
|
12
|
+
options[:body] = body
|
13
|
+
Transport::HTTP.request @http_method, @url, options
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def headers
|
19
|
+
{ "Content-Type" => "multipart/form-data, boundary=#{boundary}" }
|
20
|
+
end
|
21
|
+
|
22
|
+
def body
|
23
|
+
"--#{boundary}\r\n" +
|
24
|
+
"Content-Disposition: form-data; name=\"file\"; filename=\"#{File.basename(@filename)}\"\r\n" +
|
25
|
+
"Content-Type: application/x-yaml\r\n" +
|
26
|
+
"\r\n" +
|
27
|
+
File.read(@filename) +
|
28
|
+
"\r\n" +
|
29
|
+
"--#{boundary}--\r\n"
|
30
|
+
end
|
31
|
+
|
32
|
+
def boundary
|
33
|
+
"xxx12345xxx"
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
data/lib/holoserve.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'rack/builder'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
class Holoserve
|
5
|
+
|
6
|
+
autoload :Bucket, File.join(File.dirname(__FILE__), "holoserve", "bucket")
|
7
|
+
autoload :Configuration, File.join(File.dirname(__FILE__), "holoserve", "configuration")
|
8
|
+
autoload :History, File.join(File.dirname(__FILE__), "holoserve", "history")
|
9
|
+
autoload :Interface, File.join(File.dirname(__FILE__), "holoserve", "interface")
|
10
|
+
autoload :Pair, File.join(File.dirname(__FILE__), "holoserve", "pair")
|
11
|
+
autoload :Request, File.join(File.dirname(__FILE__), "holoserve", "request")
|
12
|
+
autoload :Response, File.join(File.dirname(__FILE__), "holoserve", "response")
|
13
|
+
autoload :Runner, File.join(File.dirname(__FILE__), "holoserve", "runner")
|
14
|
+
autoload :Tool, File.join(File.dirname(__FILE__), "holoserve", "tool")
|
15
|
+
|
16
|
+
attr_reader :logger
|
17
|
+
attr_reader :configuration
|
18
|
+
attr_reader :bucket
|
19
|
+
attr_reader :history
|
20
|
+
attr_reader :rack
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
initialize_logger
|
24
|
+
initialize_configuration
|
25
|
+
initialize_bucket
|
26
|
+
initialize_history
|
27
|
+
initialize_rack
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def initialize_logger
|
33
|
+
@logger = Logger.new STDOUT
|
34
|
+
end
|
35
|
+
|
36
|
+
def initialize_configuration
|
37
|
+
@configuration = Configuration.new @logger
|
38
|
+
end
|
39
|
+
|
40
|
+
def initialize_bucket
|
41
|
+
@bucket = Bucket.new
|
42
|
+
end
|
43
|
+
|
44
|
+
def initialize_history
|
45
|
+
@history = History.new
|
46
|
+
end
|
47
|
+
|
48
|
+
def initialize_rack
|
49
|
+
@rack = Rack::Builder.new do
|
50
|
+
use Interface::Control
|
51
|
+
run Interface::Fake.new
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.instance
|
56
|
+
@instance ||= self.new
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
metadata
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: holoserve
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Philipp Brüll
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-12-19 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rack
|
16
|
+
requirement: &70258362918600 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70258362918600
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: sinatra
|
27
|
+
requirement: &70258366899940 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70258366899940
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: unicorn
|
38
|
+
requirement: &70258366896080 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70258366896080
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: transport
|
49
|
+
requirement: &70258366894700 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :runtime
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70258366894700
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: cucumber
|
60
|
+
requirement: &70258366892660 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *70258366892660
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: &70258366891240 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ! '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: *70258366891240
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
name: transport
|
82
|
+
requirement: &70258366890200 !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
type: :development
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: *70258366890200
|
91
|
+
- !ruby/object:Gem::Dependency
|
92
|
+
name: oauth
|
93
|
+
requirement: &70258366889100 !ruby/object:Gem::Requirement
|
94
|
+
none: false
|
95
|
+
requirements:
|
96
|
+
- - ! '>='
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
type: :development
|
100
|
+
prerelease: false
|
101
|
+
version_requirements: *70258366889100
|
102
|
+
description: This tool can be used to fake webservice APIs for testing proposals.
|
103
|
+
email: philipp.bruell@skrill.com
|
104
|
+
executables: []
|
105
|
+
extensions: []
|
106
|
+
extra_rdoc_files:
|
107
|
+
- README.rdoc
|
108
|
+
files:
|
109
|
+
- README.rdoc
|
110
|
+
- LICENSE
|
111
|
+
- Rakefile
|
112
|
+
- bin/holoserve
|
113
|
+
- lib/holoserve/bucket.rb
|
114
|
+
- lib/holoserve/configuration.rb
|
115
|
+
- lib/holoserve/history.rb
|
116
|
+
- lib/holoserve/interface/control.rb
|
117
|
+
- lib/holoserve/interface/fake.rb
|
118
|
+
- lib/holoserve/interface.rb
|
119
|
+
- lib/holoserve/pair/finder.rb
|
120
|
+
- lib/holoserve/pair.rb
|
121
|
+
- lib/holoserve/request/decomposer.rb
|
122
|
+
- lib/holoserve/request/matcher.rb
|
123
|
+
- lib/holoserve/request.rb
|
124
|
+
- lib/holoserve/response/composer.rb
|
125
|
+
- lib/holoserve/response.rb
|
126
|
+
- lib/holoserve/runner.rb
|
127
|
+
- lib/holoserve/tool/hash/key_symbolizer.rb
|
128
|
+
- lib/holoserve/tool/hash.rb
|
129
|
+
- lib/holoserve/tool/uploader.rb
|
130
|
+
- lib/holoserve/tool.rb
|
131
|
+
- lib/holoserve.rb
|
132
|
+
homepage: http://github.com/skrill/holoserve
|
133
|
+
licenses: []
|
134
|
+
post_install_message:
|
135
|
+
rdoc_options: []
|
136
|
+
require_paths:
|
137
|
+
- lib
|
138
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
139
|
+
none: false
|
140
|
+
requirements:
|
141
|
+
- - ! '>='
|
142
|
+
- !ruby/object:Gem::Version
|
143
|
+
version: '0'
|
144
|
+
segments:
|
145
|
+
- 0
|
146
|
+
hash: 3189243162952921439
|
147
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
148
|
+
none: false
|
149
|
+
requirements:
|
150
|
+
- - ! '>='
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
requirements: []
|
154
|
+
rubyforge_project: holoserve
|
155
|
+
rubygems_version: 1.8.10
|
156
|
+
signing_key:
|
157
|
+
specification_version: 3
|
158
|
+
summary: Tool to fake HTTP APIs.
|
159
|
+
test_files: []
|