holoserve 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -6,18 +6,17 @@ run faster and be independent from other API and network problems.
6
6
 
7
7
  == Concept
8
8
 
9
- HoloServe runs a rack application server, that matches any incoming request to a list of request profiles. These
10
- profiles are defined in set of request/response-pairs. If a match is found, the defined static response is returned. The
11
- other half of the matched profile contains a set of responses. One for each situation the faked API can be possibily in.
12
- The response for the currently set situation will be merged with a default response and returned. The name of the
13
- matched request/response pair is saved in a request history. If no match is found, a 404 is returned and the request
14
- data is stored in the bucket for unhandeled requests. These informations can be used to extend the server layout with
15
- missing request handlers.
9
+ HoloServe runs a Goliath[https://github.com/postrank-labs/goliath] application server, that matches any incoming request
10
+ to a list of request/response pairs. If a match is found, the corresponding response is returned. To do that the
11
+ response is assembeled by a selection of responses. The selection is made by conditions on the current state.
12
+ The name of the matched request/response pair is saved in a request history. If no match is found, a 404 is returned and
13
+ the request data is stored in the bucket for unhandeled requests. These informations can be used to extend the server
14
+ layout with missing request handlers.
16
15
 
17
- To avoid too much duplication in the definition of the request/response-pairs, it is possible upload some fixture data
18
- that is shared between all pair definitions. The pair can than refer to these fixtures.
16
+ To avoid too much duplication in the definition of the request/response-pairs, it is possible reference some fixture
17
+ data that is shared between all pair definitions.
19
18
 
20
- The pairs, fixtures, situation, history and bucket can be accessed via control routes, which are described below.
19
+ The pairs, state, history and bucket can be accessed via control routes, which are described below.
21
20
 
22
21
  == Installation
23
22
 
@@ -31,56 +30,47 @@ To start up an empty Holoserve instance, type...
31
30
 
32
31
  holoserve
33
32
 
34
- To load the server with a couple of pairs, fixtures and define a situation during start up, use these parameters.
33
+ To load the server with a couple of pairs, fixtures and define a state during start up, use these parameters.
35
34
 
36
- holoserve -d path/to/pairs/*.yaml -f path/to/fixtures/*.yaml -s backend_without_users
35
+ holoserve -f 'path/to/fixtures/*.yaml' -p 'path/to/pairs/*.yaml' -s user_one=missing -s car_one=existing
37
36
 
38
37
  Notice, that the files must have either the <tt>.yaml</tt> or the <tt>.json</tt> extension.
39
38
 
39
+ A full description of all options can be displayed with <tt>holoserve --help</tt>
40
+
41
+ -P, --port PORT The port holoserve should listen to.
42
+ -f, --fixture-files PATTERN Load the specified fixture files on startup.
43
+ -p, --pair-files PATTERN Load the specified pair files on startup.
44
+ -s, --state SETTING Set a specific state. Use the pattern key=value. Can be applied multiple times.
45
+ -h, --help Shows the help message.
46
+
40
47
  == Control routes
41
48
 
42
49
  If you're using Ruby, you can control Holoserve via the
43
50
  {Holoserve Connector}[https://github.com/skrill/holoserve-connector] gem.
44
51
 
45
- === POST /_control/pairs
46
-
47
- Adds a pair definition to Holoserve. It should receive a parameter named <tt>file</tt> that contains a file with exactly
48
- one pair. The format of the file should fit the specified format. The format can be <tt>yaml</tt> or <tt>json</tt>. See
49
- {Pair file format}[rdoc-label:Pair-file-format] below. The basename of the transmitted file will be taken as the pair
50
- id.
51
-
52
- === GET /_control/pairs/:id.:format
53
-
54
- Returns the pair definition in the requested format.
55
-
56
- === DELETE /_control/pairs
52
+ === GET /_control/pairs
57
53
 
58
- Removes all pairs.
54
+ Returns all pair definitions.
59
55
 
60
- === POST /_control/fixtures
56
+ === GET /_control/pairs/:id
61
57
 
62
- Adds fixture data to Holoserve. The request is similar to <tt>POST /_control/pairs</tt>. The upload file can contain any
63
- yaml or json formatted data. The basename of the file will be taken as the fixture id.
58
+ Returns the specified pair definition.
64
59
 
65
- === GET /_control/fixtures/:id
60
+ === PUT /_control/state
66
61
 
67
- Returns the requested fixture.
62
+ Sets the current state with the transmitted parameters.
68
63
 
69
- === DELETE /_control/fixtures
64
+ === GET /_control/state
70
65
 
71
- Removes all fixtures.
72
-
73
- === PUT /_control/situation
74
-
75
- Sets the current situation with the transmitted <tt>name</tt> parameter.
76
-
77
- === GET /_control/situation
78
-
79
- Returns the name of the current situation.
66
+ Returns the current state.
80
67
 
81
68
  ==== Response example
82
69
 
83
- backend_without_users
70
+ {
71
+ "user_one": "missing",
72
+ "car_one": "existing"
73
+ }
84
74
 
85
75
  === GET /_control/bucket
86
76
 
@@ -90,24 +80,18 @@ Returns a list of all requests that has been received, but couldn't be handled.
90
80
 
91
81
  [
92
82
  {
93
- "method": "POST",
94
- "path": "test",
83
+ "method": "GET",
84
+ "path": "/test",
95
85
  "headers": {
86
+ "SERVER_SOFTWARE": "Goliath",
87
+ "SERVER_NAME": "localhost",
88
+ "SERVER_PORT": "4250",
96
89
  "REMOTE_ADDR": "127.0.0.1",
97
- "REQUEST_METHOD": "GET",
98
- "REQUEST_PATH": "/test",
99
- "PATH_INFO": "/test",
100
- "REQUEST_URI": "/test",
101
- "SERVER_PROTOCOL": "HTTP/1.1",
102
- "HTTP_VERSION": "HTTP/1.1",
103
90
  "HTTP_ACCEPT": "*/*",
104
91
  "HTTP_USER_AGENT": "Ruby",
105
- "HTTP_HOST": "localhost:8080",
106
- "SERVER_NAME": "localhost",
107
- "SERVER_PORT": "8080",
108
- "QUERY_STRING": "",
109
- "SCRIPT_NAME": "",
110
- "SERVER_SOFTWARE": "Unicorn 4.1.1"
92
+ "HTTP_HOST": "localhost:4250",
93
+ "HTTP_VERSION": "1.1",
94
+ "SCRIPT_NAME": "/test"
111
95
  }
