sk-hoth 0.0.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/README.rdoc +69 -39
  2. data/THANKS.md +9 -0
  3. data/TODO +2 -0
  4. data/lib/hoth/encoding/json.rb +28 -0
  5. data/lib/hoth/encoding/no_op.rb +19 -0
  6. data/lib/hoth/endpoint.rb +28 -0
  7. data/lib/hoth/exceptions.rb +14 -0
  8. data/lib/hoth/extension/core/exception.rb +15 -0
  9. data/lib/hoth/modules.rb +27 -0
  10. data/lib/hoth/providers/bertrpc_provider.rb +35 -0
  11. data/lib/hoth/providers/rack_provider.rb +45 -0
  12. data/lib/hoth/service.rb +50 -0
  13. data/lib/hoth/service_definition.rb +18 -0
  14. data/lib/hoth/service_module.rb +49 -0
  15. data/lib/hoth/service_registry.rb +34 -0
  16. data/lib/hoth/services.rb +51 -0
  17. data/lib/hoth/transport/base.rb +19 -0
  18. data/lib/hoth/transport/bert.rb +87 -0
  19. data/lib/hoth/transport/http.rb +41 -0
  20. data/lib/hoth/transport/http_hmac.rb +37 -0
  21. data/lib/hoth/transport/workling.rb +23 -0
  22. data/lib/hoth/transport.rb +48 -0
  23. data/lib/hoth/util/logger.rb +46 -0
  24. data/lib/hoth.rb +56 -0
  25. data/spec/spec_helper.rb +7 -26
  26. data/spec/unit/encoding/json_spec.rb +25 -0
  27. data/spec/unit/endpoint_spec.rb +34 -0
  28. data/spec/unit/extension/core/exception_spec.rb +34 -0
  29. data/spec/unit/hoth_spec.rb +30 -0
  30. data/spec/unit/providers/rack_provider_spec.rb +49 -0
  31. data/spec/unit/service_definition_spec.rb +21 -0
  32. data/spec/unit/service_module_spec.rb +59 -0
  33. data/spec/unit/service_spec.rb +77 -0
  34. data/spec/unit/transport/base_spec.rb +43 -0
  35. data/spec/unit/transport/http_hmac_spec.rb +44 -0
  36. data/spec/unit/transport/http_spec.rb +73 -0
  37. data/spec/unit/transport/workling_spec.rb +42 -0
  38. data/spec/unit/transport_spec.rb +29 -0
  39. metadata +86 -23
  40. data/lib/king_soa/rack/middleware.rb +0 -47
  41. data/lib/king_soa/registry.rb +0 -55
  42. data/lib/king_soa/service.rb +0 -88
  43. data/lib/king_soa.rb +0 -56
  44. data/spec/king_soa/rack/middleware_spec.rb +0 -36
  45. data/spec/king_soa/registry_spec.rb +0 -28
  46. data/spec/king_soa/service_spec.rb +0 -46
  47. data/spec/server/app.rb +0 -26
data/README.rdoc CHANGED
@@ -1,56 +1,86 @@
1
- = KingSoa
2
-
3
- Creating a SOA requires a centralized location to define all services within the
4
- SOA. Furthermore you want to know where those services live.
5
-
6
- == Tech details
7
-
8
- the soa registry is keeping a bunch of service objects which know where a method
9
- lives. Methods are classes with an self.perform method and can be located local,
10
- remote(http) or beeing put onto a queue
1
+ = Hoth
11
2
 
3
+ Creating a SOA requires a centralized location to define all services within the SOA. Furthermore you want to know where those services live.
12
4
 
5
+ = How to use
13
6
 
14
7
  == Install
15
8
 
16
- gem install king_soa
9
+ gem install hoth
17
10
 
18
- == Define services
11
+ == Define services and modules
19
12
 
20
- === Low Level
13
+ === Service-Definition
21
14
 
