holoserve 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,53 +1,61 @@
1
+ require 'goliath/api'
1
2
  require 'pp'
2
3
 
3
- class Holoserve::Interface::Fake
4
+ class Holoserve::Interface::Fake < Goliath::API
4
5
 
5
- def call(env)
6
- request = Holoserve::Request::Decomposer.new(env).hash
7
- pair = Holoserve::Pair::Finder.new(configuration, request).pair
6
+ use Goliath::Rack::Params
7
+
8
+ def response(env)
9
+ request = Holoserve::Request::Decomposer.new(env, params).hash
10
+ pair = Holoserve::Pair::Finder.new(pairs, request).pair
8
11
  if pair
9
- if name = pair[:name]
10
- history << name
11
- logger.info "received handled request with name '#{name}'"
12
- end
13
- response = Holoserve::Response::Combiner.new(pair[:responses], configuration).response
14
- if response.empty?
15
- logger.warn "received request #{pair[:name]} with undefined response"
16
- not_found
17
- else
18
- Holoserve::Response::Composer.new(response).response_array
19
- end
12
+ id, responses = *pair.values_at(:id, :responses)
13
+
14
+ history << id
15
+ logger.info "received handled request with id '#{id}'"
16
+
17
+ selector = Holoserve::Response::Selector.new responses, state, logger
18
+ default_response, selected_responses = selector.default_response, selector.selected_responses
19
+
20
+ update_state default_response, selected_responses
21
+
22
+ response = Holoserve::Response::Combiner.new(default_response, selected_responses).response
23
+ Holoserve::Response::Composer.new(response).response_array
20
24
  else
21
25
  bucket << request
22
26
  logger.error "received unhandled request\n" + request.pretty_inspect
27
+
23
28
  not_found
24
29
  end
25
30
  end
26
31
 
27
32
  private
28
33
 
29
- def not_found
30
- [ 404, { "Content-Type" => "text/plain" }, [ "no response found for this request" ] ]
31
- end
32
-
33
- def logger
34
- Holoserve.instance.logger
34
+ def update_state(default_response, selected_responses)
35
+ Holoserve::State::Updater.new(state, default_response[:transitions]).perform
36
+ (selected_responses || [ ]).each do |response|
37
+ Holoserve::State::Updater.new(state, response[:transitions]).perform
38
+ end
35
39
  end
36
40
 
37
- def pairs
38
- configuration[:pairs]
41
+ def not_found
42
+ [ 404, { :"Content-Type" => "text/plain" }, [ "no response found for this request" ] ]
39
43
  end
40
44
 
41
45
  def bucket
42
- configuration[:bucket]
46
+ config[:bucket] ||= [ ]
43
47
  end
44
48
 
45
49
  def history
46
- configuration[:history]
50
+ config[:history] ||= [ ]
51
+ end
52
+
53
+ def pairs
54
+ config[:pairs] ||= options[:pairs]
47
55
  end
48
56
 
49
- def configuration
50
- Holoserve.instance.configuration
57
+ def state
58
+ config[:state] ||= options[:state]
51
59
  end
52
60
 
53
61
  end
@@ -1,7 +1,23 @@
1
+ require 'goliath/api'
1
2
 
2
- module Holoserve::Interface
3
+ class Holoserve::Interface < Goliath::API
3
4
 
4
5
  autoload :Control, File.join(File.dirname(__FILE__), "interface", "control")
5
6
  autoload :Fake, File.join(File.dirname(__FILE__), "interface", "fake")
6
7
 
8
+ get "/_control/bucket", Control::Bucket::Fetch
9
+ delete "/_control/bucket", Control::Bucket::Delete
10
+
11
+ get "/_control/history", Control::History::Fetch
12
+ delete "/_control/history", Control::History::Delete
13
+
14
+ get "/_control/pairs", Control::Pair::Index
15
+ get "/_control/pairs/:id", Control::Pair::Fetch
16
+
17
+ put "/_control/state", Control::State::Update
18
+ get "/_control/state", Control::State::Fetch
19
+ delete "/_control/state", Control::State::Delete
20
+
21
+ map "/*", Fake
22
+
7
23
  end
@@ -1,26 +1,16 @@
1
1
 
2
2
  class Holoserve::Pair::Finder
3
3
 
4
- def initialize(configuration, request)
5
- @configuration, @request = configuration, request
4
+ def initialize(pairs, request)
5
+ @pairs, @request = pairs, request
6
6
  end
7
7
 