112
96
  }
113
97
  ]
@@ -142,11 +126,11 @@ The request/response pair file should have the following format.
142
126
  responses:
143
127
  default:
144
128
  status: 200
145
- user_exists:
129
+ "user_one == :existing":
146
130
  imports:
147
131
  - path: "test_fixture.users.0"
148
132
  as: "json.user"
149
- user_is_missing:
133
+ "user_one == :missing":
150
134
  json:
151
135
  message: "user not found"
152
136
  - request:
@@ -163,11 +147,11 @@ The request/response pair file should have the following format.
163
147
  oauth:
164
148
  oauth_token: "12345"
165
149
  responses:
166
- user_exists:
150
+ "user_one == :existing":
167
151
  status: 401
168
152
  json:
169
153
  message: "invalid password"
170
- user_is_missing:
154
+ "user_one == :missing":
171
155
  status: 200
172
156
  json:
173
157
  message: "user not found"
@@ -183,6 +167,6 @@ This example defines two request/response pairs and two situations. The two pair
183
167
  match against. The first one handles the case in which the parameters <tt>username=one</tt> and <tt>password=valid</tt>
184
168
  are posted and the second one reacts on the transmission of <tt>username=one</tt> and <tt>password=invalid</tt>.
185
169
 
186
- If the situation is set to <tt>user_exists</tt>, the first situation would return the complete user object encoded as
187
- json, while the second one would reply the message "invalid password". In the situation "user_is_missing", both requests
188
- would be replied with the message "user not found".
170
+ If the state includes <tt>user_one == :existing</tt>, the first response would return the complete user object encoded
171
+ as json, while the second one would reply the message "invalid password". In the state includes
172
+ <tt>user_one == :missing</tt>, both requests would be replied with the message "user not found".
data/Rakefile CHANGED
@@ -1,60 +1,8 @@
1
1
  require 'rubygems'
