holoserve 0.2.1 → 0.3.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,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