8
8
  def pair
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?
9
+ return nil unless @pairs
10
+ @pairs.each do |id, pair|
11
+ return pair.merge(:id => id) if Holoserve::Request::Matcher.new(@request, pair[:request]).match?
12
12
  end
13
13
  nil
14
14
  end
15
15
 
16
- private
17
-
18
- def pairs
19
- @configuration[:pairs]
20
- end
21
-
22
- def fixtures
23
- @configuration[:fixtures]
24
- end
25
-
26
16
  end
@@ -0,0 +1,70 @@
1
+ require 'yaml'
2
+ require 'json'
3
+
4
+ class Holoserve::Pair::Loader
5
+
6
+ def initialize(fixture_file_pattern, pair_file_pattern, logger)
7
+ @fixtures, @pairs = { }, { }
8
+ @fixture_file_pattern, @pair_file_pattern = fixture_file_pattern, pair_file_pattern
9
+ @logger = logger
10
+ end
11
+
12
+ def pairs
13
+ load_fixtures if @fixture_file_pattern
14
+ load_pairs if @pair_file_pattern
15
+ @pairs
16
+ end
17
+
18
+ private
19
+
20
+ def load_fixtures
21
+ Dir[ @fixture_file_pattern ].each do |filename|
22
+ id = extract_id filename
23
+ fixture = load_file filename
24
+ @fixtures[id] = fixture if fixture
25
+ @logger.info "loaded fixture '#{id}'"
26
+ end
27
+ @fixtures.freeze
28
+ end
29
+
30
+ def load_pairs
31
+ Dir[ @pair_file_pattern ].each do |filename|
32
+ id = extract_id filename
33
+ pair = load_file filename
34
+ @pairs[id] = pair_with_imports pair if pair
35
+ @logger.info "loaded pair '#{id}'"
36
+ end
37
+ @pairs.freeze
38
+ end
39
+
40
+ def extract_id(filename)
41
+ File.basename filename, ".*"
42
+ end
43
+
44
+ def load_file(filename)
45
+ format = File.extname(filename).sub(/^\./, "")
46
+ raise ArgumentError, "file extension indicates wrong format '#{format}' (choose yaml or json)" unless [ "yaml", "json" ].include?(format)
47
+ data = begin
48
+ YAML::load_file filename
49
+ rescue Psych::SyntaxError
50
+ begin
51
+ JSON.parse File.read(filename)
52
+ rescue JSON::ParserError
53
+ nil
54
+ end
55
+ end
56
+ Holoserve::Tool::Hash::KeySymbolizer.new(data).hash
57
+ end
58
+
59
+ def pair_with_imports(pair)
60
+ result = {
61
+ :request => Holoserve::Fixture::Importer.new(pair[:request], @fixtures).result,
62
+ :responses => { }
63
+ }
64
+ (pair[:responses] || { }).each do |id, response|
65
+ result[:responses][id] = Holoserve::Fixture::Importer.new(response, @fixtures).result
66
+ end
67
+ result
68
+ end
69
+
70
+ end
@@ -2,5 +2,6 @@
2
2
  module Holoserve::Pair
3
3
 
4
4
  autoload :Finder, File.join(File.dirname(__FILE__), "pair", "finder")
5
+ autoload :Loader, File.join(File.dirname(__FILE__), "pair", "loader")
5
6
 
6
7
  end
@@ -1,19 +1,29 @@
1
1
 
2
2
  class Holoserve::Request::Decomposer
3
3
 
4
- def initialize(request)
5
- @request = request
4
+ ONLY_HEADERS = [
5
+ "SERVER_SOFTWARE",
6
+ "SERVER_NAME",
7
+ "SERVER_PORT",
8
+ "REMOTE_ADDR",
9
+ "SCRIPT_NAME",
10
+ "CONTENT_TYPE"
11
+ ].freeze unless defined?(ONLY_HEADERS)
12
+
13
+ def initialize(request, parameters)
14
+ @request, @parameters = request, parameters
6
15
  end
7
16
 
8
17
  def hash
9
18
  hash = {
10
19
  :method => @request["REQUEST_METHOD"],
11
- :path => @request["PATH_INFO"]
20
+ :path => @request["REQUEST_PATH"]
12
21
  }
13
- hash.merge! :headers => headers unless headers.empty?
14
- hash.merge! :body => body unless body.nil?
15
- hash.merge! :parameters => parameters unless parameters.empty?
16
- hash.merge! :oauth => oauth unless oauth.empty?
22
+ hash[:headers] = headers unless headers.empty?
23
+ hash[:body] = body unless body.nil?
24
+ hash[:parameters] = parameters unless parameters.empty?
25
+ hash[:oauth] = oauth unless oauth.empty?
26
+ hash[:json] = json unless json.empty?
17
27
  hash