22
- You can define services anywhere in your app and add them to the service
23
- registry.
24
- Be aware that the registry is a singleton, so that the rack middleware and your
25
- app are seeing the same :
26
- # create the service
27
- service = KingSoa::Service.new(:name=>:increment_usage,
28
- :url=>'http://localhost:4567',
29
- :auth_key=>'12345')
30
- # register
31
- KingSoa::Registry << service
15
+ This is how you define services:
32
16
 
33
- # somewhere in your app just call your service method
34
- KingSoa.increment_usage(12)
17
+ Hoth::Services.define do
18
+
19
+ service :service_name do |first_param, second_param|
20
+ returns :descriptive_name
21
+ end
22
+
23
+ end
35
24
 
36
- A service name is always required. If the service is to be called remotely you
37
- must add a url and an auth_key.
38
- The transport is done via http calls, so to make it secure you should either use
39
- https in public or hide the servers somwhere on your farm.
40
-
41
- == Integration
25
+ This definition describes a service with a name, some parameters and its return value. The naming of the parameters is just for your understanding and will never be used again, so be descriptive. Same goes for the return value. The only exception is, if you want to assure that a service returns nil you can write
26
+
27
+ returns :nothing
28
+
29
+ A service whith this return value will always return nil. You can also specify `:nil`, with the same result.
30
+
31
+ === Module-Definition
32
+
33
+ After defining all you services, you need to specify in which modules they live. Each module can be seen as a set of implemented services. Each module can have one or more endpoints. Here is how you define these modules with its endpoints and services:
34
+
35
+
36
+ Hoth::Modules.define do
37
+
38
+ service_module :module_name do
39
+ env :development, :test do
40
+ endpoint :default do
41
+ host 'localhost'
42
+ port 3000
43
+ transport :http
44
+ end
45
+
46
+ endpoint :bert do
47
+ host 'localhost'
48
+ port 9999
49
+ transport :bert
50
+ end
51
+ end
52
+
53
+ env :production do
54
+ endpoint :default do
55
+ host '192.168.1.12'
56
+ port 3000
57
+ transport :http
58
+ end
59
+
60
+ endpoint :bert do
61
+ host '192.168.1.15'
62
+ port 9999
63
+ transport :bert
64
+ end
65
+ end
66
+
67
+ add_service :first_service
68
+ add_service :second_service, :via => :bert
69
+ end
70
+
71
+ end
42
72
 
43
- === Rails
44
73
 
74
+ As you can see, it is possible to define different endpoints for different environments. Each endpoint has a host, a port and a transport-type. After defining your endpoints you can add your previously defined services to the module and define which endpoint they should use. If you do not specify an endpoint the :default endpoint will be used.
45
75
 
46
- === Sinatra
76
+ == Integrate in your project
47
77
 
48
- Take a look at spec/server/app where you can see a minimal sinatra implementation
49
- The base is just:
50
- require 'king_soa'
51
- use KingSoa::Rack::Middleware
78
+ Just execute current code (in rails you can add this line to an initializer):
79
+
80
+ Hoth.init!
81
+
82
+ By default, Hoth looks for the files service_definition and module_definition in the config-Directory (`./config`). If you need to load these files from another place, just set `Hoth.config_path` to your needs.
52
83
 
53
-
54
84
  == Note on Patches/Pull Requests
55
85
 
56
86
  * Fork the project.
@@ -61,4 +91,4 @@ The base is just:
61
91
 
62
92
  == Copyright
63
93
 
