holoserve 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -6,14 +6,18 @@ 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 defined in
10
- the server layout. If a match is found, the defined static response is returned. The response is defined by a default
11
- and the current server situation.
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.
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.
15
16
 
16
- The layout, situation, history and bucket can be accessed via control routes, which are described below.
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.
19
+
20
+ The pairs, fixtures, situation, history and bucket can be accessed via control routes, which are described below.
17
21
 
18
22
  == Installation
19
23
 
@@ -27,48 +31,58 @@ To start up an empty Holoserve instance, type...
27
31
 
28
32
  holoserve
29
33
 
30
- To load a server layout and define a situation during start up, use these parameters.
34
+ To load the server with a couple of pairs, fixtures and define a situation during start up, use these parameters.
31
35
 
32
- holoserve -l layout.yaml -s backend_without_users
36
+ holoserve -d path/to/pairs/*.yaml -f path/to/fixtures/*.yaml -s backend_without_users
33
37
 
34
- Notice, that the layout file must have either the <tt>.yaml</tt> or the <tt>.json</tt> extension.
38
+ Notice, that the files must have either the <tt>.yaml</tt> or the <tt>.json</tt> extension.
35
39
 
36
40
  == Control routes
37
41
 
38
42
  If you're using Ruby, you can control Holoserve via the
39
43
  {Holoserve Connector}[https://github.com/skrill/holoserve-connector] gem.
40
44
 
41
- === POST /_control/layout.:format
45
+ === POST /_control/pairs
42
46
 
43
- It should receive a parameter named <tt>file</tt> that contains a file with the server layout. The format of the file
44
- should fit the specified format. The format can be <tt>yaml</tt> or <tt>json</tt>. See
45
- {Layout file format}[rdoc-label:Layout-file-format] below.
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.
46
51
 
47
- === GET /_control/layout.:format
52
+ === GET /_control/pairs/:id.:format
48
53
 
49
- Returns the server layout in the requested format.
54
+ Returns the pair definition in the requested format.
50
55
 
51
- === DELETE /_control/layout
56
+ === DELETE /_control/pairs
52
57
 
53
- Removes the server layout.
58
+ Removes all pairs.
54
59
 
55
- === PUT /_control/situation/:name
60
+ === POST /_control/fixtures
56
61
 
57
- Sets the current situation.
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
64
 
59
- === GET /_control/situation
65
+ === GET /_control/fixtures/:id
60
66
 
61
- Returns the name of the current situation.
67
+ Returns the requested fixture.
68
+
69
+ === DELETE /_control/fixtures
70
+
71
+ Removes all fixtures.
62
72
 
63
- === DELETE /_control/situation
73
+ === PUT /_control/situation
64
74
 
65
- Clears the current situation.
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
80
 
67
81
  ==== Response example
68
82
 
69
83
  backend_without_users
70
84
 
71
- === GET /_control/bucket/requests
85
+ === GET /_control/bucket
72
86
 
73
87
  Returns a list of all requests that has been received, but couldn't be handled.
74
88
 
@@ -110,50 +124,65 @@ Returns a list of all names of pairs that has been triggered.
110
124
 
111
125
  Removes all entries from the history.
112
126
 
113
- == Layout file format
127
+ == Pair file format
114
128
 
115
- The server layout file should have the following format.
129
+ The request/response pair file should have the following format.
116
130
 
117
- -
118
- name: "test_received"
119
- request:
131
+ - request:
132
+ imports:
133
+ - path: "test_fixture.users.0"
134
+ as: "parameters"
135
+ only: [ "username", "password" ]
120
136
  method: "POST"
121
- path: "/test"
137
+ path: "/session"
122
138
  headers:
123
139
  HTTP_USER_AGENT: "Ruby"
124
- HTTP_AUTHORIZATION: "OAuth oauth_token=12345"
125
- body:
126
- "test=value"
127
- parameters:
128
- test: "value"
129
140
  oauth:
130
141
  oauth_token: "12345"
131
142
  responses:
132
143
  default:
133
144
  status: 200
134
- one:
135
- body:
136
- "ok"
137
- -
138
- request:
139
- method: "GET"
140
- path: "/test"
145
+ user_exists:
146
+ imports:
147
+ - path: "test_fixture.users.0"
148
+ as: "json.user"
149
+ user_is_missing:
150
+ json:
151
+ message: "user not found"
152
+ - request:
153
+ imports:
154
+ - path: "test_fixture.users.0"
155
+ as: "parameters"
156
+ only: "username"
157
+ method: "POST"
158
+ path: "/session"
159
+ headers:
160
+ HTTP_USER_AGENT: "Ruby"
161
+ parameters:
162
+ password: "invalid"
163
+ oauth:
164
+ oauth_token: "12345"
141
165
  responses:
142
- default:
166
+ user_exists:
167
+ status: 401
168
+ json:
169
+ message: "invalid password"
170
+ user_is_missing:
143
171
  status: 200
144
- body:
145
- "ok too"
172
+ json:
173
+ message: "user not found"
146
174
 
147
- This example would define a server layout that has two request/response pairs. The first pair would have the name
148
- <tt>test_received</tt> and would match a <tt>POST</tt> request to the path <tt>/test</tt>.
175
+ The fixture data where this example pair definition relies on, could look like to following.
149
176
 
150
- Notice, that the given request attributes are the _minimal_ values that have to match the incomong one. An incoming
151
- request may has much more attributes (see bucket example above). If a request is matched, the corresponding pair name is
152
- placed in the history.
177
+ users:
178
+ - email: "one@test.com"
179
+ username: "one"
180
+ password: "valid"
153
181
 
154
- As the sections <tt>headers</tt> and <tt>body</tt> are raw values from the request, the sections <tt>parameters</tt> and
155
- <tt>oauth</tt> contain high-level values. The <tt>parameters</tt> hash is taken from the request body or the query and
156
- the hash containing the OAuth values is parsed from a fitting <tt>HTTP_AUTHORIZATION</tt> header.
182
+ This example defines two request/response pairs and two situations. The two pairs differs only the parameters that they
183
+ match against. The first one handles the case in which the parameters <tt>username=one</tt> and <tt>password=valid</tt>
184
+ are posted and the second one reacts on the transmission of <tt>username=one</tt> and <tt>password=invalid</tt>.
157
185
 
158
- The response is defined in the <tt>responses</tt> section of each pair. The server will response a merge of default
159
- response and the response defined by the current situation.
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".
data/bin/holoserve CHANGED
@@ -14,8 +14,11 @@ OptionParser.new do |parser|
14
14
  parser.on("-p", "--port PORT", Integer, "The port holoserve should listen to.") do |value|
15
15
  options[:port] = value
16
16
  end
17
- parser.on("-l", "--layout-file FILE", "Load the specified layout file on startup.") do |value|
18
- options[:layout_filename] = value
17
+ parser.on("-f", "--fixture-files PATTERN", "Load the specified fixture files on startup.") do |value|
18
+ options[:fixture_file_pattern] = value
19
+ end
20
+ parser.on("-d", "--pair-files PATTERN", "Load the specified pair files on startup.") do |value|
21
+ options[:pair_file_pattern] = value
19
22
  end
20
23
  parser.on("-s", "--situation SITUATION", "Sets the situation.") do |value|
21
24
  options[:situation] = value
@@ -0,0 +1,44 @@
1
+
2
+ class Holoserve::Fixture::Importer
3
+
4
+ attr_accessor :fixtures
5
+
6
+ def initialize(hash, fixtures)
7
+ @fixtures = fixtures
8
+ self.hash = hash
9
+ end
10
+
11
+ def hash=(value)
12
+ @imports = value.respond_to?(:delete) && value.delete(:imports) || [ ]
13
+ @hash = value
14
+ end
15
+
16
+ def hash
17
+ import
18
+ merge
19
+ @result
20
+ end
21
+
22
+ private
23
+
24
+ def import
25
+ @imports.each do |import|
26
+ path = import[:path]
27
+ as = import[:as] || path
28
+ only = import[:only]
29
+
30
+ value = Holoserve::Tool::DataPath.new(path, @fixtures).fetch
31
+
32
+ value.delete_if do |key, v|
33
+ ![ only ].flatten.compact.include?(key.to_s)
34
+ end if only.respond_to?(:include?) && value.respond_to?(:delete_if)
35
+
36
+ @result = Holoserve::Tool::DataPath.new(as, @result || { }).store value
37
+ end
38
+ end
39
+
40
+ def merge
41
+ @result = Holoserve::Tool::Merger.new(@result, @hash).result if @hash
42
+ end
43
+
44
+ end
@@ -0,0 +1,6 @@
1
+
2
+ module Holoserve::Fixture
3
+
4
+ autoload :Importer, File.join(File.dirname(__FILE__), "fixture", "importer")
5
+
6
+ end
@@ -7,60 +7,67 @@ class Holoserve::Interface::Control < Sinatra::Base
7
7
  mime_type :yaml, "application/x-yaml"
8
8
  mime_type :json, "application/json"
9
9
 
10
- post "/_control/layout.:format" do |format|
11
- begin
12
- if format == "yaml"
13
- configuration.load_layout_from_yaml_file params["file"][:tempfile]
14
- respond_json_acknowledgement
15
- elsif format == "json"
16
- configuration.load_layout_from_json_file params["file"][:tempfile]
17
- respond_json_acknowledgement
18
- else
19
- not_acceptable
20
- end
21
- rescue Holoserve::Configuration::InvalidFormatError => error
22
- error 400, error.inspect
10
+ post "/_control/pairs" do
11
+ load_file_into(pairs) ?
12
+ acknowledgement :
13
+ bad_request
14
+ end
15
+
16
+ get "/_control/pairs/:id.:format" do |id, format|
17
+ pair = pairs[id.to_sym]
18
+ if pair
19
+ respond_formatted pair, format
20
+ else
21
+ not_found
23
22
  end
24
23
  end
25
24
 
26
- get "/_control/layout.:format" do |format|
27
- if format == "yaml"
28
- respond_yaml configuration.layout
29
- elsif format == "json"
30
- respond_json configuration.layout
25
+ delete "/_control/pairs" do
26
+ pairs.clear
27
+ end
28
+
29
+ post "/_control/fixtures" do
30
+ load_file_into(fixtures) ?
31
+ acknowledgement :
32
+ bad_request
33
+ end
34
+
35
+ get "/_control/fixtures/:id.:format" do |id, format|
36
+ fixture = fixtures[id.to_sym]
37
+ if fixture
38
+ respond_formatted fixture, format
31
39
  else
32
- not_acceptable
40
+ not_found
33
41
  end
34
42
  end
35
43
 
36
- delete "/_control/layout" do
37
- configuration.clear_layout!
38
- respond_json_acknowledgement
44
+ delete "/_control/fixtures" do
45
+ fixtures.clear
39
46
  end
40
47
 
41
- put "/_control/situation/:name" do |situation|
42
- configuration.situation = situation
48
+ put "/_control/situation" do
49
+ configuration[:situation] = params[:name]
43
50
  respond_json_acknowledgement
44
51
  end
45
52
 
46
53
  get "/_control/situation" do
47
- configuration.situation.to_s
54
+ respond_json :name => configuration[:situation]
48
55
  end
49
56
 
50
- delete "/_control/situation" do
51
- configuration.clear_situation!
57
+ get "/_control/bucket" do
58
+ respond_json bucket
52
59
  end
53
60
 
54
- get "/_control/bucket/requests" do
55
- respond_json bucket.requests
61
+ delete "/_control/bucket" do
62
+ bucket.clear
56
63
  end
57
64
 
58
65
  get "/_control/history" do
59
- respond_json history.pair_names
66
+ respond_json history
60
67
  end
61
68
 
62
69
  delete "/_control/history" do
63
- history.clear!
70
+ history.clear
64
71
  respond_json_acknowledgement
65
72
  end
66
73
 
@@ -70,6 +77,16 @@ class Holoserve::Interface::Control < Sinatra::Base
70
77
  respond_json :ok => true
71
78
  end
72
79
 
80
+ def respond_formatted(data, format)
81
+ if format == "yaml"
82
+ respond_yaml data
83
+ elsif format == "json"
84
+ respond_json data
85
+ else
86
+ bad_request
87
+ end
88
+ end
89
+
73
90
  def respond_json(object)
74
91
  content_type :json
75
92
  JSON.dump object
@@ -80,16 +97,50 @@ class Holoserve::Interface::Control < Sinatra::Base
80
97
  object.to_yaml
81
98
  end
82
99
 
100
+ def acknowledgement
101
+ [ 200, { }, [ "" ] ]
102
+ end
103
+
104
+ def bad_request
105
+ [ 400, { }, [ "bad request" ] ]
106
+ end
107
+
83
108
  def not_acceptable
84
109
  [ 406, { }, [ "format not acceptable" ] ]
85
110
  end
86
111
 
112
+ def load_file_into(hash)
113
+ data = load_file params["file"][:tempfile]
114
+ return false unless data
115
+ id = File.basename params["file"][:filename], ".*"
116
+ hash[id.to_sym] = Holoserve::Tool::Hash::KeySymbolizer.new(data).hash
117
+ true
118
+ end
119
+
120
+ def load_file(filename)
121
+ YAML::load_file filename
122
+ rescue Psych::SyntaxError
123
+ begin
124
+ JSON.parse File.read(filename)
125
+ rescue JSON::ParserError
126
+ nil
127
+ end
128
+ end
129
+
130
+ def pairs
131
+ configuration[:pairs]
132
+ end
133
+
134
+ def fixtures
135
+ configuration[:fixtures]
136
+ end
137
+
87
138
  def bucket
88
- Holoserve.instance.bucket
139
+ configuration[:bucket]
89
140
  end
90
141
 
91
142
  def history
92
- Holoserve.instance.history
143
+ configuration[:history]
93
144
  end
94
145
 
95
146
  def configuration
@@ -7,10 +7,10 @@ class Holoserve::Interface::Fake
7
7
  pair = Holoserve::Pair::Finder.new(configuration, request).pair
8
8
  if pair
9
9
  if name = pair[:name]
10
- history.pair_names << name
10
+ history << name
11
11
  logger.info "received handled request with name '#{name}'"
12
12
  end
13
- response = compose_response pair
13
+ response = Holoserve::Response::Combiner.new(pair[:responses], configuration).response
14
14
  if response.empty?
15
15
  logger.warn "received request #{pair[:name]} with undefined response"
16
16
  not_found
@@ -18,7 +18,7 @@ class Holoserve::Interface::Fake
18
18
  Holoserve::Response::Composer.new(response).response_array
19
19
  end
20
20
  else
21
- bucket.requests << request
21
+ bucket << request
22
22
  logger.error "received unhandled request\n" + request.pretty_inspect
23
23
  not_found
24
24
  end
@@ -26,31 +26,24 @@ class Holoserve::Interface::Fake
26
26
 
27
27
  private
28
28
 
29
- def compose_response(pair)
30
- responses = pair[:responses]
31
- response_default = responses[:default] || { }
32
- response_situation = (configuration.situation && responses[configuration.situation.to_sym]) || { }
33
- Holoserve::Tool::Merger.new(response_default, response_situation).result
34
- end
35
-
36
29
  def not_found
37
30
  [ 404, { "Content-Type" => "text/plain" }, [ "no response found for this request" ] ]
38
31
  end
39
32
 
40
33
  def logger
41
- configuration.logger
34
+ Holoserve.instance.logger
42
35
  end
43
36
 
44
- def layout
45
- configuration.layout
37
+ def pairs
38
+ configuration[:pairs]
46
39
  end
47
40
 
48
41
  def bucket
49
- Holoserve.instance.bucket
42
+ configuration[:bucket]
50
43
  end
51
44
 
52
45
  def history
53
- Holoserve.instance.history
46
+ configuration[:history]
54
47
  end
55
48
 
56
49
  def configuration
@@ -6,16 +6,21 @@ class Holoserve::Pair::Finder
6
6
  end
7
7
 
8
8
  def pair
9
- return nil unless layout
10
- layout.detect do |pair|
11
- Holoserve::Request::Matcher.new(@request, pair[:request]).match?
9
+ return nil unless pairs
10
+ pairs.each do |name, pair|
11
+ return pair.merge(:name => name) if Holoserve::Request::Matcher.new(@request, pair[:request], fixtures).match?
12
12
  end
13
+ nil
13
14
  end
14
15
 
15
16
  private
16
17
 
17
- def layout
18
- @configuration.layout
18
+ def pairs
19
+ @configuration[:pairs]
20
+ end
21
+
22
+ def fixtures
23
+ @configuration[:fixtures]
19
24
  end
20
25
 
21
26
  end
@@ -1,8 +1,9 @@
1
1
 
2
2
  class Holoserve::Request::Matcher
3
3
 
4
- def initialize(request, request_subset)
5
- @request, @request_subset = request, request_subset
4
+ def initialize(request, request_subset, fixtures)
5
+ @request = request
6
+ @request_subset = Holoserve::Fixture::Importer.new(request_subset, fixtures).hash
6
7
  end
7
8
 
8
9
  def match?
@@ -15,7 +16,7 @@ class Holoserve::Request::Matcher
15
16
  end
16
17
 
17
18
  private
18
-
19
+
19
20
  def match_method?
20
21
  @request_subset[:method] ?
21
22
  @request[:method] == @request_subset[:method] :
@@ -0,0 +1,34 @@
1
+
2
+ class Holoserve::Response::Combiner
3
+
4
+ def initialize(responses, configuration)
5
+ @responses, @configuration = responses, configuration
6
+ end
7
+
8
+ def response
9
+ Holoserve::Tool::Merger.new(default_response, situation_response).result
10
+ end
11
+
12
+ private
13
+
14
+ def default_response
15
+ @responses[:default] ?
16
+ Holoserve::Fixture::Importer.new(@responses[:default], fixtures).hash :
17
+ { }
18
+ end
19
+
20
+ def situation_response
21
+ situation && @responses[situation.to_sym] ?
22
+ Holoserve::Fixture::Importer.new(@responses[situation.to_sym], fixtures).hash :
23
+ { }
24
+ end
25
+
26
+ def fixtures
27
+ @configuration[:fixtures]
28
+ end
29
+
30
+ def situation
31
+ @configuration[:situation]
32
+ end
33
+
34
+ end
@@ -1,6 +1,7 @@
1
1
 
2
2
  module Holoserve::Response
3
3
 
4
+ autoload :Combiner, File.join(File.dirname(__FILE__), "response", "combiner")
4
5
  autoload :Composer, File.join(File.dirname(__FILE__), "response", "composer")
5
6
 
6
7
  end
@@ -7,7 +7,8 @@ class Holoserve::Runner
7
7
 
8
8
  def initialize(options = { })
9
9
  @port = options[:port] || 4250
10
- @layout_filename = options[:layout_filename]
10
+ @fixture_file_pattern = options[:fixture_file_pattern]
11
+ @pair_file_pattern = options[:pair_file_pattern]
11
12
  @situation = options[:situation]
12
13
 
13
14
  @rackup_options = Unicorn::Configurator::RACKUP
@@ -20,7 +21,8 @@ class Holoserve::Runner
20
21
 
21
22
  def start
22
23
  @unicorn.start
23
- upload_layout if @layout_filename
24
+ upload_fixtures if @fixture_file_pattern
25
+ upload_pairs if @pair_file_pattern
24
26
  set_situation if @situation
25
27
  end
26
28
 
@@ -42,15 +44,22 @@ class Holoserve::Runner
42
44
 
43
45
  private
44
46
 
45
- def upload_layout
46
- format = File.extname(@layout_filename).sub(/^\./, "")
47
+ def upload_fixtures
48
+ Dir[ @fixture_file_pattern ].each do |filename|
49
+ upload_file "http://localhost:#{port}/_control/fixtures", filename
50
+ end
51
+ end
52
+
53
+ def upload_pairs
54
+ Dir[ @pair_file_pattern ].each do |filename|
55
+ upload_file "http://localhost:#{port}/_control/pairs", filename
56
+ end
57
+ end
58
+
59
+ def upload_file(url, filename)
60
+ format = File.extname(filename).sub(/^\./, "")
47
61
  raise ArgumentError, "file extension indicates wrong format '#{format}' (choose yaml or json)" unless [ "yaml", "json" ].include?(format)
48
- Holoserve::Tool::Uploader.new(
49
- @layout_filename,
50
- :post,
51
- "http://localhost:#{port}/_control/layout.#{format}",
52
- :expected_status_code => 200
53
- ).upload
62
+ Holoserve::Tool::Uploader.new(filename, :post, url, :expected_status_code => 200).upload
54
63
  nil
55
64
  end
56
65
 
@@ -0,0 +1,56 @@
1
+
2
+ class Holoserve::Tool::DataPath
3
+
4
+ PATH_SEPARATOR = ".".freeze unless defined?(PATH_SEPARATOR)
5
+
6
+ attr_accessor :path
7
+ attr_accessor :data
8
+
9
+ def initialize(path, data)
10
+ @path, @data = path, data
11
+ end
12
+
13
+ def fetch
14
+ path = @path ? @path.split(PATH_SEPARATOR) : [ ]
15
+ return @data if path.empty?
16
+
17
+ selected = @data
18
+ key = parse_key path.shift
19
+
20
+ while path.length > 0
21
+ return nil unless selected.respond_to?(:[])
22
+ selected = selected[key]
23
+
24
+ key = parse_key path.shift
25
+ end
26
+
27
+ return nil unless selected.respond_to?(:[])
28
+ selected[key]
29
+ end
30
+
31
+ def store(value)
32
+ path = @path ? @path.split(PATH_SEPARATOR) : [ ]
33
+ return value if path.empty?
34
+
35
+ selected = @data
36
+ key = parse_key path.shift
37
+
38
+ while path.length > 0
39
+ break unless selected.respond_to?(:[])
40
+
41
+ selected[key] ||= { }
42
+ selected = selected[key]
43
+ key = parse_key path.shift
44
+ end
45
+
46
+ selected[key] = value
47
+ @data
48
+ end
49
+
50
+ private
51
+
52
+ def parse_key(key)
53
+ key =~ /^\d+$/ ? key.to_i : key.to_sym
54
+ end
55
+
56
+ end
@@ -7,10 +7,7 @@ class Holoserve::Tool::Uploader
7
7
  end
8
8
 
9
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
10
+ Transport::HTTP.request @http_method, @url, @options.merge(:headers => headers, :body => body)
14
11
  end
15
12
 
16
13
  private
@@ -22,13 +19,20 @@ class Holoserve::Tool::Uploader
22
19
  def body
23
20
  "--#{boundary}\r\n" +
24
21
  "Content-Disposition: form-data; name=\"file\"; filename=\"#{File.basename(@filename)}\"\r\n" +
25
- "Content-Type: application/x-yaml\r\n" +
22
+ "Content-Type: #{content_type}\r\n" +
26
23
  "\r\n" +
27
24
  File.read(@filename) +
28
25
  "\r\n" +
29
26
  "--#{boundary}--\r\n"
30
27
  end
31
28
 
29
+ def content_type
30
+ {
31
+ "yaml" => "application/x-yaml",
32
+ "json" => "application/json"
33
+ }[ File.extname(@filename) ] || "text/plain"
34
+ end
35
+
32
36
  def boundary
33
37
  "xxx12345xxx"
34
38
  end
@@ -1,6 +1,7 @@
1
1
 
2
2
  module Holoserve::Tool
3
3
 
4
+ autoload :DataPath, File.join(File.dirname(__FILE__), "tool", "data_path")
4
5
  autoload :Hash, File.join(File.dirname(__FILE__), "tool", "hash")
5
6
  autoload :Merger, File.join(File.dirname(__FILE__), "tool", "merger")
6
7
  autoload :Uploader, File.join(File.dirname(__FILE__), "tool", "uploader")
data/lib/holoserve.rb CHANGED
@@ -3,9 +3,7 @@ require 'logger'
3
3
 
4
4
  class Holoserve
5
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")
6
+ autoload :Fixture, File.join(File.dirname(__FILE__), "holoserve", "fixture")
9
7
  autoload :Interface, File.join(File.dirname(__FILE__), "holoserve", "interface")
10
8
  autoload :Pair, File.join(File.dirname(__FILE__), "holoserve", "pair")
11
9
  autoload :Request, File.join(File.dirname(__FILE__), "holoserve", "request")
@@ -15,15 +13,11 @@ class Holoserve
15
13
 
16
14
  attr_reader :logger
17
15
  attr_reader :configuration
18
- attr_reader :bucket
19
- attr_reader :history
20
16
  attr_reader :rack
21
17
 
22
18
  def initialize
23
19
  initialize_logger
24
20
  initialize_configuration
25
- initialize_bucket
26
- initialize_history
27
21
  initialize_rack
28
22
  end
29
23
 
@@ -34,15 +28,13 @@ class Holoserve
34
28
  end
35
29
 
36
30
  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
31
+ @configuration = {
32
+ :pairs => { },
33
+ :fixtures => { },
34
+ :situation => nil,
35
+ :bucket => [ ],
36
+ :history => [ ]
37
+ }
46
38
  end
47
39
 
48
40
  def initialize_rack
@@ -0,0 +1,79 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "helper"))
2
+
3
+ describe Holoserve::Fixture::Importer do
4
+
5
+ let(:fixtures) { { :test => { :nested => "value", :second => "value" }, :another => "value" } }
6
+
7
+ subject { described_class.new nil, fixtures }
8
+
9
+ describe "hash" do
10
+
11
+ it "should return the original hash if there is no imports section" do
12
+ subject.hash = {
13
+ :test => "value"
14
+ }
15
+ subject.hash.should == { :test => "value" }
16
+ end
17
+
18
+ it "should return a hash with imported fixtures" do
19
+ subject.hash = {
20
+ :imports => [
21
+ { :path => "test.nested" }
22
+ ]
23
+ }
24
+ subject.hash.should == { :test => { :nested => "value" } }
25
+ end
26
+
27
+ it "should return a hash with imported fixtures at a target path" do
28
+ subject.hash = {
29
+ :imports => [
30
+ { :path => "test.nested", :as => "test" }
31
+ ]
32
+ }
33
+ subject.hash.should == { :test => "value" }
34
+ end
35
+
36
+ it "should return a hash with imported and filtered fixtures" do
37
+ subject.hash = {
38
+ :imports => [
39
+ { :path => "test", :as => "test", :only => [ "second" ] }
40
+ ]
41
+ }
42
+ subject.hash.should == { :test => { :second => "value" } }
43
+ end
44
+
45
+ it "should return a hash where all the imports are imported" do
46
+ subject.hash = {
47
+ :imports => [
48
+ { :path => "test.nested", :as => "test" },
49
+ { :path => "another", :as => "another.test" }
50
+ ]
51
+ }
52
+ subject.hash.should == { :test => "value", :another => { :test => "value" } }
53
+ end
54
+
55
+ it "should return a hash where the data is merged with the imports" do
56
+ subject.hash = {
57
+ :imports => [
58
+ { :path => "test.nested", :as => "test" }
59
+ ],
60
+ :another => "value"
61
+ }
62
+ subject.hash.should == { :test => "value", :another => "value" }
63
+ end
64
+
65
+ it "should return a hash where the data is deep merged with the imports" do
66
+ subject.hash = {
67
+ :imports => [
68
+ { :path => "test.nested", :as => "test.nested" }
69
+ ],
70
+ :test => {
71
+ :another => "value"
72
+ }
73
+ }
74
+ subject.hash.should == { :test => { :nested => "value", :another => "value" } }
75
+ end
76
+
77
+ end
78
+
79
+ end
@@ -0,0 +1,75 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "helper"))
2
+
3
+ describe Holoserve::Tool::DataPath do
4
+
5
+ subject { described_class.new nil, nil }
6
+
7
+ describe "fetch" do
8
+
9
+ it "should return the given data if no path is specified" do
10
+ subject.path = nil
11
+ subject.data = { :test => "value" }
12
+ subject.fetch.should == { :test => "value" }
13
+ end
14
+
15
+ it "should return the value specified by the given path" do
16
+ subject.path = "test"
17
+ subject.data = { :test => "value" }
18
+ subject.fetch.should == "value"
19
+ end
20
+
21
+ it "should return the nested value specified by the given path" do
22
+ subject.path = "test.nested"
23
+ subject.data = { :test => { :nested => "value" } }
24
+ subject.fetch.should == "value"
25
+ end
26
+
27
+ it "should return nil if the path points to nowhere" do
28
+ subject.path = "test.nested"
29
+ subject.data = { :test => true }
30
+ subject.fetch.should be_nil
31
+ end
32
+
33
+ it "should return nil if the last path element doesn't exists'" do
34
+ subject.path = "test.nested"
35
+ subject.data = { :test => { :another => "value" } }
36
+ subject.fetch.should be_nil
37
+ end
38
+
39
+ it "should return in an array nested value specified by the given path" do
40
+ subject.path = "test.1.nested"
41
+ subject.data = { :test => [ { }, { :nested => "value" } ] }
42
+ subject.fetch.should == "value"
43
+ end
44
+
45
+ end
46
+
47
+ describe "store" do
48
+
49
+ it "should return the given value if no path is specified" do
50
+ subject.path = nil
51
+ subject.data = { :test => "value" }
52
+ subject.store("value").should == "value"
53
+ end
54
+
55
+ it "should store the given value at the specified path" do
56
+ subject.path = "another"
57
+ subject.data = { :test => "value" }
58
+ subject.store("value").should == { :test => "value", :another => "value" }
59
+ end
60
+
61
+ it "should store the given value in a nested hash" do
62
+ subject.path = "test.another"
63
+ subject.data = { :test => { :nested => "value" } }
64
+ subject.store("value").should == { :test => { :nested => "value", :another => "value" } }
65
+ end
66
+
67
+ it "should store the given value at the specified path and create it if missing" do
68
+ subject.path = "another.test"
69
+ subject.data = { :test => "value" }
70
+ subject.store("value").should == { :test => "value", :another => { :test => "value" } }
71
+ end
72
+
73
+ end
74
+
75
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: holoserve
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-02-03 00:00:00.000000000 Z
12
+ date: 2012-02-09 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rack
16
- requirement: &70196505909820 !ruby/object:Gem::Requirement
16
+ requirement: &70132546243580 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70196505909820
24
+ version_requirements: *70132546243580
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: sinatra
27
- requirement: &70196505909260 !ruby/object:Gem::Requirement
27
+ requirement: &70132546257040 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: '0'
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *70196505909260
35
+ version_requirements: *70132546257040
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: unicorn
38
- requirement: &70196505908780 !ruby/object:Gem::Requirement
38
+ requirement: &70132546255800 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: '0'
44
44
  type: :runtime
45
45
  prerelease: false
46
- version_requirements: *70196505908780
46
+ version_requirements: *70132546255800
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: transport
49
- requirement: &70196505908180 !ruby/object:Gem::Requirement
49
+ requirement: &70132546254300 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ! '>='
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: '0'
55
55
  type: :runtime
56
56
  prerelease: false
57
- version_requirements: *70196505908180
57
+ version_requirements: *70132546254300
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: cucumber
60
- requirement: &70196505907560 !ruby/object:Gem::Requirement
60
+ requirement: &70132546252800 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ! '>='
@@ -65,10 +65,10 @@ dependencies:
65
65
  version: '0'
66
66
  type: :development
67
67
  prerelease: false
68
- version_requirements: *70196505907560
68
+ version_requirements: *70132546252800
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: rspec
71
- requirement: &70196505907080 !ruby/object:Gem::Requirement
71
+ requirement: &70132546265600 !ruby/object:Gem::Requirement
72
72
  none: false
73
73
  requirements:
74
74
  - - ! '>='
@@ -76,10 +76,10 @@ dependencies:
76
76
  version: '0'
77
77
  type: :development
78
78
  prerelease: false
79
- version_requirements: *70196505907080
79
+ version_requirements: *70132546265600
80
80
  - !ruby/object:Gem::Dependency
81
81
  name: transport
82
- requirement: &70196505906560 !ruby/object:Gem::Requirement
82
+ requirement: &70132546264780 !ruby/object:Gem::Requirement
83
83
  none: false
84
84
  requirements:
85
85
  - - ! '>='
@@ -87,10 +87,10 @@ dependencies:
87
87
  version: '0'
88
88
  type: :development
89
89
  prerelease: false
90
- version_requirements: *70196505906560
90
+ version_requirements: *70132546264780
91
91
  - !ruby/object:Gem::Dependency
92
92
  name: oauth
93
- requirement: &70196505906040 !ruby/object:Gem::Requirement
93
+ requirement: &70132546263580 !ruby/object:Gem::Requirement
94
94
  none: false
95
95
  requirements:
96
96
  - - ! '>='
@@ -98,7 +98,7 @@ dependencies:
98
98
  version: '0'
99
99
  type: :development
100
100
  prerelease: false
101
- version_requirements: *70196505906040
101
+ version_requirements: *70132546263580
102
102
  description: This tool can be used to fake webservice APIs for testing proposals.
103
103
  email: philipp.bruell@skrill.com
104
104
  executables:
@@ -111,9 +111,8 @@ files:
111
111
  - LICENSE
112
112
  - Rakefile
113
113
  - bin/holoserve
114
- - lib/holoserve/bucket.rb
115
- - lib/holoserve/configuration.rb
116
- - lib/holoserve/history.rb
114
+ - lib/holoserve/fixture/importer.rb
115
+ - lib/holoserve/fixture.rb
117
116
  - lib/holoserve/interface/control.rb
118
117
  - lib/holoserve/interface/fake.rb
119
118
  - lib/holoserve/interface.rb
@@ -122,9 +121,11 @@ files:
122
121
  - lib/holoserve/request/decomposer.rb
123
122
  - lib/holoserve/request/matcher.rb
124
123
  - lib/holoserve/request.rb
124
+ - lib/holoserve/response/combiner.rb
125
125
  - lib/holoserve/response/composer.rb
126
126
  - lib/holoserve/response.rb
127
127
  - lib/holoserve/runner.rb
128
+ - lib/holoserve/tool/data_path.rb
128
129
  - lib/holoserve/tool/hash/key_symbolizer.rb
129
130
  - lib/holoserve/tool/hash.rb
130
131
  - lib/holoserve/tool/merger.rb
@@ -132,6 +133,8 @@ files:
132
133
  - lib/holoserve/tool.rb
133
134
  - lib/holoserve.rb
134
135
  - spec/helper.rb
136
+ - spec/lib/holoserve/fixture/importer_spec.rb
137
+ - spec/lib/holoserve/tool/data_path_spec.rb
135
138
  - spec/lib/holoserve/tool/merger_spec.rb
136
139
  homepage: http://github.com/skrill/holoserve
137
140
  licenses: []
@@ -147,7 +150,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
147
150
  version: '0'
148
151
  segments:
149
152
  - 0
150
- hash: -388162576537081624
153
+ hash: 4596962680025174660
151
154
  required_rubygems_version: !ruby/object:Gem::Requirement
152
155
  none: false
153
156
  requirements:
@@ -161,4 +164,6 @@ signing_key:
161
164
  specification_version: 3
162
165
  summary: Tool to fake HTTP APIs.
163
166
  test_files:
167
+ - spec/lib/holoserve/fixture/importer_spec.rb
168
+ - spec/lib/holoserve/tool/data_path_spec.rb
164
169
  - spec/lib/holoserve/tool/merger_spec.rb
@@ -1,10 +0,0 @@
1
-
2
- class Holoserve::Bucket
3
-
4
- attr_reader :requests
5
-
6
- def initialize
7
- @requests = [ ]
8
- end
9
-
10
- end
@@ -1,50 +0,0 @@
1
- require 'yaml'
2
-
3
- class Holoserve::Configuration
4
-
5
- class InvalidFormatError < StandardError; end
6
-
7
- attr_reader :logger
8
-
9
- attr_reader :layout
10
- attr_reader :situation
11
-
12
- def initialize(logger)
13
- @logger = logger
14
- end
15
-
16
- def layout=(hash_or_array)
17
- @layout = Holoserve::Tool::Hash::KeySymbolizer.new(hash_or_array).hash
18
- end
19
-
20
- def clear_layout!
21
- self.layout = nil
22
- end
23
-
24
- def situation=(value)
25
- @situation = value.to_sym
26
- logger.info "made '#{value}' the current situation"
27
- end
28
-
29
- def clear_situation!
30
- @situation = nil
31
- logger.info "cleared the current situation"
32
- end
33
-
34
- def load_layout_from_yaml_file(file)
35
- self.layout = YAML::load_file file
36
- logger.info "loaded layouts from yaml file #{file.path}"
37
- rescue Psych::SyntaxError => error
38
- self.clear_layout!
39
- raise InvalidFormatError, error.to_s
40
- end
41
-
42
- def load_layout_from_json_file(file)
43
- self.layout = JSON.parse File.read(file)
44
- logger.info "loaded layouts from json file #{file.path}"
45
- rescue JSON::ParserError => error
46
- self.clear_layout!
47
- raise InvalidFormatError, error.to_s
48
- end
49
-
50
- end
@@ -1,14 +0,0 @@
1
-
2
- class Holoserve::History
3
-
4
- attr_reader :pair_names
5
-
6
- def initialize
7
- @pair_names = [ ]
8
- end
9
-
10
- def clear!
11
- @pair_names.clear
12
- end
13
-
14
- end