18
28
  end
19
29
 
@@ -22,7 +32,7 @@ class Holoserve::Request::Decomposer
22
32
  def headers
23
33
  headers = { }
24
34
  @request.each do |key, value|
25
- headers[ key.to_sym ] = value unless key =~ /^rack\./
35
+ headers[ key.to_sym ] = value if ONLY_HEADERS.include?(key) || key =~ /^HTTP_/
26
36
  end
27
37
  headers
28
38
  end
@@ -35,15 +45,7 @@ class Holoserve::Request::Decomposer
35
45
  end
36
46
 
37
47
  def parameters
38
- Holoserve::Tool::Hash::KeySymbolizer.new(query_hash.merge(form_hash)).hash
39
- end
40
-
41
- def query_hash
42
- @request["rack.request.query_hash"] || { }
43
- end
44
-
45
- def form_hash
46
- @request["rack.request.form_hash"] || { }
48
+ Holoserve::Tool::Hash::KeySymbolizer.new(@parameters).hash
47
49
  end
48
50
 
49
51
  def oauth
@@ -62,4 +64,12 @@ class Holoserve::Request::Decomposer
62
64
  end
63
65
  end
64
66
 
67
+ def json
68
+ @json ||= if @request["CONTENT_TYPE"] == "application/json"
69
+ Holoserve::Tool::Hash::KeySymbolizer.new(JSON.parse(@body)).hash
70
+ else
71
+ { }
72
+ end
73
+ end
74
+
65
75
  end
@@ -1,9 +1,8 @@
1
1
 
2
2
  class Holoserve::Request::Matcher
3
3
 
4
- def initialize(request, request_subset, fixtures)
5
- @request = request
6
- @request_subset = Holoserve::Fixture::Importer.new(request_subset, fixtures).result
4
+ def initialize(request, request_subset)
5
+ @request, @request_subset = request, request_subset
7
6
  end
8
7
 
9
8
  def match?
@@ -12,7 +11,8 @@ class Holoserve::Request::Matcher
12
11
  match_headers? &&
13
12
  match_body? &&
14
13
  match_parameters? &&
15
- match_oauth?
14
+ match_oauth? &&
15
+ match_json?
16
16
  end
17
17
 
18
18
  private
@@ -59,4 +59,16 @@ class Holoserve::Request::Matcher
59
59
  match
60
60
  end
61
61
 
62
+ def match_json?
63
+ match_hash? :json
64
+ end
65
+
66
+ def match_hash?(hash_key)
67
+ match = true
68
+ (@request_subset[hash_key] || { }).each do |key, value|
69
+ match &&= @request[hash_key].is_a?(Hash) && (@request[hash_key][key] == value)
70
+ end
71
+ match
72
+ end
73
+
62
74
  end
@@ -1,34 +1,14 @@
1
1
 
2
2
  class Holoserve::Response::Combiner
3
3
 
4
- def initialize(responses, configuration)
5
- @responses, @configuration = responses, configuration
4
+ def initialize(default, responses)
5
+ @default, @responses = default, responses
6
6
  end
7
7
 
8
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).result :
17
- { }
18
- end
19
-
20
- def situation_response
21
- situation && @responses[situation.to_sym] ?
22
- Holoserve::Fixture::Importer.new(@responses[situation.to_sym], fixtures).result :
23
- { }
24
- end
25
-
26
- def fixtures
27
- @configuration[:fixtures]
28
- end
29
-
30
- def situation
31
- @configuration[:situation]
9
+ @responses.inject @default do |result, response|
10
+ Holoserve::Tool::Merger.new(result, response).result
11
+ end
32
12
  end
33
13
 
34
14
  end
@@ -0,0 +1,43 @@
1
+
2
+ class Holoserve::Response::Selector
3
+
4
+ class Sandbox
5
+
6
+ def initialize(state)
7
+ state.each do |resource, value|
8
+ define_singleton_method resource.to_sym do
9
+ value ? value.to_sym : nil
10
+ end
11
+ end
12
+ end
13
+
14
+ end
15
+
16
+ def initialize(responses, state, logger)
17
+ @responses, @logger = responses, logger
18
+ @sandbox = Sandbox.new state
19
+ end
20
+
21
+ def default_response
22
+ @responses[:default] ?
23
+ @responses[:default] :
24
+ { }
25
+ end
26
+
27
+ def selected_responses
28
+ result = [ ]
29
+ (@responses || { }).each do |line, response|
30
+ next if line.to_s == "default"
31
+ begin
32
+ match = @sandbox.instance_eval do
33
+ eval line.to_s
34
+ end
35
+ result << response if match
36
+ rescue Object => error
37
+ @logger.error error.inspect
38
+ end
39
+ end
40
+ result
41
+ end
42
+
43
+ end
@@ -3,5 +3,6 @@ module Holoserve::Response
3
3
 