2
2
  require 'bundler/setup'
3
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 'rspec/core/rake_task'
32
-
33
- RSpec::Core::RakeTask.new
34
- rescue LoadError
35
- end
36
-
37
- begin
38
- require 'rdoc'
39
- require 'rdoc/task'
40
-
41
- Rake::RDocTask.new do |rdoc|
42
- rdoc.main = "README.rdoc"
43
- rdoc.rdoc_files.include("README.rdoc", "lib/**/*.rb")
44
- end
45
- rescue LoadError
46
- end
47
-
48
- namespace :gem do
49
-
50
- desc "Builds the gem"
51
- task :build do
52
- system "gem build *.gemspec && mkdir -p pkg/ && mv *.gem pkg/"
53
- end
54
-
55
- desc "Builds and installs the gem"
56
- task :install => :build do
57
- system "gem install pkg/"
58
- end
59
-
60
- end
4
+ load File.join(File.dirname(__FILE__), "tasks", "features.rake")
5
+ load File.join(File.dirname(__FILE__), "tasks", "gem.rake")
6
+ load File.join(File.dirname(__FILE__), "tasks", "goliath.rake")
7
+ load File.join(File.dirname(__FILE__), "tasks", "rdoc.rake")
8
+ load File.join(File.dirname(__FILE__), "tasks", "spec.rake")
data/bin/holoserve CHANGED
@@ -6,22 +6,23 @@ require 'optparse'
6
6
 
7
7
  require File.expand_path(File.join(File.dirname(__FILE__), "..", "lib", "holoserve"))
8
8
 
9
- options = { }
9
+ options = { :state => { } }
10
10
  OptionParser.new do |parser|
11
11
  parser.banner = "Usage: holoserve [options]"
12
12
  parser.separator ""
13
13
  parser.separator "Options:"
14
- parser.on("-p", "--port PORT", Integer, "The port holoserve should listen to.") do |value|
14
+ parser.on("-P", "--port PORT", Integer, "The port holoserve should listen to.") do |value|
15
15
  options[:port] = value
16
16
  end
17
17
  parser.on("-f", "--fixture-files PATTERN", "Load the specified fixture files on startup.") do |value|
18
18
  options[:fixture_file_pattern] = value
19
19
  end
20
- parser.on("-d", "--pair-files PATTERN", "Load the specified pair files on startup.") do |value|
20
+ parser.on("-p", "--pair-files PATTERN", "Load the specified pair files on startup.") do |value|
21
21
  options[:pair_file_pattern] = value
22
22
  end
23
- parser.on("-s", "--situation SITUATION", "Sets the situation.") do |value|
24
- options[:situation] = value
23
+ parser.on("-s", "--state SETTING", "Set a specific state. Use the pattern key=value. Can be applied multiple times.") do |value|
24
+ resource, state = *value.split("=").map(&:strip)
25
+ options[:state][resource] = state
25
26
  end
26
27
  parser.on_tail("-h", "--help", "Shows the help message.") do
27
28
  puts parser
@@ -29,5 +30,5 @@ OptionParser.new do |parser|
29
30
  end
30
31
  end.parse!(ARGV)
31
32
 
