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.
data/Rakefile ADDED
@@ -0,0 +1,116 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+ require 'rake/packagetask'
5
+ require 'rake/gempackagetask'
6
+
7
+ $LOAD_PATH.push('lib')
8
+ require File.join(File.dirname(__FILE__), 'lib', 'rhcp')
9
+
10
+ PKG_NAME = 'rhcp'
11
+ PKG_VERSION = RHCP::Version.to_s
12
+ PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
13
+
14
+ desc "Default Task"
15
+ task :default => [ :test ]
16
+
17
+ ###############################################
18
+ ### TESTS
19
+ Rake::TestTask.new() { |t|
20
+ t.libs << "lib"
21
+ t.libs << "test"
22
+ t.libs << "test/rhcp"
23
+ t.test_files = FileList['test/rhcp/*_test.rb']
24
+ t.verbose = true
25
+ }
26
+
27
+ ###############################################
28
+ ### RDOC
29
+ Rake::RDocTask.new { |rdoc|
30
+ rdoc.rdoc_dir = 'doc'
31
+ rdoc.title = "RHCP - really helpful command protocol"
32
+ rdoc.options << '--line-numbers' << '--inline-source' <<
33
+ '--accessor' << 'cattr_accessor=object'
34
+ rdoc.template = "#{ENV['template']}.rb" if ENV['template']
35
+ #rdoc.rdoc_files.include('README', 'CHANGELOG')
36
+ rdoc.rdoc_files.include('lib/**/*.rb')
37
+ }
38
+
39
+ ###############################################
40
+ ### METRICS
41
+ task :lines do
42
+ lines, codelines, total_lines, total_codelines = 0, 0, 0, 0
43
+
44
+ for file_name in FileList["lib/**/*.rb"]
45
+ f = File.open(file_name)
46
+
47
+ while line = f.gets
48
+ lines += 1
49
+ next if line =~ /^\s*$/
50
+ next if line =~ /^\s*#/
51
+ codelines += 1
52
+ end
53
+ puts "L: #{sprintf("%4d", lines)}, LOC #{sprintf("%4d", codelines)} | #{file_name}"
54
+
55
+ total_lines += lines
56
+ total_codelines += codelines
57
+
58
+ lines, codelines = 0, 0
59
+ end
60
+
61
+ puts "Total: Lines #{total_lines}, LOC #{total_codelines}"
62
+ end
63
+
64
+
65
+ # TODO add rcov
66
+ # rcov -I lib/ -x rcov.rb -x rhcp.rb test/**/*.rb
67
+
68
+ ###############################################
69
+ ### GEM
70
+ spec = Gem::Specification.new do |s|
71
+ s.name = PKG_NAME
72
+ s.version = PKG_VERSION
73
+ s.summary = "Library for exporting parts of your application using the rhcp protocol"
74
+ s.description = "The rhcp protocol allows you to register commands along with their parameter descriptions and some metadata that can then be executed by rhcp clients."
75
+
76
+ dist_dirs = [ "lib", "test" ]
77
+ # TODO add README and CHANGELOG
78
+ s.files = [ "Rakefile" ]
79
+ dist_dirs.each do |dir|
80
+ s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if do |item|
81
+ item.include?( "\.svn" )
82
+ end
83
+ end
84
+
85
+ # s.add_dependency('uuidtools', '>= 1.0.0')
86
+ # s.add_dependency('builder', '>= 1.2.4')
87
+
88
+ s.require_path = 'lib'
89
+ s.test_files = Dir.glob('test/**/*.rb')
90
+
91
+
92
+ begin
93
+ s.post_install_message = <<-TEXT
94
+
95
+ This is the initial release of rhcp - hope you'll like it.
96
+ Feel free to send me feedback to rhcp at hitchhackers dot net.
97
+
98
+ TEXT
99
+ rescue Exception
100
+ end
101
+
102
+ s.has_rdoc = true
103
+ #s.extra_rdoc_files = %w( README )
104
+ #s.rdoc_options.concat ['--main', 'README']
105
+
106
+ s.author = "Philipp Traeder"
107
+ s.email = "philipp@hitchhackers.net"
108
+ s.homepage = "http://hitchhackers.net/projects/rhcp"
109
+ s.rubyforge_project = "rhcp"
110
+ end
111
+
112
+ Rake::GemPackageTask.new(spec) do |p|
113
+ p.gem_spec = spec
114
+ p.need_tar = true
115
+ p.need_zip = true
116
+ end
@@ -0,0 +1,37 @@
1
+ module RHCP
2
+
3
+ # Applications register the commands they are implementing at a broker.
4
+ # The broker holds the list of commands and is the central entry point for
5
+ # all code that wants to export/publish these commands
6
+ class Broker
7
+
8
+ def initialize
9
+ # command_name => command
10
+ @known_commands = Hash.new()
11
+ end
12
+
13
+ # returns a list of all known commands
14
+ def get_command_list()
15
+ @known_commands
16
+ end
17
+
18
+ # returns the specified command object
19
+ def get_command(command_name)
20
+ raise RHCP::RhcpException.new("no such command : #{command_name}") unless @known_commands.has_key?(command_name)
21
+ get_command_list[command_name]
22
+ end
23
+
24
+ # registers a new command - this method should be called by the application
25
+ # providing the command
26
+ def register_command(command)
27
+ raise RHCP::RhcpException.new("duplicate command name : #{command.name}") if @known_commands.has_key?(command.name)
28
+ @known_commands[command.name] = command
29
+ end
30
+
31
+ # removes all commands that have been registered previously
32
+ def clear
33
+ @known_commands = Hash.new()
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,51 @@
1
+ require 'rhcp/command_param'
2
+
3
+ module RHCP
4
+
5
+ module Client
6
+
7
+ # This is a proxy representing a remote CommandParam - see
8
+ # RHCP::CommandParam for details
9
+ class CommandParamStub < RHCP::CommandParam
10
+
11
+ # the block that should be executed when +get_lookup_values()+ is called
12
+ # on this CommandParamStub
13
+ attr_accessor :get_lookup_values_block
14
+
15
+ # when constructing a CommandParamStub, the +:lookup_method+ option is
16
+ # set to +remote_get_lookup_values+ so that a method can be injected
17
+ # from outside that will retrieve the lookup values (using the
18
+ # +get_lookup_values_block+ property).
19
+ def initialize(name, description, options)
20
+ # we don't need to stub anything if the method does not have lookup values
21
+ if (options[:has_lookup_values])
22
+ options[:lookup_method] = self.method(:remote_get_lookup_values)
23
+ end
24
+ super(name, description, options)
25
+ end
26
+
27
+ def self.reconstruct_from_json(json_data)
28
+ object = json_data.instance_of?(Hash) ? json_data : JSON.parse(json_data)
29
+ args = object.values_at('name', 'description')
30
+ args << {
31
+ :allows_multiple_values => object['allows_multiple_values'],
32
+ :has_lookup_values => object['has_lookup_values'],
33
+ :is_default_param => object['is_default_param'],
34
+ :mandatory => object['mandatory']
35
+ }
36
+ self.new(*args)
37
+ end
38
+
39
+ def remote_get_lookup_values(partial_value = "")
40
+ @get_lookup_values_block.call(partial_value)
41
+ end
42
+
43
+ end
44
+
45
+ end
46
+
47
+ end
48
+
49
+
50
+
51
+
@@ -0,0 +1,56 @@
1
+ require 'rhcp/command'
2
+ require 'rhcp/client/command_param_stub'
3
+ require 'rubygems'
4
+ require 'json'
5
+
6
+
7
+ module RHCP
8
+
9
+ module Client
10
+
11
+ # This is a stub that represents a remote RHCP::Command.
12
+ # Instances of this class will live in a client that uses one of the brokers
13
+ # from the RHCP::Client package. The interesting aspect about this stub is
14
+ # that the execute method is modified so that it does not execute the
15
+ # command directly, but can be modified from outside (by setting the
16
+ # +execute_block+ property) so that broker classes can inject their
17
+ # remote invocation logic.
18
+ class CommandStub < RHCP::Command
19
+
20
+ attr_accessor :execute_block
21
+
22
+ # constructs a new instance
23
+ # should not be invoked directly, but is called from +reconstruct_from_json+
24
+ def initialize(name, description)
25
+ super(name, description, lambda {})
26
+ end
27
+
28
+ # builds a CommandStub out of some json data (either serialized as string
29
+ # or already unpacked into a ruby-hash)
30
+ # all nested params are unmarshalled as RHCP::CommandParamStub instances
31
+ # so that we are able to inject the logic for invoking get_lookup_values
32
+ # remotely
33
+ def self.reconstruct_from_json(json_data)
34
+ object = json_data.instance_of?(Hash) ? json_data : JSON.parse(json_data)
35
+ args = object.values_at('name', 'description')
36
+ instance = self.new(*args)
37
+ object['params'].each do |p|
38
+ param = RHCP::Client::CommandParamStub.reconstruct_from_json(p)
39
+ instance.add_param(param)
40
+ end
41
+ instance
42
+ end
43
+
44
+ # we don't want to execute the command block as the normal RHCP::Command
45
+ # does, but rather want to call the block injected by the registry that
46
+ # has created this stub.
47
+ def execute_request(request)
48
+ @execute_block.call(request)
49
+ end
50
+ end
51
+
52
+ end
53
+
54
+ end
55
+
56
+
@@ -0,0 +1,86 @@
1
+ #require 'rhcp/rhcp'
2
+ require 'rhcp/broker'
3
+ require 'rhcp/rhcp_exception'
4
+ require 'rhcp/client/command_stub'
5
+ require 'net/http'
6
+ require 'json'
7
+
8
+ module RHCP
9
+
10
+ module Client
11
+
12
+ # This is an implementation of a RHCP broker that retrieves it's data via
13
+ # http from a remote broker. Since it implements the same interface as RHCP::Broker,
14
+ # clients can use it exactly as if they were talking with the broker itself.
15
+ # TODO shouldn't this descent from Broker?
16
+ class HttpBroker
17
+
18
+ def initialize(url)
19
+ # TODO should this really be an URL? or just a host name?
20
+ @url = url
21
+ @logger = RHCP::ModuleHelper.instance.logger
22
+ @logger.debug "connecting to #{@url}"
23
+ res = Net::HTTP.new(@url.host, @url.port).start { |http| http.get("/rhcp/") }
24
+ if (res.code == "200")
25
+ @logger.info "connected to '#{@url}' successfully."
26
+ else
27
+ @logger.error "cannot connect to '#{@url}' : got http status code #{res.code}"
28
+ raise RHCP::RhcpException.new("could not connect to '#{@url}' : got http status code #{res.code} (#{res.message})");
29
+ end
30
+ end
31
+
32
+ # returns a list of all known commands
33
+ # TODO add caching for commands
34
+ def get_command_list()
35
+ res = Net::HTTP.new(@url.host, @url.port).start { |http| http.get("/rhcp/get_commands") }
36
+ if (res.code != "200")
37
+ raise RHCP::RhcpException.new("could not retrieve command list from remote server : http status #{res.code} (#{res.message})")
38
+ end
39
+
40
+ @logger.debug "raw response : >>#{res.body}<<"
41
+
42
+ # the commands are transferred as array => convert into hash
43
+ command_array = JSON.parse(res.body)
44
+ commands = Hash.new()
45
+ command_array.each do |command_string|
46
+ command = RHCP::Client::CommandStub.reconstruct_from_json(command_string)
47
+ commands[command.name] = command
48
+ # and inject the block for executing over http
49
+ command.execute_block = lambda {
50
+ |req|
51
+ @logger.debug("executing command #{command.name} via http using #{@url}")
52
+
53
+ res = Net::HTTP.new(@url.host, @url.port).start { |http| http.post("/rhcp/execute", req.to_json()) }
54
+ if (res.code != "200")
55
+ raise RHCP::RhcpException.new("could not execute command using remote server : http status #{res.code} (#{res.message})")
56
+ end
57
+
58
+ json_response = RHCP::Response.reconstruct_from_json(res.body)
59
+ @logger.debug "json response : #{json_response}"
60
+ json_response
61
+ }
62
+
63
+ # inject appropriate blocks for all params
64
+ command.params.each do |name, param|
65
+ param.get_lookup_values_block = lambda {
66
+ |partial_value|
67
+ @logger.debug("getting lookup values for param #{name} of command #{command.name} using #{@url}")
68
+ # TODO add caching for lookup values
69
+ res = Net::HTTP.new(@url.host, @url.port).start { |http|
70
+ http.get("/rhcp/get_lookup_values?command=#{command.name}&param=#{name}")
71
+ }
72
+ if (res.code != "200")
73
+ raise RHCP::RhcpException.new("could not retrieve lookup values from remote server : http status #{res.code} (#{res.message})")
74
+ end
75
+ JSON.parse(res.body)
76
+ }
77
+ end
78
+ end
79
+ commands
80
+ end
81
+
82
+ end
83
+
84
+ end
85
+
86
+ end
@@ -0,0 +1,120 @@
1
+ module RHCP
2
+
3
+ require 'rubygems'
4
+ require 'json'
5
+
6
+ #require 'rhcp/rhcp'
7
+ require 'rhcp/response'
8
+
9
+ # This class represents a single RHCP command, i.e. a functionality that
10
+ # should be exposed to clients.
11
+ class Command
12
+
13
+ # TODO add read_write/read_only marker
14
+
15
+ # TODO add metadata about the command's result (display type ("table"), column order etc.)
16
+
17
+ # a unique name for this command
18
+ attr_reader :name
19
+
20
+ # textual description of what this command is doing
21
+ attr_reader :description
22
+
23
+ # the parameters supported by this command
24
+ attr_reader :params
25
+
26
+ # the block that should be executed if this command is invoked
27
+ attr_reader :block
28
+
29
+ # the parameter that is the default param for this command; might be nil
30
+ attr_reader :default_param
31
+
32
+ def initialize(name, description, block)
33
+ @logger = RHCP::ModuleHelper.instance().logger
34
+
35
+ @name = name
36
+ @description = description
37
+ @block = block
38
+ @params = Hash.new()
39
+ @default_param = nil
40
+ end
41
+
42
+ # adds a new parameter and returns the command
43
+ def add_param(new_param)
44
+ raise "duplicate parameter name '#{new_param.name}'" if @params.has_key?(new_param.name)
45
+ @params[new_param.name] = new_param
46
+ if new_param.is_default_param
47
+ if @default_param != nil
48
+ raise RHCP::RhcpException.new("there can be only one default parameter per command, and the parameter '#{@default_param.name}' has already been marked as default")
49
+ end
50
+ @default_param = new_param
51
+ end
52
+ self
53
+ end
54
+
55
+ # returns the specified CommandParam
56
+ # or throws an RhcpException if the parameter does not exist
57
+ def get_param(param_name)
58
+ raise RHCP::RhcpException.new("no such parameter : #{param_name}") unless @params.has_key?(param_name)
59
+ @params[param_name]
60
+ end
61
+
62
+ # invokes this command
63
+ def execute_request(request)
64
+ @logger.debug "gonna execute request >>#{request}<<"
65
+ response = RHCP::Response.new()
66
+ begin
67
+ # TODO redirect the block's output (both Logger and STDOUT/STDERR) and send it back with the response
68
+ result = block.call(request, response)
69
+ response.set_payload(result)
70
+ rescue => ex
71
+ response.mark_as_error(ex.to_s, ex.backtrace.join("\n"))
72
+ end
73
+
74
+ fire_post_exec_event(request, response)
75
+
76
+ response
77
+ end
78
+
79
+ def execute(param_values = {})
80
+ req = RHCP::Request.new(self, param_values)
81
+ execute_request(req)
82
+ end
83
+
84
+
85
+ # we do not serialize the block (no sense in this) nor the default param
86
+ # when the command is unmarshalled as stub, the default param will be set again
87
+ # automatically ba add_param(), and the block will be replaced by the block
88
+ # that does the remote invocation
89
+ def to_json(*args)
90
+ {
91
+ 'name' => @name,
92
+ 'description' => @description,
93
+ 'params' => @params.values
94
+ }.to_json(*args)
95
+ end
96
+
97
+ # TODO think about moving this and the whole command thing to the broker
98
+ def Command.register_post_exec_listener(block)
99
+ @@listener = [] unless defined?(@@listener)
100
+ @@listener << block
101
+ end
102
+
103
+ def Command.clear_post_exec_listeners()
104
+ @@listener = []
105
+ end
106
+
107
+ def fire_post_exec_event(request, response)
108
+ return unless defined? @@listener
109
+ @@listener.each do |listener|
110
+ begin
111
+ listener.call(request, response)
112
+ rescue => ex
113
+ @logger.warn "a post-exec listener failed with the following message : #{ex}"
114
+ end
115
+ end
116
+ end
117
+
118
+ end
119
+
120
+ end
@@ -0,0 +1,93 @@
1
+ require 'rubygems'
2
+ require 'json'
3
+
4
+ require 'rhcp/rhcp_exception'
5
+
6
+ module RHCP
7
+
8
+ # This class represents a single parameter that can be specified for
9
+ # a certain command. It is used by the server side to define which
10
+ # commands are available for clients.
11
+ class CommandParam
12
+
13
+ # a unique name for this parameter
14
+ attr_reader :name
15
+ # a textual description of this parameter's meaning
16
+ attr_reader :description
17
+ # "true" if the command can not be invoked without this parameter
18
+ attr_reader :mandatory
19
+ # "true" if there are lookup values available for this parameter
20
+ attr_reader :has_lookup_values
21
+ # "true" if this parameter might be specified multiple times, resulting
22
+ # in a list of values for this parameter
23
+ attr_reader :allows_multiple_values
24
+ # "true" if this parameter is the default parameter for the enclosing command
25
+ attr_reader :is_default_param
26
+
27
+ # creates a new command parameter
28
+ # options is a hash, possibly holding the following values (all optional)
29
+ # :mandatory true if the parameter is necessary for this command
30
+ # :allows_multiple_values true if this parameter might be specified multiple times
31
+ # :lookup_method a block that returns an array of lookup values valid for this param
32
+ # :is_default_param true if this parameter is the default param for the enclosing command
33
+ def initialize(name, description, options = Hash.new)
34
+ @name = name
35
+ @description = description
36
+
37
+ @mandatory = options[:mandatory] || false
38
+ @lookup_value_block = options[:lookup_method]
39
+ @has_lookup_values = @lookup_value_block != nil
40
+ @allows_multiple_values = options[:allows_multiple_values] || false
41
+ @is_default_param = options[:is_default_param] || false
42
+ end
43
+
44
+ # returns lookup values for this parameter
45
+ # if "partial_value" is specified, only those values are returned that start
46
+ # with "partial_value"
47
+ def get_lookup_values(partial_value = "")
48
+ if @has_lookup_values
49
+ @lookup_value_block.call().grep(/^#{partial_value}/)
50
+ else
51
+ []
52
+ end
53
+ end
54
+
55
+ # throws an exception if +possible_value+ is not a valid value for this parameter
56
+ # returns true otherwise
57
+ # note that this methods expects +possible_value+ to be an array since all param values
58
+ # are specified as arrays
59
+ def check_param_is_valid(possible_value)
60
+
61
+ # formal check : single-value params against multi-value params
62
+ if ((! @allows_multiple_values) && (possible_value.size > 1))
63
+ raise RHCP::RhcpException.new("multiple values specified for single-value parameter '#{@name}'")
64
+ end
65
+
66
+ # check against lookup values
67
+ if @has_lookup_values
68
+ possible_value.each do |value|
69
+ if ! get_lookup_values().include?(value)
70
+ raise RHCP::RhcpException.new("invalid value '#{value}' for parameter '#{@name}'")
71
+ end
72
+ end
73
+ end
74
+
75
+ true
76
+ end
77
+
78
+ # we do not serialize the lookup_method (it couldn't be invoked remotely
79
+ # anyway)
80
+ def to_json(*args)
81
+ {
82
+ 'name' => @name,
83
+ 'description' => @description,
84
+ 'allows_multiple_values' => @allows_multiple_values,
85
+ 'has_lookup_values' => @has_lookup_values,
86
+ 'is_default_param' => @is_default_param,
87
+ 'mandatory' => @mandatory
88
+ }.to_json(*args)
89
+ end
90
+
91
+ end
92
+
93
+ end
@@ -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