holoserve 0.3.1 → 0.4.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/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