rhcp 0.1.3 → 0.1.4

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,25 @@
1
+ require 'rhcp/rhcp_exception'
2
+ require 'rhcp/broker'
3
+
4
+ module RHCP
5
+
6
+ class DispatchingBroker < RHCP::Broker
7
+ def initialize
8
+ @known_commands = Hash.new()
9
+ end
10
+
11
+ # returns a list of all known commands
12
+ def get_command_list()
13
+ @known_commands
14
+ end
15
+
16
+ def add_broker(new_broker)
17
+ new_broker.get_command_list.each do |name, command|
18
+ raise RHCP::RhcpException.new("duplicate command: '#{name}' has already been defined.") if @known_commands.has_key?(name)
19
+ @known_commands[name] = command
20
+ end
21
+ end
22
+
23
+ end
24
+
25
+ end
@@ -0,0 +1,159 @@
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 one 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}/info2", HTTPServlet::FileHandler, "qooxdoo_rhcp/build"
65
+ @server.mount "/#{@url_prefix}/get_commands", GetCommandsServlet, @broker
66
+ @server.mount "/#{@url_prefix}/get_lookup_values", GetLookupValuesServlet, @broker
67
+ @server.mount "/#{@url_prefix}/execute", ExecuteServlet, @broker
68
+ @logger.info("http exporter has been initialized - once started, it will listen on port '#{port}' for URLs starting with prefix '#{@url_prefix}'")
69
+ end
70
+
71
+ class BaseServlet < HTTPServlet::AbstractServlet
72
+
73
+ def initialize(server, broker)
74
+ super(server)
75
+ @broker = broker
76
+ end
77
+
78
+ def do_GET(req, res)
79
+ res['Content-Type'] = "application/json"
80
+ begin
81
+ #@logger.info("executing #{req}")
82
+ do_it(req, res)
83
+ #@logger.info("finished executing #{req}")
84
+ rescue RHCP::RhcpException => ex
85
+ # TODO define error format (should we send back a JSON-ified response object here?)
86
+ @logger.error("got an exception while executing request . #{ex}")
87
+ res.status = 500
88
+ res.body = [ ex.to_s ].to_json()
89
+ #puts ex
90
+ #puts ex.backtrace.join("\n")
91
+ end
92
+ end
93
+
94
+ # TODO is this a good idea?
95
+ def do_POST(req, res)
96
+ do_GET(req, res)
97
+ end
98
+
99
+ end
100
+
101
+ class GetCommandsServlet < BaseServlet
102
+
103
+ def do_it(req, res)
104
+ commands = @broker.get_command_list
105
+ puts "about to send back commands : #{commands.values.to_json()}"
106
+ res.body = commands.values.to_json()
107
+ end
108
+ end
109
+
110
+ class GetLookupValuesServlet < BaseServlet
111
+
112
+ def do_it(req, res)
113
+ partial_value = req.query.has_key?('partial') ? req.query['partial'] : ''
114
+ command = @broker.get_command(req.query['command'])
115
+ param = command.get_param(req.query['param'])
116
+ lookup_values = param.get_lookup_values(partial_value)
117
+ res.body = lookup_values.to_json()
118
+ end
119
+
120
+ end
121
+
122
+ class ExecuteServlet < BaseServlet
123
+
124
+ def do_it(req, res)
125
+ @logger.info("got an execute request : #{req}")
126
+ # TODO filter "nocache" parameters from query string
127
+ request = RHCP::Request.reconstruct_from_json(@broker, req.body)
128
+
129
+ response = request.execute()
130
+
131
+ @logger.debug("sending back response : #{response.to_json()}")
132
+ res.body = response.to_json()
133
+ end
134
+
135
+ end
136
+
137
+ def run
138
+ @server.start
139
+ end
140
+
141
+ def start
142
+ puts "about to start server..."
143
+ @thread = Thread.new {
144
+ puts "in thread; starting server..."
145
+ @server.start
146
+ puts "in thread: continuing..."
147
+ }
148
+ end
149
+
150
+ def stop
151
+ puts "about to stop server"
152
+ @server.stop
153
+ puts "thread stopped"
154
+ # TODO wait until after the server has ended - when this method exits, the server is still shutting down
155
+ end
156
+
157
+ end
158
+
159
+ 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
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+
3
+ ruby -I lib test/rhcp/http_test_server.rb
@@ -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