32
- runner = Holoserve::Runner.new options
33
- runner.run
33
+ holoserve = Holoserve.new options
34
+ holoserve.run
@@ -0,0 +1,24 @@
1
+ require 'goliath/api'
2
+
3
+ module Holoserve::Interface::Control::Bucket
4
+
5
+ class Fetch < Goliath::API
6
+ include Holoserve::Interface::Control::Helper
7
+
8
+ def response(environment)
9
+ respond_json bucket
10
+ end
11
+
12
+ end
13
+
14
+ class Delete < Goliath::API
15
+ include Holoserve::Interface::Control::Helper
16
+
17
+ def response(environment)
18
+ bucket.clear
19
+ respond_json_acknowledgement
20
+ end
21
+
22
+ end
23
+
24
+ end
@@ -0,0 +1,24 @@
1
+ require 'goliath/api'
2
+
3
+ module Holoserve::Interface::Control::History
4
+
5
+ class Fetch < Goliath::API
6
+ include Holoserve::Interface::Control::Helper
7
+
8
+ def response(environment)
9
+ respond_json history
10
+ end
11
+
12
+ end
13
+
14
+ class Delete < Goliath::API
15
+ include Holoserve::Interface::Control::Helper
16
+
17
+ def response(environment)
18
+ history.clear
19
+ respond_json_acknowledgement
20
+ end
21
+
22
+ end
23
+
24
+ end
@@ -0,0 +1,24 @@
1
+ require 'goliath/api'
2
+
3
+ module Holoserve::Interface::Control::Pair
4
+
5
+ class Index < Goliath::API
6
+ include Holoserve::Interface::Control::Helper
7
+
8
+ def response(environment)
9
+ respond_json pairs
10
+ end
11
+
12
+ end
13
+
14
+ class Fetch < Goliath::API
15
+ include Holoserve::Interface::Control::Helper
16
+
17
+ def response(environment)
18
+ pair = pairs[params[:id]]
19
+ respond_json pair
20
+ end
21
+
22
+ end
23
+
24
+ end
@@ -0,0 +1,38 @@
1
+ require 'goliath/api'
2
+
3
+ module Holoserve::Interface::Control::State
4
+
5
+ class Update < Goliath::API
6
+ include Holoserve::Interface::Control::Helper
7
+
8
+ use Goliath::Rack::Params
9
+
10
+ def response(environment)
11
+ state.merge! params
12
+ logger.info "set state to '#{state.inspect}'"
13
+ respond_json_acknowledgement
14
+ end
15
+
16
+ end
17
+
18
+ class Fetch < Goliath::API
19
+ include Holoserve::Interface::Control::Helper
20
+
21
+ def response(environment)
22
+ respond_json state
23
+ end
24
+
25
+ end
26
+
27
+ class Delete < Goliath::API
28
+ include Holoserve::Interface::Control::Helper
29
+
30
+ def response(environment)
31
+ state.clear
32
+ logger.info "state cleared"
33
+ respond_json_acknowledgement
34
+ end
35
+
36
+ end
37
+
38
+ end
@@ -1,178 +1,46 @@
1
- require 'sinatra'
2
- require 'yaml'
3
1
  require 'json'
4
2
 
5
- class Holoserve::Interface::Control < Sinatra::Base
3
+ module Holoserve::Interface::Control
6
4
 
7
- mime_type :yaml, "application/x-yaml"
8
- mime_type :json, "application/json"
5
+ autoload :Bucket, File.join(File.dirname(__FILE__), "control", "bucket")
6
+ autoload :History, File.join(File.dirname(__FILE__), "control", "history")
7
+ autoload :Pair, File.join(File.dirname(__FILE__), "control", "pair")
8
+ autoload :State, File.join(File.dirname(__FILE__), "control", "state")
9
9
 
10
- post "/_control/pairs" do
11
- if pair_id = load_file_into(pairs)
12
- logger.info "loaded pair #{pair_id}"
13
- acknowledgement
14
- else
15
- bad_request
16
- end
17
- end
18
-
19
- get "/_control/pairs.:format" do |format|
20
- respond_formatted params["evaluate"] ? evaluated_pairs : pairs, format
21
- end
10
+ module Helper
22
11
 
23
- get "/_control/pairs/:id.:format" do |id, format|
24
- pair = (params["evaluate"] ? evaluated_pairs : pairs)[id.to_sym]
25
- if pair
26
- respond_formatted pair, format
27
- else
28
- not_found
12
+ def respond_json_acknowledgement
13
+ respond_json :ok => true
29
14
  end
30
- end
31
-
32
- delete "/_control/pairs" do
33
- pairs.clear
34
- end
35
15
 
36
- post "/_control/fixtures" do
37
- if fixture_id = load_file_into(fixtures)
38
- logger.info "loaded fixture #{fixture_id}"
39
- acknowledgement
40
- else
41
- bad_request
16
+ def respond_json(object)
17
+ ok JSON.dump(object), "application/json"
42
18
  end
