holoserve 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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