stargate 0.1.1

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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +12 -0
  3. data/.gitignore +9 -0
  4. data/CHANGELOG.md +13 -0
  5. data/Dockerfile +11 -0
  6. data/Gemfile +4 -0
  7. data/README.md +240 -0
  8. data/Rakefile +17 -0
  9. data/bin/console +14 -0
  10. data/bin/setup +7 -0
  11. data/docker-compose.yml +4 -0
  12. data/examples/client/client.rb +27 -0
  13. data/examples/server/config.ru +45 -0
  14. data/examples/server/start +7 -0
  15. data/lib/stargate.rb +28 -0
  16. data/lib/stargate/client.rb +12 -0
  17. data/lib/stargate/client/core_ext/kernel.rb +8 -0
  18. data/lib/stargate/client/errors.rb +12 -0
  19. data/lib/stargate/client/injector.rb +37 -0
  20. data/lib/stargate/client/protocol.rb +72 -0
  21. data/lib/stargate/client/protocol/http.rb +47 -0
  22. data/lib/stargate/client/protocol/inproc.rb +28 -0
  23. data/lib/stargate/client/proxy.rb +52 -0
  24. data/lib/stargate/client/remote_execution_error.rb +22 -0
  25. data/lib/stargate/codec.rb +34 -0
  26. data/lib/stargate/codec/bencode.rb +28 -0
  27. data/lib/stargate/codec/json.rb +28 -0
  28. data/lib/stargate/codec/message_pack.rb +28 -0
  29. data/lib/stargate/errors.rb +4 -0
  30. data/lib/stargate/marshal.rb +7 -0
  31. data/lib/stargate/marshal/marshaller.rb +58 -0
  32. data/lib/stargate/marshal/payload.rb +31 -0
  33. data/lib/stargate/marshal/unmarshaller.rb +46 -0
  34. data/lib/stargate/metadata.rb +68 -0
  35. data/lib/stargate/serialization.rb +21 -0
  36. data/lib/stargate/server.rb +10 -0
  37. data/lib/stargate/server/caller.rb +21 -0
  38. data/lib/stargate/server/engine/inproc.rb +15 -0
  39. data/lib/stargate/server/engine/sinatra.rb +97 -0
  40. data/lib/stargate/server/errors.rb +25 -0
  41. data/lib/stargate/server/registry.rb +26 -0
  42. data/lib/stargate/server/registry_version.rb +59 -0
  43. data/lib/stargate/version.rb +3 -0
  44. data/stargate.gemspec +41 -0
  45. metadata +367 -0