64
- Copyright (c) 2010 Georg Leciejewski. See LICENSE for details.
94
+ Copyright (c) 2009-2010 Dirk Breuer. See LICENSE for details.
data/THANKS.md ADDED
@@ -0,0 +1,9 @@
1
+ # Hoth THANKS
2
+
3
+ A number of people have contributed to Hoth by reporting problems,
4
+ suggesting improvements or submitting changes. Some of these people are:
5
+
6
+ * Andreas Bade (<andi.bade@gmail.com>)
7
+ * Sebastian Cohnen (<sebastian.cohnen@gmail.com>)
8
+ * Björn Vollmer (<bjoern.vollmer@googlemail.com>)
9
+ * Andreas Riemer
data/TODO ADDED
@@ -0,0 +1,2 @@
1
+ * Make the rack provider independent from one specific transport.
2
+ * Make the bodies of the rack_provider return an object which responds to each in order not break on Ruby 1.9.
@@ -0,0 +1,28 @@
1
+ require 'json'
2
+
3
+ module Hoth
4
+ module Encoding
5
+ class Json
6
+
7
+ class <<self
8
+ def encode(string)
9
+ string.to_json
10
+ end
11
+
12
+ def decode(string)
13
+ begin
14
+ Hoth::Logger.debug "Original params before decode: #{string.inspect}"
15
+ JSON.parse(string)
16
+ rescue JSON::ParserError => jpe
17
+ raise EncodingError.wrap(jpe)
18
+ end
19
+ end
20
+
21
+ def content_type
22
+ "application/json"
23
+ end
24
+ end
25
+
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,19 @@
1
+ module Hoth
2
+ module Encoding
3
+ class NoOp
4
+
5
+ class <<self
6
+ def encode(string)
7
+ string
8
+ end
9
+
10
+ def decode(string)
11
+ string
12
+ end
13
+
14
+ def content_type; end
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,28 @@
1
+ module Hoth
2
+ class Endpoint
3
+ attr_accessor :host, :port, :module_name, :transport
4
+
5
+ class ConfigEvaluator
6
+ attr_reader :endpoint
7
+ def initialize(endpoint, &block)
8
+ @endpoint = endpoint
9
+ instance_eval(&block)
10
+ end
11
+
12
+ [:host, :port, :module_name, :transport].each do |endpoint_attribute|
13
+ define_method endpoint_attribute do |value|
14
+ endpoint.send("#{endpoint_attribute}=", value)
15
+ end
16
+ end
17
+ end
18
+
19
+ def initialize(&block)
20
+ ConfigEvaluator.new(self, &block)
21
+ end
22
+
23
+ def to_url
24
+ "http://#{@host}:#{@port}/execute"
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,14 @@
1
+ module Hoth
2
+ class HothException < StandardError
3
+ attr_reader :original
4
+ def self.wrap(original)
5
+ wrapped = new("#{original.class} says: #{original.message}")
6
+ wrapped.set_backtrace original.backtrace
7
+ wrapped.instance_variable_set :@original, original
8
+ wrapped
9
+ end
10
+ end
11
+
12
+ class TransportError < HothException; end
13
+ class TransportException < HothException; end
14
+ end
@@ -0,0 +1,15 @@
1
+ class Exception
2
+ def to_json(*params)
3
+ {
4
+ 'json_class' => self.class.name,
5
+ 'message' => self.message,
6
+ 'backtrace' => self.backtrace
7
+ }.to_json(*params)
8
+ end
9
+
10
+ def self.json_create(hash)
11
+ exception = new(hash["message"])
12
+ exception.set_backtrace hash['backtrace']
13
+ exception
14
+ end
15
+ end
@@ -0,0 +1,27 @@
1
+ module Hoth
2
+ class Modules
3
+ include Singleton
4
+
5
+ attr_reader :service_modules
6
+
7
+ def self.define(&block)
8
+ instance.instance_eval(&block)
9
+ end
10
+
11
+ def self.module(module_name)
12
+ instance.service_modules[module_name]
13
+ end
14
+
15
+ def service_module(module_name, &block)
16
+ service_module = ServiceModule.new(:name => module_name)
17
+ service_module.instance_eval(&block)
18
+ @service_modules[module_name] = service_module
19
+ end
20
+
21
+ private
22
+
23
+ def initialize
24
+ @service_modules = {}
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,35 @@
1
+ require 'ernie'
2
+
3
+ module Hoth
4
+ module Providers
5
+ class BertRPCProvider
6
+
7
+ def self.create_ernie_definition
8
+ Ernie.log "Possible Service implementations: #{Object.constants.grep(/.*Impl$/).inspect}"
9
+ Object.constants.grep(/.*Impl$/).each do |impl_class_name|
10
+ if impl_class = Object.const_get(impl_class_name) #&& impl_class.respond_to?(:execute)
11
+ Ernie.log "Service implementation was loaded! (#{impl_class.inspect})"
12
+ if impl_class.respond_to?(:execute)
13
+ service_name = impl_class_name.gsub("Impl", "").underscore.to_sym
14
+ mod(service_name) do
15
+ fun(:execute) do |*args|
16
+ return_value = begin
17
+ Hoth::Transport::Bert::TuplePreparer.prepare(Hoth::Services.send(service_name, *args))
18
+ rescue Exception => e
19
+ Ernie.log %Q{An Exception occured: #{e.message} -- #{e.backtrace.join("\n\t")}}
20
+ false
21
+ end
22
+ end
23
+ end
24
+ else
25
+ Ernie.log "Implementation wasn't applicatable. :execute method is missing!"
26
+ end
27
+ else
28
+ Ernie.log "Service implementation was not loaded! (#{impl_class_name.inspect})"
29
+ end
30
+ end
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,45 @@
1
+ require 'rack/request'
2
+
3
+ module Hoth
4
+ module Providers
5
+ class RackProvider
6
+
7
+ def initialize(app)
8
+ @app = app
9
+ end
10
+
11
+ def call(env)
12
+ Hoth::Logger.debug "env: #{env.inspect}"
13
+ if env["PATH_INFO"] =~ /^\/execute/
14
+ begin
15
+ req = Rack::Request.new(env)
16
+
17
+ service_name = req.params["name"]
18
+ service_params = req.params["params"]
19
+
20
+ responsible_service = ServiceRegistry.locate_service(service_name)
21
+
22
+ decoded_params = responsible_service.transport.encoder.decode(service_params)
23
+ result = Hoth::Services.send(service_name, *decoded_params)
24
+
25
+ encoded_result = responsible_service.transport.encoder.encode({"result" => result})
26
+
27
+ [200, {'Content-Type' => responsible_service.transport.encoder.content_type, 'Content-Length' => "#{encoded_result.length}"}, [encoded_result]]
28
+ rescue Exception => e
29
+ Hoth::Logger.debug "e: #{e.message}"
30
+ if responsible_service
31
+ encoded_error = responsible_service.transport.encoder.encode({"error" => e})
32
+ [500, {'Content-Type' => service.transport.encoder.content_type, 'Content-Length' => "#{encoded_error.length}"}, [encoded_error]]
33
+ else
34
+ plain_error = "An error occuered! (#{e.message})"
35
+ [500, {'Content-Type' => "text/plain", 'Content-Length' => "#{plain_error.length}"}, [plain_error]]
36
+ end
37
+ end
38
+ else
39
+ @app.call(env)
40
+ end
41
+ end
42
+
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,50 @@
1
+ module Hoth
2
+ class Service
3
+ attr_accessor :name, :params_arity, :module
4
+
5
+ def initialize(name, &block)
6
+ @name = name
7
+ @params_arity = block.arity
8
+ instance_eval(&block)
9
+ end
10
+
11
+ def returns(return_value)
12
+ @return_value = return_value
13
+ end
14
+
15
+ def transport
16
+ @transport ||= Transport.create(endpoint.transport, self)
17
+ end
18
+
19
+ def impl_class
20
+ @impl_class_name ||= "#{self.name.to_s.camelize}Impl"
21
+ begin
22
+ @impl_class_name.constantize
23
+ rescue NameError => e
24
+ # no local implementation
25
+ false
26
+ end
27
+ end
28
+
29
+ def is_local?
30
+ !!impl_class
31
+ end
32
+
33
+ def execute(*args)
34
+ result = self.is_local? ? impl_class.send(:execute, *args) : transport.call_remote_with(*args)
35
+ return return_nothing? ? nil : result
36
+ end
37
+
38
+ def return_nothing?
39
+ [:nothing, :nil, nil].include? @return_value
40
+ end
41
+
42
+ def via_endpoint(via = nil)
43
+ @via_endpoint = via || :default
44
+ end
45
+
46
+ def endpoint
47
+ @endpoint ||= self.module[Hoth.env][@via_endpoint]
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,18 @@
1
+ module Hoth
2
+ class ServiceDefinition
3
+
4
+ # create a new service with service_name and register it at the
5
+ # ServiceRegistry. The paramters of the block define the parameters of the
6
+ # defined service. Within the block you can describe the return value.
7
+ #
8
+ # Example:
9
+ #
10
+ # service :create_account do |account|
11
+ # returns :account_id
12
+ # end
13
+ #
14
+ def service(service_name, &block)
15
+ ServiceRegistry.add_service(Service.new(service_name, &block))
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,49 @@
1
+ module Hoth
2
+ class ServiceModule
3
+ attr_accessor :name, :environments
4
+
5
+ class Environment
6
+ attr_accessor :endpoints
7
+
8
+ def initialize(&block)
9
+ @endpoints = {}
10
+ instance_eval(&block)
11
+ end
12
+
13
+ def endpoint(endpoint_name, &block)
14
+ @endpoints[endpoint_name.to_sym] = Endpoint.new(&block)
15
+ end
16
+
17
+ def [](endpoint_name)
18
+ @endpoints[endpoint_name.to_sym]
19
+ end
20
+ end
21
+
22
+ def initialize(attributes = {})
23
+ @environments = {}
24
+ @name = attributes[:name]
25
+ end
26
+
27
+ def env(*env_names, &block)
28
+ env_names.each do |env_name|
29
+ @environments[env_name.to_sym] = Environment.new(&block)
30
+ end
31
+ end
32
+
33
+ def add_service(service_name, options = {})
34
+ raise HothException.new("no endpoint-definition for environment '#{Hoth.env}' and service '#{service_name}'") unless self.environments[Hoth.env]
35
+
36
+ service = ServiceRegistry.locate_service(service_name.to_sym)
37
+
38
+ raise HothException.new("tried to add service '#{service_name}' but was not defined by service-definition") unless service
39
+
40
+ service.module = self
41
+ service.via_endpoint(options[:via])
42
+ end
43
+
44
+ def [](env_name)
45
+ @environments[env_name.to_sym]
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,34 @@
1
+ module Hoth
2
+
3
+ # The ServiceRegistry knows all registered services. You can register new
4
+ # services and locate existing services.
5
+
6
+ class ServiceRegistry
7
+ include Singleton
8
+
9
+ # add a service to the registry
10
+ def self.add_service(service)
11
+ instance.add_service(service)
12
+ end
13
+ # alias_method :register_service, :add_service
14
+
15
+ # find a service with a given name
16
+ def self.locate_service(service_name)
17
+ instance.locate_service(service_name)
18
+ end
19
+
20
+ def add_service(service) # :nodoc:
21
+ @registry[service.name.to_sym] = service
22
+ end
23
+
24
+ def locate_service(service_name) # :nodoc:
25
+ @registry[service_name.to_sym]
26
+ end
27
+
28
+ private
29
+
30
+ def initialize # :nodoc:
31
+ @registry = {}
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,51 @@
1
+ module Hoth
2
+ class Services
3
+
4
+ # With Hoth::Services.define followed by a block you define all your
5
+ # available services.
6
+ #
7
+ # Example:
8
+ #
9
+ # Hoth::Services.define do
10
+ #
11
+ # service :increment_counter do |counter|
12
+ # returns :nothing
13
+ # end
14
+ #
15
+ # service :value_of_counter do |counter|
16
+ # returns :fixnum
17
+ # end
18
+ #
19
+ # service :create_account do |account|
20
+ # returns :account_id
21
+ # end
22
+ #
23
+ # end
24
+ #
25
+ # after defining your services you can call each of them with
26
+ # <tt>Hoth::Services.service_name(params)</tt>
27
+ #
28
+ # Hoth::Services.increment_counter(counter)
29
+ # current_number = Hoth::Services.value_of_counter(counter)
30
+ # created_account = Hoth::Services.create_account(account)
31
+ #
32
+ # see Hoth::ServiceDefinition for further informations of the block
33
+ # content.
34
+
35
+ def self.define(&block)
36
+ ServiceDefinition.new.instance_eval(&block)
37
+ end
38
+
39
+ class <<self
40
+
41
+ # this is where the services get called
42
+ def method_missing(meth, *args, &blk) # :nodoc:
43
+ if _service = ServiceRegistry.locate_service(meth)
44
+ _service.execute(*args)
45
+ else
46
+ super
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,19 @@
1
+ require 'forwardable'
2
+
3
+ module Hoth
4
+ module Transport
5
+ class Base
6
+ extend Forwardable
7
+
8
+ attr_reader :encoder
9
+
10
+ def_delegators :@service_delegate, :name, :module, :endpoint, :params, :return_nothing?
11
+
12
+ def initialize(service_delegate, options = {})
13
+ @service_delegate = service_delegate
14
+ @encoder = options[:encoder] || Encoding::NoOp
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,87 @@
1
+ require 'bert'
2
+ require 'bertrpc'
3
+
4
+ module Hoth
5
+ module Transport
6
+ class Bert < Base
7
+
8
+ class TuplePreparer
9
+ def self.prepare(obj)
10
+ case obj
11
+ when Array
12
+ obj.collect { |o| prepare o }
13
+ when Hash
14
+ obj.each { |k,v| obj[k] = prepare(v) }
15
+ else
16
+ ruby2tuple obj
17
+ end
18
+ end
19
+
20
+ def self.ruby2tuple(ruby)
21
+ if ruby.respond_to? :to_serialize
22
+ tuple = t[ruby.class.name.underscore, {}]
23
+ ruby.to_serialize.each do |field|
24
+ tuple.last[field] = prepare(ruby.send(field))
25
+ end
26
+ tuple
27
+ else
28
+ ruby
29
+ end
30
+ end
31
+ end
32
+
33
+ class Deserializer
34
+ def self.deserialize(data)
35
+ case data
36
+ when BERT::Tuple
37
+ tuple2ruby data
38
+ when Array
39
+ data.collect { |o| deserialize o }
40
+ when Hash
41
+ data.each { |k,v| data[k] = deserialize(v) }
42
+ else
43
+ data
44
+ end
45
+ end
46
+
47
+ def self.tuple2ruby(tuple)
48
+ case tuple
49
+ when BERT::Tuple
50
+ begin
51
+ ruby_class = tuple.first.camelize.constantize
52
+ ruby_obj = ruby_class.new({})
53
+ ruby_obj.to_serialize.each do |field|
54
+ ruby_obj.send("#{field}=", deserialize(tuple.last[field]))
55
+ end
56
+
57
+ ruby_obj
58
+ rescue NameError => e
59
+ puts %Q{An Exception occured: #{e.message} -- #{e.backtrace.join("\n\t")}}
60
+ tuple
61
+ end
62
+ else
63
+ puts "Was not anything we could decode!"
64
+ tuple
65
+ end
66
+ end
67
+ end
68
+
69
+ def call_remote_with(*args)
70
+ bert_service = BERTRPC::Service.new(self.endpoint.host, self.endpoint.port)
71
+
72
+ response = bert_service.call.send(self.name).execute(*TuplePreparer.prepare(args))
73
+
74
+ if self.return_value
75
+ return Deserializer.deserialize(response)
76
+ else
77
+ return true
78
+ end
79
+ end
80
+
81
+ def decode_params(params)
82
+ Deserializer.deserialize(params)
83
+ end
84
+
85
+ end
86
+ end
87
+ end