rhcp 0.1.2

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.
@@ -0,0 +1,157 @@
1
+ require 'webrick'
2
+ require 'json'
3
+ require 'net/http'
4
+
5
+ require 'rhcp/broker'
6
+ require 'rhcp/request'
7
+ require 'rhcp/rhcp_exception'
8
+
9
+ include WEBrick
10
+
11
+ module RHCP
12
+
13
+ # TODO add some logging
14
+
15
+ # The +HttpExporter+ connects to the RHCP::Broker and exports all commands
16
+ # registered there via http, i.e. it servers requests to the URLs
17
+ #
18
+ # /rhcp/get_commands
19
+ # /rhcp/get_lookup_values
20
+ # /rhcp/execute
21
+ #
22
+ # All data is transferred in JSON format.
23
+ class HttpExporter
24
+
25
+ DEFAULT_PORT = 42000
26
+ DEFAULT_PREFIX = "rhcp"
27
+
28
+ # by default, a new HttpExporter will create it's own +HTTPServer+
29
+ # on port 42000 - if you want to change this port, you can pass the port
30
+ # to use as +port+ option. If you want to use an existing +HTTPServer+
31
+ # instance, you can pass it as +server+ option instead.
32
+ #
33
+ # Also, you can specify the prefix for the rhcp URLs using the +prefix+
34
+ # option - otherwise, all
35
+ # rhcp actions will be exported at
36
+ # /rhcp/get_commands
37
+ # /rhcp/get_lookup_values
38
+ # /rhcp/execute
39
+ # If you change this, please be aware that all clients that want to connect
40
+ # to this server need to be configured accordingly.
41
+ def initialize(broker, options = Hash.new())
42
+
43
+ @logger = RHCP::ModuleHelper::instance.logger
44
+ @broker = broker
45
+
46
+ # build your own server or use the on passed in as param
47
+ port = options.has_key?(:port) ? options[:port] : DEFAULT_PORT
48
+
49
+ if options.has_key?(:server)
50
+ @server = options[:server]
51
+ @logger.debug("using existing server #{@server}")
52
+ else
53
+ @logger.debug("opening own server on port #{port}")
54
+ @server = HTTPServer.new( :Port => port )
55
+ end
56
+
57
+ @url_prefix = options.has_key?(:prefix) ? options[:prefix] : DEFAULT_PREFIX
58
+ @server.mount_proc("/#{@url_prefix}/") {|req, res|
59
+ res.body = "<HTML>hello (again)</HTML>"
60
+ res['Content-Type'] = "text/html"
61
+ }
62
+ # TODO this path should probably be quite relative
63
+ @server.mount "/#{@url_prefix}/info", HTTPServlet::FileHandler, "docroot"
64
+ @server.mount "/#{@url_prefix}/get_commands", GetCommandsServlet, @broker
65
+ @server.mount "/#{@url_prefix}/get_lookup_values", GetLookupValuesServlet, @broker
66
+ @server.mount "/#{@url_prefix}/execute", ExecuteServlet, @broker
67
+ @logger.info("http exporter has been initialized - once started, it will listen on port '#{port}' for URLs starting with prefix '#{@url_prefix}'")
68
+ end
69
+
70
+ class BaseServlet < HTTPServlet::AbstractServlet
71
+
72
+ def initialize(server, broker)
73
+ super(server)
74
+ @broker = broker
75
+ end
76
+
77
+ def do_GET(req, res)
78
+ res['Content-Type'] = "application/json"
79
+ begin
80
+ #@logger.info("executing #{req}")
81
+ do_it(req, res)
82
+ #@logger.info("finished executing #{req}")
83
+ rescue RHCP::RhcpException => ex
84
+ # TODO define error format (should we send back a JSON-ified response object here?)
85
+ @logger.error("got an exception while executing request . #{ex}")
86
+ res.status = 500
87
+ res.body = [ ex.to_s ].to_json()
88
+ #puts ex
89
+ #puts ex.backtrace.join("\n")
90
+ end
91
+ end
92
+
93
+ # TODO is this a good idea?
94
+ def do_POST(req, res)
95
+ do_GET(req, res)
96
+ end
97
+
98
+ end
99
+
100
+ class GetCommandsServlet < BaseServlet
101
+
102
+ def do_it(req, res)
103
+ commands = @broker.get_command_list
104
+ puts "about to send back commands : #{commands.values.to_json()}"
105
+ res.body = commands.values.to_json()
106
+ end
107
+ end
108
+
109
+ class GetLookupValuesServlet < BaseServlet
110
+
111
+ def do_it(req, res)
112
+ partial_value = req.query.has_key?('partial') ? req.query['partial'] : ''
113
+ command = @broker.get_command(req.query['command'])
114
+ param = command.get_param(req.query['param'])
115
+ lookup_values = param.get_lookup_values(partial_value)
116
+ res.body = lookup_values.to_json()
117
+ end
118
+
119
+ end
120
+
121
+ class ExecuteServlet < BaseServlet
122
+
123
+ def do_it(req, res)
124
+ @logger.info("got an execute request : #{req}")
125
+ request = RHCP::Request.reconstruct_from_json(@broker, req.body)
126
+
127
+ response = request.execute()
128
+
129
+ @logger.debug("sending back response : #{response.to_json()}")
130
+ res.body = response.to_json()
131
+ end
132
+
133
+ end
134
+
135
+ def run
136
+ @server.start
137
+ end
138
+
139
+ def start
140
+ puts "about to start server..."
141
+ @thread = Thread.new {
142
+ puts "in thread; starting server..."
143
+ @server.start
144
+ puts "in thread: continuing..."
145
+ }
146
+ end
147
+
148
+ def stop
149
+ puts "about to stop server"
150
+ @server.stop
151
+ puts "thread stopped"
152
+ # TODO wait until after the server has ended - when this method exits, the server is still shutting down
153
+ end
154
+
155
+ end
156
+
157
+ end
@@ -0,0 +1,93 @@
1
+ require 'rhcp/rhcp_exception'
2
+ require 'rhcp/broker'
3
+
4
+ require 'rubygems'
5
+ require 'json'
6
+
7
+ module RHCP
8
+
9
+ # This class represents a request made by a RHCP client to a RHCP server.
10
+ # It is passed as an argument to the method that should execute the requested
11
+ # command
12
+ class Request
13
+
14
+ attr_reader :command
15
+ attr_reader :param_values
16
+
17
+ # default constructor; will throw exceptions on invalid values
18
+ def initialize(command, param_values = {})
19
+ raise RHCP::RhcpException.new("command may not be null") if command == nil
20
+
21
+ # autobox the parameters if necessary
22
+ param_values.each do |k,v|
23
+ if ! v.instance_of?(Array)
24
+ param_values[k] = [ v ]
25
+ end
26
+ end
27
+
28
+ # check all param values for plausibility
29
+ param_values.each do |key,value|
30
+ command.get_param(key).check_param_is_valid(value)
31
+ end
32
+
33
+ # check that we've got all mandatory params
34
+ command.params.each do |name, param|
35
+ raise RHCP::RhcpException.new("missing mandatory parameter '#{name}'") if param.mandatory && ! param_values.has_key?(name)
36
+ end
37
+
38
+ @command = command
39
+ @param_values = param_values
40
+ end
41
+
42
+ # used to retrieve the value for the specified parameter
43
+ # returns either the value or an array of values if the parameter allows
44
+ # multiple values
45
+ def get_param_value(param_name)
46
+ raise "no such parameter : #{param_name}" unless @param_values.has_key?(param_name)
47
+ param = @command.get_param(param_name)
48
+ if (param.allows_multiple_values)
49
+ @param_values[param_name]
50
+ else
51
+ @param_values[param_name][0]
52
+ end
53
+ end
54
+
55
+ def has_param_value(param_name)
56
+ @param_values.has_key?(param_name)
57
+ end
58
+
59
+ # convenience method that executes the command that actually delegates to the
60
+ # command that's inside this request
61
+ def execute
62
+ @command.execute_request(self)
63
+ end
64
+
65
+ # reconstructs the request from it's JSON representation
66
+ # Since the JSON version of a request does hold the command name instead
67
+ # of the full command only, a broker is needed to lookup the command by
68
+ # it's name
69
+ #
70
+ # Params:
71
+ # +broker+ is the broker to use for command lookup
72
+ # +json_data+ is the JSON data that represents the request
73
+ def self.reconstruct_from_json(broker, json_data)
74
+ object = JSON.parse(json_data)
75
+ command = broker.get_command(object['command_name'])
76
+ self.new(command, object['param_values'])
77
+ end
78
+
79
+ # returns a JSON representation of this request.
80
+ def to_json(*args)
81
+ {
82
+ 'command_name' => @command.name,
83
+ 'param_values' => @param_values
84
+ }.to_json(*args)
85
+ end
86
+
87
+ def to_s
88
+ "#{@command.name} (#{@param_values})"
89
+ end
90
+
91
+ end
92
+
93
+ end
@@ -0,0 +1,62 @@
1
+ require 'rubygems'
2
+ require 'json'
3
+
4
+ module RHCP
5
+
6
+ class Response
7
+
8
+ class Status
9
+ OK="ok"
10
+ ERROR="error"
11
+ end
12
+
13
+ # TODO these should be attr_reader's, but then we've got to solve json_create() differently
14
+ attr_accessor :status
15
+ attr_accessor :error_text
16
+ attr_accessor :error_detail
17
+ attr_accessor :data
18
+
19
+ # textual description of the result (optional)
20
+ attr_accessor :result_text
21
+
22
+ def initialize
23
+ @status = Status::OK
24
+ @error_text = ""
25
+ @error_detail = ""
26
+ @result_text = "";
27
+ end
28
+
29
+ def mark_as_error(text, detail="")
30
+ @status = Status::ERROR
31
+ @error_text = text
32
+ @error_detail = detail
33
+ end
34
+
35
+ def set_payload(data)
36
+ @data = data
37
+ end
38
+
39
+ def self.reconstruct_from_json(json_data)
40
+ object = JSON.parse(json_data)
41
+ instance = self.new()
42
+ instance.status = object["status"]
43
+ instance.error_text = object["error_text"]
44
+ instance.error_detail = object["error_detail"]
45
+ instance.set_payload(object["data"])
46
+ instance.result_text = object['result_text']
47
+ instance
48
+ end
49
+
50
+ def to_json(*args)
51
+ {
52
+ 'status' => @status,
53
+ 'error_text' => @error_text,
54
+ 'error_detail' => @error_detail,
55
+ 'data' => @data, # TODO what about JSONinification of data? (probably data should be JSON-ish data only, i.e. no special objects)
56
+ 'result_text' => @result_text
57
+ }.to_json(*args)
58
+ end
59
+
60
+ end
61
+
62
+ end
@@ -0,0 +1,6 @@
1
+ module RHCP
2
+
3
+ class RhcpException < Exception
4
+ end
5
+
6
+ end
data/lib/rhcp.rb ADDED
@@ -0,0 +1,47 @@
1
+ require 'singleton'
2
+ require 'logger'
3
+
4
+ require 'rhcp/broker'
5
+ require 'rhcp/command'
6
+ require 'rhcp/command_param'
7
+ require 'rhcp/dispatching_broker'
8
+ require 'rhcp/http_exporter'
9
+ require 'rhcp/request'
10
+ require 'rhcp/response'
11
+ require 'rhcp/rhcp_exception'
12
+ require 'rhcp/client/http_broker'
13
+ require 'rhcp/client/command_stub'
14
+ require 'rhcp/client/command_param_stub'
15
+
16
+ module RHCP #:nodoc:
17
+
18
+ class Version
19
+
20
+ include Singleton
21
+
22
+ MAJOR = 0
23
+ MINOR = 1
24
+ TINY = 2
25
+
26
+ def Version.to_s
27
+ [ MAJOR, MINOR, TINY ].join(".")
28
+ end
29
+
30
+ end
31
+
32
+ class ModuleHelper
33
+
34
+ include Singleton
35
+
36
+ attr_accessor :logger
37
+
38
+ def initialize()
39
+ # TODO do we really want to log to STDOUT per default?
40
+ # TODO check the whole package for correct usage of loggers
41
+ @logger = Logger.new(STDOUT)
42
+ end
43
+
44
+
45
+ end
46
+
47
+ end
@@ -0,0 +1,29 @@
1
+ $:.unshift File.join(File.dirname(__FILE__),'..','lib')
2
+
3
+ require 'test/unit'
4
+
5
+ require 'rhcp'
6
+
7
+ class BrokerTest < Test::Unit::TestCase
8
+
9
+ def test_register_commands
10
+ broker = RHCP::Broker.new()
11
+ assert_not_nil broker
12
+ commands = broker.get_command_list
13
+ assert_not_nil commands
14
+ assert_equal 0, commands.size
15
+
16
+ command = RHCP::Command.new("test", "a test command", lambda {})
17
+ broker.register_command(command)
18
+ commands = broker.get_command_list
19
+ assert_equal 1, commands.size
20
+ assert_equal command, commands["test"]
21
+ end
22
+
23
+ def test_register_duplicate
24
+ broker = RHCP::Broker.new()
25
+ broker.register_command RHCP::Command.new("test", "a test command", lambda {})
26
+ assert_raise(RHCP::RhcpException) { broker.register_command RHCP::Command.new("test", "a command with the same name", lambda {}) }
27
+ end
28
+
29
+ end
@@ -0,0 +1,73 @@
1
+ $:.unshift File.join(File.dirname(__FILE__),'..','lib')
2
+
3
+ require 'test/unit'
4
+ require 'rhcp/client/command_param_stub'
5
+ require 'rhcp/command_param'
6
+
7
+ class CommandParamStubTest < Test::Unit::TestCase
8
+
9
+ def param_lookup
10
+ ["foo", "bar", "baz"]
11
+ end
12
+
13
+ def setup
14
+ @p = RHCP::CommandParam.new("test", "this param is used for testing purposes only",
15
+ :mandatory => true,
16
+ :allows_multiple_values => true,
17
+ :lookup_method => self.method(:param_lookup),
18
+ :is_default_param => true
19
+ )
20
+ end
21
+
22
+ def test_json
23
+ json = @p.to_json
24
+ puts "json : >>#{json}<<"
25
+ p2 = RHCP::Client::CommandParamStub.reconstruct_from_json(json)
26
+ assert_not_nil p2
27
+ assert_instance_of RHCP::Client::CommandParamStub, p2
28
+ assert_equal @p.name, p2.name
29
+ assert_equal @p.description, p2.description
30
+ assert_equal @p.allows_multiple_values, p2.allows_multiple_values
31
+ assert_equal @p.has_lookup_values, p2.has_lookup_values
32
+ assert_equal @p.is_default_param, p2.is_default_param
33
+ assert_equal @p.mandatory, p2.mandatory
34
+
35
+ json_hash = JSON.parse(json)
36
+ p3 = RHCP::Client::CommandParamStub.reconstruct_from_json(json_hash)
37
+ assert_instance_of RHCP::Client::CommandParamStub, p3
38
+ assert_equal @p.name, p3.name
39
+ assert_equal @p.description, p3.description
40
+ assert_equal @p.allows_multiple_values, p3.allows_multiple_values
41
+ end
42
+
43
+ # when a param is "stubbed", it should be possible to inject a method
44
+ # for retrieving the lookup values
45
+ def test_stubbing
46
+ json = @p.to_json
47
+ stub = RHCP::Client::CommandParamStub.reconstruct_from_json(json)
48
+ stub.get_lookup_values_block = lambda {
49
+ |partial_value|
50
+ [ "mascarpone", "limoncello" ]
51
+ }
52
+ lookup_values = stub.get_lookup_values()
53
+ assert_equal [ "mascarpone", "limoncello" ], lookup_values
54
+ end
55
+
56
+ def test_stubbing_without_lookup_values
57
+ p = RHCP::CommandParam.new("test", "without lookup values",
58
+ :mandatory => true,
59
+ :allows_multiple_values => true,
60
+ :is_default_param => true
61
+ )
62
+ stub = RHCP::Client::CommandParamStub.reconstruct_from_json(p.to_json())
63
+ has_been_invoked = false
64
+ stub.get_lookup_values_block = lambda {
65
+ |partial_value|
66
+ has_been_invoked = true
67
+ }
68
+ stub.get_lookup_values()
69
+ assert_equal false, has_been_invoked
70
+ end
71
+
72
+
73
+ end
@@ -0,0 +1,40 @@
1
+ $:.unshift File.join(File.dirname(__FILE__),'..','lib')
2
+
3
+ require 'rubygems'
4
+ require 'json'
5
+
6
+ require 'test/unit'
7
+ require 'rhcp'
8
+ require 'rhcp'
9
+ require 'rhcp'
10
+
11
+ class CommandStubTest < Test::Unit::TestCase
12
+
13
+ def command_method(request, response)
14
+ first_param = request.get_param_value("first_param")
15
+ puts "just testing : #{first_param}"
16
+ first_param.reverse
17
+ end
18
+
19
+ def test_json
20
+ c = RHCP::Command.new("test", "a command for testing", self.method(:command_method))
21
+ c.add_param(RHCP::CommandParam.new("first_param", "this is the first param"))
22
+ json = c.to_json
23
+ puts "JSON : >>#{JSON.pretty_generate(c)}<<"
24
+ assert_not_nil json
25
+ c2 = RHCP::Client::CommandStub.reconstruct_from_json(json)
26
+ assert_not_nil c2
27
+ assert_instance_of RHCP::Client::CommandStub, c2
28
+ assert_equal c.name, c2.name
29
+ assert_equal c.description, c2.description
30
+ assert_equal c.params.size, c2.params.size
31
+
32
+ json_hash = JSON.parse(json)
33
+ c3 = RHCP::Client::CommandStub.reconstruct_from_json(json_hash)
34
+ assert_instance_of RHCP::Client::CommandStub, c3
35
+ assert_equal c.name, c3.name
36
+ assert_equal c.description, c3.description
37
+ assert_equal c.params.size, c3.params.size
38
+ end
39
+
40
+ end
@@ -0,0 +1,72 @@
1
+ $:.unshift File.join(File.dirname(__FILE__),'..','lib')
2
+
3
+ require 'test/unit'
4
+ require 'rhcp/command_param'
5
+
6
+ class CommandParamTest < Test::Unit::TestCase
7
+
8
+ def param_lookup
9
+ ["foo", "bar", "baz"]
10
+ end
11
+
12
+ def test_creation
13
+ param = RHCP::CommandParam.new("test", "this param is used for testing purposes only")
14
+ assert_not_nil param, "a param should not be nil after creation"
15
+ assert_equal false, param.mandatory, "by default, params should not be mandatory"
16
+ assert_equal false, param.has_lookup_values, "by default, a param does not have lookup values"
17
+ assert_equal false, param.allows_multiple_values, "by default, a param should not allow multiple values"
18
+ assert_equal false, param.is_default_param
19
+ end
20
+
21
+ def test_options
22
+ param = RHCP::CommandParam.new("test", "this param is used for testing purposes only",
23
+ :mandatory => true,
24
+ :allows_multiple_values => true,
25
+ :lookup_method => self.method(:param_lookup),
26
+ :is_default_param => true
27
+ )
28
+ assert_not_nil param, "a param should not be nil after creation"
29
+ assert_equal true, param.mandatory
30
+ assert_equal true, param.allows_multiple_values
31
+ assert_equal true, param.has_lookup_values
32
+ assert_equal true, param.is_default_param
33
+ end
34
+
35
+ def test_lookup_values
36
+ param = RHCP::CommandParam.new("lookup_test", "testing if lookup values are working",
37
+ :lookup_method => self.method(:param_lookup)
38
+ )
39
+ assert_not_nil param
40
+ assert_equal param_lookup(), param.get_lookup_values()
41
+ end
42
+
43
+ def test_partial_lookup_values
44
+ param = RHCP::CommandParam.new("lookup_test", "testing if partial lookup values are working",
45
+ :lookup_method => self.method(:param_lookup)
46
+ )
47
+ assert_not_nil param
48
+ assert_equal ["bar", "baz"], param.get_lookup_values("ba")
49
+ end
50
+
51
+ def test_get_lookup_values_without_lookup
52
+ param = RHCP::CommandParam.new("lookup_test", "testing if partial lookup values are working")
53
+ assert_not_nil param
54
+ assert_equal [], param.get_lookup_values()
55
+ end
56
+
57
+ def test_check_param_is_valid
58
+ param = RHCP::CommandParam.new("validity_test", "testing if valid values are valid")
59
+ assert_not_nil param
60
+ assert param.check_param_is_valid([ "bla" ])
61
+ assert_raise(RHCP::RhcpException) { param.check_param_is_valid [ "bla", "blubb" ] }
62
+ end
63
+
64
+ # values that aren't part of the lookup values are invalid
65
+ def test_check_param_is_valid_lookup_values
66
+ param = RHCP::CommandParam.new("lookup_test", "testing if partial lookup values are working",
67
+ :lookup_method => self.method(:param_lookup)
68
+ )
69
+ assert_raise(RHCP::RhcpException) { param.check_param_is_valid(["zaphod"]) }
70
+ end
71
+
72
+ end