@@ -0,0 +1,12 @@
1
+ require 'stargate'
2
+
3
+ module Stargate
4
+ module Client
5
+ require 'stargate/client/errors'
6
+ require 'stargate/client/remote_execution_error'
7
+ require 'stargate/client/protocol'
8
+ require 'stargate/client/proxy'
9
+ require 'stargate/client/injector'
10
+ require 'stargate/client/core_ext/kernel'
11
+ end
12
+ end
@@ -0,0 +1,8 @@
1
+ module Kernel
2
+ def require_remote(url)
3
+ @required_remotes ||= []
4
+ return if @required_remotes.include?(url)
5
+ injector = Stargate::Client::Injector.new(url)
6
+ @required_remotes << url
7
+ end
8
+ end
@@ -0,0 +1,12 @@
1
+ module Stargate
2
+ module Client
3
+ # Public: Base client error.
4
+ Error = Class.new(::Stargate::Error)
5
+
6
+ # Public: Raised when unable to read data from the remote.
7
+ FetchError = Class.new(Error)
8
+
9
+ # Public: Raised when unable to fetch directives from the remote.
10
+ DirectivesFetchError = Class.new(FetchError)
11
+ end
12
+ end
@@ -0,0 +1,37 @@
1
+ module Stargate
2
+ module Client
3
+ class Injector
4
+ include Logging
5
+ include Stargate::Client
6
+
7
+ def initialize(url)
8
+ @uri = URI.parse(url)
9
+ @uri.scheme, codec_id = @uri.scheme.split('+')
10
+ @codec = Codec.find_by_id(codec_id)
11
+ @protocol = Protocol[@uri.scheme].new(@codec, @uri)
12
+ @definitions = @protocol.fetch_definitions
13
+
14
+ inject!
15
+ end
16
+
17
+ private
18
+
19
+ def inject!
20
+ log.debug("Injecting remote defintions", url: @protocol.safe_uri.to_s)
21
+ protocol = @protocol
22
+
23
+ @definitions.each do |metadata|
24
+ klass = Class.new(Proxy) { configure_stargate_portal(protocol, metadata) }
25
+ *namespaces, klass_name = metadata.name.split('::')
26
+
27
+ namespace = namespaces.inject(Object) do |namespace, name|
28
+ namespace.const_defined?(name) ? namespace.const_get(name) : Module.new.tap { |m| m = namespace.const_set(name, m) }
29
+ end
30
+
31
+ namespace.const_set(klass_name, klass)
32
+ log.debug("Loaded remote class", metadata.serialize)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,72 @@
1
+ module Stargate
2
+ module Client
3
+ class Protocol
4
+ UnsupportedProtocolError = Class.new(::Stargate::Client::Error)
5
+
6
+ include Logging
7
+ include Stargate::Marshal
8
+
9
+ DEFAULT_TIMEOUT = 15
10
+
11
+ @@transports = {}
12
+
13
+ def self.transports
14
+ @@transports
15
+ end
16
+
17
+ def self.register(protocols, transport)
18
+ protocols.each { |protocol| transports[protocol] = transport }
19
+ end
20
+
21
+ def self.[](protocol)
22
+ transports[protocol.downcase] or raise UnsupportedProtocolError, "Unsupported protocol: #{protocol}"
23
+ end
24
+
25
+ attr_reader :codec
26
+ attr_reader :uri
27
+ attr_reader :safe_uri
28
+ attr_reader :options
29
+
30
+ def initialize(codec, uri)
31
+ @codec = codec
32
+ @uri = uri
33
+ @safe_uri = uri.dup.tap { |safe_uri| safe_uri.user, safe_uri.password = nil, nil }
34
+ @options = default_options.merge(parse_options_from_uri_fragment)
35
+ @unmarshaller = Unmarshaller.new
36
+ end
37
+
38
+ def fetch_definitions
39
+ raise NotImplementedError
40
+ end
41
+
42
+ def call(klass, method, *args)
43
+ raise NotImplementedError
44
+ end
45
+
46
+ protected
47
+
48
+ def default_options
49
+ { timeout: DEFAULT_TIMEOUT }
50
+ end
51
+
52
+ def unpack_payload(data)
53
+ @unmarshaller.unmarshal(data)
54
+ end
55
+
56
+ def unpack_definitions(definitions)
57
+ definitions.map { |metadata| ::Stargate::Metadata.from_hash(metadata) }
58
+ end
59
+
60
+ def parse_options_from_uri_fragment
61
+ uri.fragment.to_s.split(';').inject({}) do |options, entry|
62
+ k, v = entry.split('=')
63
+ options[k.to_sym] = v
64
+ options
65
+ end
66
+ end
67
+ end
68
+
69
+ require 'stargate/client/protocol/http'
70
+ require 'stargate/client/protocol/inproc'
71
+ end
72
+ end
@@ -0,0 +1,47 @@
1
+ require 'rest-client'
2
+
3
+ module Stargate
4
+ module Client
5
+ class Protocol::HTTP < Protocol
6
+ def fetch_definitions
7
+ log.debug("Loading remote definitions", url: safe_uri.to_s)
8
+
9
+ request('definitions', 'GET') do |response|
10
+ raise DirectivesFetchError, "Unable to fetch directives from #{safe_uri.to_s}" unless response.code == 200
11
+ data = codec.decode(response.body.to_s)
12
+ definitions = data['definitions'] or return []
13
+ unpack_definitions(definitions)
14
+ end
15
+ end
16
+
17
+ def call(klass, method, *args)
18
+ log.debug("Executing remote method", class: klass.remote_name, method: method)
19
+
20
+ request_options = {}
21
+ request_options[:payload] = codec.encode(args)
22
+ request_options[:timeout] = options[:timeout] if options[:timeout]
23
+ request_headers = { 'Content-Type': codec.content_type }
24
+
25
+ request("#{klass.remote_name}.#{method}", 'POST', request_options, request_headers) do |response|
26
+ data = codec.decode(response.body.to_s)
27
+ raise RemoteExecutionError.from_hash(data) unless response.code == 200
28
+ unpack_payload(data)
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def request(path, method, options = {}, headers = {}, &block)
35
+ options = {
36
+ url: File.join(uri.to_s, path),
37
+ method: method,
38
+ headers: { 'Accept': codec.content_type, 'User-Agent': 'Stargate' }.merge(headers)
39
+ }.merge(options)
40
+
41
+ RestClient::Request.execute(options, &block)
42
+ end
43
+
44
+ register %w[http https], self
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,28 @@
1
+ require 'rest-client'
2
+
3
+ module Stargate
4
+ module Client
5
+ class Protocol::Inproc < Protocol
6
+ RegistryNotFoundError = Class.new(::Stargate::Client::Error)
7
+
8
+ def initialize(*)
9
+ super
10
+ @registry_version = ::Stargate::INPROC[uri.to_s]
11
+ @caller = ::Stargate::Server::Caller.new(@registry_version)
12
+ raise RegistryNotFoundError, "Local registry not found: #{uri.to_s}" unless @registry_version
13
+ end
14
+
15
+ def fetch_definitions
16
+ unpack_definitions(@registry_version.definitions.map(&:serialize))
17
+ end
18
+
19
+ def call(klass, method, *args)
20
+ unpack_payload(@caller.call(klass.remote_name, method, *args))
21
+ rescue => err
22
+ raise RemoteExecutionError.new(err.class.to_s, err.message)
23
+ end
24
+
25
+ register %w[inproc], self
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,52 @@
1
+ module Stargate
2
+ module Client
3
+ class Proxy
4
+ def self.metaclass
5
+ class << self; self; end
6
+ end
7
+
8
+ def self.configure_stargate_portal(protocol, metadata)
9
+ metaclass.instance_eval do
10
+ metadata.class_methods.each do |method|
11
+ define_method(method) do |*args|
12
+ protocol.call(self, method, *args)
13
+ end
14
+ end
15
+
16
+ define_method(:remote_name) do
17
+ metadata.name
18
+ end
19
+ end
20
+
21
+ define_attributes(*metadata.attributes)
22
+ define_readers(*metadata.readers)
23
+ end
24
+
25
+ def self.define_attributes(*names)
26
+ names.each { |name| attr_accessor(name) }
27
+ end
28
+
29
+ def self.define_readers(*names)
30
+ names.each do |name|
31
+ name = name.to_s
32
+ simple_name = name.split('?').first
33
+ attr_reader(simple_name)
34
+ alias name simple_name if simple_name != name
35
+ end
36
+ end
37
+
38
+ def initialize(attributes = {})
39
+ attributes.each do |name,value|
40
+ instance_variable_set("@#{name}", value)
41
+ end
42
+ end
43
+
44
+ def to_h
45
+ self.class.stargate_directives.attributes.inject({}) do |hash,name|
46
+ hash[name] = instance_variable_get("@#{name}")
47
+ hash
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,22 @@
1
+ module Stargate
2
+ module Client
3
+ # Public: Raised when data has been fetched but execution failed.
4
+ class RemoteExecutionError < Error
5
+ def self.from_hash(hash)
6
+ hash.symbolize_keys!
7
+ new(hash[:error], hash[:message])
8
+ end
9
+
10
+ attr_reader :cause_type
11
+
12
+ def initialize(cause_type, message)
13
+ @cause_type = cause_type
14
+ super(message)
15
+ end
16
+
17
+ def inspect
18
+ "#{self.class.name} caused by #{cause_type}: #{message}"
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,34 @@
1
+ module Stargate
2
+ module Codec
3
+ # Public: Base codec error.
4
+ Error = Class.new(::Stargate::Error)
5
+
6
+ # Public: Raised when given codec is not registered.
7
+ UndefinedCodecError = Class.new(Error)
8
+
9
+ # Public: Raised when codec cannot decode given data string.
10
+ DecodeError = Class.new(Error)
11
+
12
+ @@codecs = {}
13
+
14
+ def self.codecs
15
+ @@codecs
16
+ end
17
+
18
+ def self.register(codec)
19
+ codecs[codec.content_type.to_s] = codec
20
+ end
21
+
22
+ def self.find_by_id(id)
23
+ codecs.values.find { |codec| codec.id == id.to_sym } or raise UndefinedCodecError, "No such codec: #{id}"
24
+ end
25
+
26
+ def self.[](content_type)
27
+ codecs[content_type.to_s] or raise UndefinedCodecError, "No codec for type: #{content_type}"
28
+ end
29
+
30
+ require 'stargate/codec/json'
31
+ require 'stargate/codec/bencode'
32
+ require 'stargate/codec/message_pack'
33
+ end
34
+ end
@@ -0,0 +1,28 @@
1
+ require 'bencode'
2
+
3
+ module Stargate
4
+ module Codec
5
+ # Internal: BEncode (BitTorrent encoding) powered codec.
6
+ class BEncode
7
+ def self.id
8
+ :bencode
9
+ end
10
+
11
+ def self.content_type
12
+ 'application/bencode'
13
+ end
14
+
15
+ def self.encode(obj)
16
+ obj.bencode
17
+ end
18
+
19
+ def self.decode(str)
20
+ ::BEncode::Parser.new(str).parse!
21
+ rescue ::BEncode::DecodeError => err
22
+ raise DecodeError, "Arguments cannot be decoded: bencode parser error"
23
+ end
24
+ end
25
+
26
+ register BEncode
27
+ end
28
+ end
@@ -0,0 +1,28 @@
1
+ require 'json'
2
+
3
+ module Stargate
4
+ module Codec
5
+ # Internal: Default codec based on JSON parser.
6
+ class JSON
7
+ def self.id
8
+ :json
9
+ end
10
+
11
+ def self.content_type
12
+ 'application/json'
13
+ end
14
+
15
+ def self.encode(obj)
16
+ obj.to_json
17
+ end
18
+
19
+ def self.decode(str)
20
+ ::JSON.parse(str)
21
+ rescue ::JSON::ParserError => err
22
+ raise DecodeError, "Arguments cannot be decoded: JSON parser error"
23
+ end
24
+ end
25
+
26
+ register JSON
27
+ end
28
+ end
@@ -0,0 +1,28 @@
1
+ require 'msgpack'
2
+
3
+ module Stargate
4
+ module Codec
5
+ # Internal: Serialization done with MessagePack format.
6
+ class MessagePack
7
+ def self.id
8
+ :msgpack
9
+ end
10
+
11
+ def self.content_type
12
+ 'application/msgpack'
13
+ end
14
+
15
+ def self.encode(obj)
16
+ ::MessagePack.pack(obj)
17
+ end
18
+
19
+ def self.decode(str)
20
+ ::MessagePack.unpack(str)
21
+ rescue ::MessagePack::MalformedFormatError => err
22
+ raise DecodeError, "Arguments cannot be decoded: MessagePack parser error"
23
+ end
24
+ end
25
+
26
+ register MessagePack
27
+ end
28
+ end
@@ -0,0 +1,4 @@
1
+ module Stargate
2
+ # Public: Base error.
3
+ Error = Class.new(StandardError)
4
+ end
@@ -0,0 +1,7 @@
1
+ module Stargate
2
+ module Marshal
3
+ require 'stargate/marshal/payload'
4
+ require 'stargate/marshal/marshaller'
5
+ require 'stargate/marshal/unmarshaller'
6
+ end
7
+ end
@@ -0,0 +1,58 @@
1
+ module Stargate
2
+ module Marshal
3
+ # Internal: Use it in order to pack data for exchange. Marshaller is created per instance of registry
4
+ # version. It packs basic values and objects of registered classes into Stargate::Marshal::Payload wraps.
5
+ class Marshaller
6
+ # Public: Constructor. Initializes marshaller for given registry version object.
7
+ def initialize(registry_version)
8
+ @registry_version = registry_version
9
+ end
10
+
11
+ # Public: Returns given object after marshalling.
12
+ def marshal(obj)
13
+ case obj
14
+ when String, Numeric, TrueClass, FalseClass, Float, BigDecimal, NilClass
15
+ Payload.new(Payload::SIMPLE, obj)
16
+ when Symbol
17
+ Payload.new(Payload::SYMBOL, obj.to_s)
18
+ when Hash
19
+ Payload.new(Payload::HASH, marshal_hash(obj))
20
+ when Array
21
+ Payload.new(Payload::LIST, marshal_list(obj))
22
+ else
23
+ Payload.new(*marshal_complex_object(obj))
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ # Internal: Returns array marshalled.
30
+ def marshal_list(obj)
31
+ obj.map { |x| marshal(x) }
32
+ end
33
+
34
+ # Internal: Returns hash marshalled.
35
+ def marshal_hash(obj)
36
+ obj.inject({}) do |res,(k,v)|
37
+ res[k.to_s] = marshal(v)
38
+ res
39
+ end
40
+ end
41
+
42
+ # Internal: Checks if type of given object is registered for exchange. No? Raises an error. Yes? Marshals
43
+ # all public attributes and computed readers.
44
+ #
45
+ # Returns marshalled object.
46
+ def marshal_complex_object(obj)
47
+ metadata = @registry_version.metadata_for(obj)
48
+
49
+ attributes = (metadata.attributes + metadata.readers).inject({}) do |result,key|
50
+ result[key.to_s] = marshal(obj.send(key.to_sym))
51
+ result
52
+ end
53
+
54
+ [ metadata.name, attributes ]
55
+ end
56
+ end
57
+ end
58
+ end