4
4
  autoload :Combiner, File.join(File.dirname(__FILE__), "response", "combiner")
5
5
  autoload :Composer, File.join(File.dirname(__FILE__), "response", "composer")
6
+ autoload :Selector, File.join(File.dirname(__FILE__), "response", "selector")
6
7
 
7
8
  end
@@ -0,0 +1,14 @@
1
+
2
+ class Holoserve::State::Updater
3
+
4
+ def initialize(state, transitions)
5
+ @state, @transitions = state, transitions
6
+ end
7
+
8
+ def perform
9
+ (@transitions || { }).each do |resource, value|
10
+ @state[resource] = value
11
+ end
12
+ end
13
+
14
+ end
@@ -0,0 +1,6 @@
1
+
2
+ module Holoserve::State
3
+
4
+ autoload :Updater, File.join(File.dirname(__FILE__), "state", "updater")
5
+
6
+ end
@@ -4,10 +4,14 @@ class Holoserve::Tool::DataPath
4
4
  PATH_SEPARATOR = ".".freeze unless defined?(PATH_SEPARATOR)
5
5
 
6
6
  attr_accessor :path
7
- attr_accessor :data
7
+ attr_reader :data
8
8
 
9
9
  def initialize(path, data)
10
- @path, @data = path, data
10
+ self.path, self.data = path, data
11
+ end
12
+
13
+ def data=(value)
14
+ @data = Holoserve::Tool::Hash::KeySymbolizer.new(value).hash
11
15
  end
12
16
 
13
17
  def fetch
data/lib/holoserve.rb CHANGED
@@ -1,4 +1,4 @@
1
- require 'rack/builder'
1
+ require 'goliath/runner'
2
2
  require 'logger'
3
3
 
4
4
  class Holoserve
@@ -8,44 +8,85 @@ class Holoserve
8
8
  autoload :Pair, File.join(File.dirname(__FILE__), "holoserve", "pair")
9
9
  autoload :Request, File.join(File.dirname(__FILE__), "holoserve", "request")
10
10
  autoload :Response, File.join(File.dirname(__FILE__), "holoserve", "response")
11
- autoload :Runner, File.join(File.dirname(__FILE__), "holoserve", "runner")
11
+ autoload :State, File.join(File.dirname(__FILE__), "holoserve", "state")
12
12
  autoload :Tool, File.join(File.dirname(__FILE__), "holoserve", "tool")
13
13
 
14
- attr_reader :logger
15
- attr_reader :configuration
16
- attr_reader :rack
14
+ def initialize(options = { })
15
+ @port = options[:port] || 4250
16
+ @pid_filename = options[:pid_filename] || File.expand_path(File.join(File.dirname(__FILE__), "..", "holoserve.pid"))
17
+ @log_filename = options[:log_filename] || File.expand_path(File.join(File.dirname(__FILE__), "..", "holoserve.log"))
18
+ @fixture_file_pattern = options[:fixture_file_pattern]
19
+ @pair_file_pattern = options[:pair_file_pattern]
20
+ @state = options[:state] || { }
21
+ end
22
+
23
+ def start
24
+ initialize_logger
25
+ load_pairs
26
+ run_goliath true
27
+ wait_until_running
28
+ end
17
29
 
18
- def initialize
30
+ def run
19
31
  initialize_logger
20
- initialize_configuration
21
- initialize_rack
32
+ load_pairs
33
+ run_goliath false
34
+ end
35
+
36
+ def stop
37
+ kill_goliath
38
+ end
39
+
40
+ def running?
41
+ !!(process_id && Process.kill(0, process_id) == 1)
42
+ rescue Errno::ESRCH
43
+ false
44
+ end
45
+
46
+ def process_id
47
+ File.read(@pid_filename).to_i
48
+ rescue Errno::ENOENT
49
+ nil
22
50
  end
23
51
 
24
52
  private
25
53
 
26
54
  def initialize_logger
27
- @logger = Logger.new STDOUT
55
+ @logger = Logger.new @log_filename
28
56
  end
29
57
 