43
- end
44
19
 
45
- get "/_control/fixtures/:id.:format" do |id, format|
46
- fixture = fixtures[id.to_sym]
47
- if fixture
48
- respond_formatted fixture, format
49
- else
50
- not_found
20
+ def ok(content, content_type = "text/plain")
21
+ [ 200, { "Content-Type" => content_type }, [ content ] ]
51
22
  end
52
- end
53
-
54
- delete "/_control/fixtures" do
55
- fixtures.clear
56
- end
57
-
58
- put "/_control/situation" do
59
- configuration[:situation] = params[:name]
60
- logger.info "set situation to #{params[:name]}"
61
- respond_json_acknowledgement
62
- end
63
-
64
- get "/_control/situation" do
65
- respond_json :name => configuration[:situation]
66
- end
67
-
68
- get "/_control/bucket" do
69
- respond_json bucket
70
- end
71
-
72
- delete "/_control/bucket" do
73
- bucket.clear
74
- end
75
-
76
- get "/_control/history" do
77
- respond_json history
78
- end
79
-
80
- delete "/_control/history" do
81
- history.clear
82
- respond_json_acknowledgement
83
- end
84
-
85
- private
86
-
87
- def respond_json_acknowledgement
88
- respond_json :ok => true
89
- end
90
23
 
91
- def respond_formatted(data, format)
92
- if format == "yaml"
93
- respond_yaml data
94
- elsif format == "json"
95
- respond_json data
96
- else
97
- bad_request
24
+ def bad_request
25
+ [ 400, { }, [ "bad request" ] ]
98
26
  end
99
- end
100
-
101
- def respond_json(object)
102
- content_type :json
103
- JSON.dump object
104
- end
105
-
106
- def respond_yaml(object)
107
- content_type :yaml
108
- object.to_yaml
109
- end
110
-
111
- def acknowledgement
112
- [ 200, { }, [ "" ] ]
113
- end
114
-
115
- def bad_request
116
- [ 400, { }, [ "bad request" ] ]
117
- end
118
-
119
- def not_acceptable
120
- [ 406, { }, [ "format not acceptable" ] ]
121
- end
122
-
123
- def load_file_into(hash)
124
- data = load_file params["file"][:tempfile]
125
- return nil unless data
126
- id = File.basename params["file"][:filename], ".*"
127
- hash[id.to_sym] = Holoserve::Tool::Hash::KeySymbolizer.new(data).hash
128
- id.to_sym
129
- end
130
27
 
131
- def load_file(filename)
132
- YAML::load_file filename
133
- rescue Psych::SyntaxError
134
- begin
135
- JSON.parse File.read(filename)
136
- rescue JSON::ParserError
137
- nil
28
+ def bucket
29
+ config[:bucket] ||= [ ]
138
30
  end
139
- end
140
31
 
141
- def evaluated_pairs
142
- result = { }
143
- pairs.each do |id, pair|
144
- result[id] = { }
145
- result[id][:request] = Holoserve::Fixture::Importer.new(pair[:request], fixtures).result
146
- result[id][:responses] = { }
147
- pair[:responses].each do |situation, response|
148
- result[id][:responses][situation] = Holoserve::Fixture::Importer.new(response, fixtures).result
149
- end
32
+ def history
33
+ config[:history] ||= [ ]
150
34
  end
151
- result
152
- end
153
-
154
- def pairs
155
- configuration[:pairs]
156
- end
157
-
158
- def fixtures
159
- configuration[:fixtures]
160
- end
161
35
 
162
- def bucket
163
- configuration[:bucket]
164
- end
165
-
166
- def history
167
- configuration[:history]
168
- end
36
+ def pairs
37
+ config[:pairs] ||= options[:pairs]
38
+ end
169
39
 
170
- def configuration
171
- Holoserve.instance.configuration
172
- end
40
+ def state
41
+ config[:state] ||= { }
42
+ end
173
43
 
174
- def logger
175
- Holoserve.instance.logger
176
44
  end
177
45
 
178
- end
46
+ end