30
- def initialize_configuration
31
- @configuration = {
32
- :pairs => { },
33
- :fixtures => { },
34
- :situation => nil,
35
- :bucket => [ ],
36
- :history => [ ]
37
- }
58
+ def load_pairs
59
+ @pairs = Pair::Loader.new(@fixture_file_pattern, @pair_file_pattern, @logger).pairs
38
60
  end
39
61
 
40
- def initialize_rack
41
- @rack = Rack::Builder.new do
42
- use Interface::Control
43
- run Interface::Fake.new
44
- end
62
+ def run_goliath(daemonize)
63
+ saved_directory = Dir.pwd
64
+
65
+ runner = Goliath::Runner.new [
66
+ "-P", @pid_filename,
67
+ "-l", @log_filename,
68
+ "-e", "production",
69
+ "-p", @port.to_s,
70
+ daemonize ? "-d" : "-s"
71
+ ], nil
72
+ runner.options[:pairs] = @pairs
73
+ runner.options[:state] = @state
74
+ runner.api = Interface.new
75
+ runner.app = Goliath::Rack::Builder.build Interface, runner.api
76
+ runner.run
77
+
78
+ Dir.chdir saved_directory
79
+ end
80
+
81
+ def wait_until_running
82
+ sleep 0.2 while !self.running?
45
83
  end
46
84
 
47
- def self.instance
48
- @instance ||= self.new
85
+ def kill_goliath
86
+ if File.exists?(@pid_filename)
87
+ system "kill -s QUIT `cat #{@pid_filename}`"
88
+ File.delete @pid_filename
89
+ end
49
90
  end
50
91
 
51
92
  end
@@ -25,9 +25,10 @@ describe Holoserve::Fixture::Importer do
25
25
  subject.hash = {
26
26
  :imports => [
27
27
  { :path => "one" }
28
- ]
28
+ ],
29
+ :test => "value"
29
30
  }
30
- subject.result.should == { :first => 1, :second => 2 }
31
+ subject.result.should == { :first => 1, :second => 2, :test => "value" }
31
32
  end
32
33
 
33
34
  it "should return a hash with imported fixtures at a target path" do
@@ -77,6 +78,20 @@ describe Holoserve::Fixture::Importer do
77
78
  subject.result.should == { :first => 1, :second => 2, :third => 4 }
78
79
  end
79
80
 
81
+ it "should return a hash where all the imports (with and without an :as statement) are properly merged together" do
82
+ subject.hash = {
83
+ :imports => [
84
+ { :path => "one" },
85
+ { :path => "three", :as => "test" },
86
+ { :path => "two", :as => "test.another" }
87
+ ],
88
+ :test => {
89
+ :fifth => 6
90
+ }
91
+ }
92
+ subject.result.should == { :first => 1, :second => 2, :test => { :third => 4, :another => 3, :fifth => 6 } }
93
+ end
94
+
80
95
  it "should return a hash where the data is merged with the imports" do
81
96
  subject.hash = {
82
97
  :imports => [
@@ -18,6 +18,12 @@ describe Holoserve::Tool::DataPath do
18
18
  subject.fetch.should == "value"
19
19
  end
20
20
 
21
+ it "should return the value even if string keys are used" do
22
+ subject.path = "test"
23
+ subject.data = { "test" => "value" }.freeze
24
+ subject.fetch.should == "value"
25
+ end
26
+
21
27
  it "should return the nested value specified by the given path" do
22
28
  subject.path = "test.nested"
23
29
  subject.data = { :test => { :nested => "value" }.freeze }.freeze
@@ -0,0 +1,45 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), "..", "helper"))
2
+
3
+ describe Holoserve do
4
+
5
+ subject { described_class.new }
6
+
7
+ describe "#start" do
8
+
9
+ after :each do
10
+ subject.stop
11
+ end
12
+
13
+ it "should not change the working directory" do
14
+ lambda do
15
+ subject.start
16
+ end.should_not change(Dir, :pwd)
17
+ end
18
+
19
+ it "should start holoserve" do
20
+ lambda do
21
+ subject.start
22
+ end.should change(subject, :running?).from(false).to(true)
23
+ end
24
+
25
+ end
26
+
27
+ describe "#stop" do
28
+
29
+ before :each do
30
+ subject.start
31
+ end
32
+
33
+ after :each do
34
+ subject.stop
35
+ end
36
+
37
+ it "should stop holoserve" do
38
+ lambda do
39
+ subject.stop
40
+ end.should change(subject, :running?).from(true).to(false)
41
+ end
42
+
43
+ end
44
+
45
+ end