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,31 @@
1
+ require 'stargate/serialization'
2
+
3
+ module Stargate
4
+ module Marshal
5
+ # Internal: We need some packaging to move stuff from one machine to another. We have to know what types
6
+ # are coming in. We distinguish four base types:
7
+ #
8
+ # * Simple type (string, number, booleans),
9
+ # * Symbol,
10
+ # * List (Array),
11
+ # * Hash
12
+ #
13
+ # Later, we can pack any other type that has been registered for exchange.
14
+ class Payload < Struct.new(:type, :data)
15
+ include Serialization
16
+
17
+ LIST = :a
18
+ SIMPLE = :b
19
+ SYMBOL = :s
20
+ HASH = :h
21
+
22
+ def serialize
23
+ [ type, data ]
24
+ end
25
+
26
+ def inspect
27
+ serialize.inspect
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,46 @@
1
+ module Stargate
2
+ module Marshal
3
+ # Internal: On the client side there's unmarshalling needed then. Use this unmarshaller to unpack data.
4
+ # Unmarshaller is assigned to a module (namespace). It shall look up within that module in case of unpacking
5
+ # complex objects of specific type.
6
+ class Unmarshaller
7
+ # Public: Unpacks given object from payload wrap.
8
+ #
9
+ # There are two scenarios for unpacking:
10
+ #
11
+ # * Basic value - Just return value or adapt contents (like symbol, arrays or hashes).
12
+ # * An object of specific type - Check if our namespace defines such class. No? Erorr. Yes. Unmarshal into.
13
+ #
14
+ # Returns object unmarshalled.
15
+ def unmarshal(data)
16
+ payload = Payload.new(*data)
17
+
18
+ case payload.type.to_sym
19
+ when Payload::SIMPLE
20
+ payload.data
21
+ when Payload::SYMBOL
22
+ payload.data.to_sym
23
+ when Payload::LIST
24
+ unmarshal_list(payload.data)
25
+ when Payload::HASH
26
+ unmarshal_hash(payload.data)
27
+ else
28
+ klass = payload.type.constantize
29
+ klass.new(unmarshal_hash(payload.data))
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ # Internal: Unmarshals standalone hash.
36
+ def unmarshal_hash(hash)
37
+ hash.inject({}) { |result,(k,v)| result[k] = unmarshal(v); result }
38
+ end
39
+
40
+ # Internal: Unmarshals list.
41
+ def unmarshal_list(list)
42
+ list.map { |obj| unmarshal(obj) }
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,68 @@
1
+ module Stargate
2
+ class Metadata
3
+ include Serialization
4
+
5
+ def self.from_hash(hash)
6
+ hash.symbolize_keys!
7
+
8
+ new(hash[:klass], hash[:name]).tap do |metadata|
9
+ metadata.class_methods(*hash.fetch(:class_methods, []))
10
+ metadata.attributes(*hash.fetch(:attributes, []))
11
+ metadata.readers(*hash.fetch(:readers, []))
12
+ end
13
+ end
14
+
15
+ attr_reader :klass
16
+
17
+ attr_reader :name
18
+
19
+ def initialize(klass, name, &block)
20
+ @klass, @name = klass, name
21
+ @class_methods = []
22
+ @attributes = []
23
+ @readers = []
24
+
25
+ instance_eval(&block) if block_given?
26
+ end
27
+
28
+ def active_record?
29
+ @active_record
30
+ end
31
+
32
+ def class_methods(*names)
33
+ load_or_add(:class_methods, *names)
34
+ end
35
+
36
+ def attributes(*names)
37
+ load_or_add(:attributes, *names)
38
+ end
39
+
40
+ def readers(*names)
41
+ load_or_add(:readers, *names)
42
+ end
43
+
44
+ def serialize
45
+ {
46
+ name: name,
47
+ class_methods: class_methods,
48
+ attributes: attributes,
49
+ readers: readers
50
+ }
51
+ end
52
+
53
+ def inspect
54
+ "#<#{self.class.name} name=#{name.inspect} class_methods=#{class_methods.inspect} attributes=#{attributes.inspect} readers=#{readers.inspect}"
55
+ end
56
+
57
+ private
58
+
59
+ def klass_is_an_active_record?
60
+ defined?(::ActiveRecord) && klass && klass < ::ActiveRecord::Base
61
+ end
62
+
63
+ def load_or_add(method, *args)
64
+ value = instance_variable_get("@#{method}")
65
+ args.empty? ? value : value.concat(args.flatten.map(&:to_sym))
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,21 @@
1
+ module Stargate
2
+ # Internal: This tiny mixin provides serialization directives for includee class. The target class must
3
+ # implement #serialize method.
4
+ module Serialization
5
+ def serialize
6
+ raise NotImplementedError, "Not implemented: #{self.class.name}#serialize"
7
+ end
8
+
9
+ def to_json(*args)
10
+ serialize.to_json(*args)
11
+ end
12
+
13
+ def to_msgpack(*args)
14
+ serialize.to_msgpack(*args)
15
+ end
16
+
17
+ def bencode
18
+ serialize.bencode
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,10 @@
1
+ require 'stargate'
2
+
3
+ module Stargate
4
+ module Server
5
+ require 'stargate/server/errors'
6
+ require 'stargate/server/registry_version'
7
+ require 'stargate/server/registry'
8
+ require 'stargate/server/caller'
9
+ end
10
+ end
@@ -0,0 +1,21 @@
1
+ module Stargate
2
+ module Server
3
+ class Caller
4
+ include Logging
5
+ include Stargate::Marshal
6
+
7
+ def initialize(registry_version)
8
+ @marshaller = Marshaller.new(registry_version)
9
+ @registry_version = registry_version
10
+ end
11
+
12
+ def call(klass_name, method, *args)
13
+ method, metadata = method.to_sym, @registry_version[klass_name]
14
+ metadata.class_methods.include?(method) or raise MethodNotExposedError, "Method not exposed: #{klass_name}.#{method}"
15
+ result = metadata.klass.send(method, *args)
16
+ log.debug("Method call performed", class: klass_name, method: method, result:result.inspect)
17
+ @marshaller.marshal(result)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,15 @@
1
+ require 'stargate/server'
2
+
3
+ module Stargate
4
+ module Server
5
+ module Engine
6
+ class Inproc
7
+ def self.register(name, registry)
8
+ registry.each do |version_number, actual_registry|
9
+ ::Stargate::INPROC["inproc://#{name}/v#{version_number}"] = actual_registry
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,97 @@
1
+ require 'stargate/server'
2
+
3
+ module Stargate
4
+ module Server
5
+ module Engine
6
+ class Sinatra
7
+ include Logging
8
+
9
+ def initialize(registry)
10
+ require 'sinatra/base'
11
+
12
+ logger = log
13
+
14
+ @app = ::Sinatra.new do
15
+ helpers Helpers
16
+
17
+ registry.each do |version_number, actual_registry|
18
+ caller = Caller.new(actual_registry)
19
+
20
+ get "/v#{version_number}/definitions" do
21
+ log.debug("Serving registry definitions")
22
+ content_type(response_codec.content_type)
23
+ response_codec.encode(actual_registry)
24
+ end
25
+
26
+ post "/v#{version_number}/:klass_name.:method" do |klass_name, method|
27
+ args = parse_args
28
+ log.debug("Executing call", class: klass_name, method: method, args: args)
29
+ result = caller.call(klass_name, method, *args)
30
+ content_type(response_codec.content_type)
31
+ response_codec.encode(result)
32
+ end
33
+ end
34
+
35
+ error Stargate::Server::NotFoundError do
36
+ error_response(404)
37
+ end
38
+
39
+ error Stargate::Codec::Error do
40
+ error_response(400)
41
+ end
42
+
43
+ error NotImplementedError do
44
+ error_response(501)
45
+ end
46
+
47
+ error 500 do
48
+ error_response(500)
49
+ end
50
+ end
51
+
52
+ @app.set(:default_mime_type, ::Stargate::Codec::JSON.content_type)
53
+ @app.set(:show_exceptions, false)
54
+ @app.set(:logger, log)
55
+ end
56
+
57
+ def call(env)
58
+ @app.call(env)
59
+ end
60
+
61
+ module Helpers
62
+ def log
63
+ settings.logger
64
+ end
65
+
66
+ def parse_args
67
+ request.body.rewind
68
+
69
+ args = body_codec.decode(request.body.read)
70
+ args = [] if args.nil?
71
+ args = [ args ] if args.kind_of?(Hash)
72
+ args
73
+ end
74
+
75
+ def error_response(code)
76
+ status(code)
77
+ err = env['sinatra.error']
78
+ log.error(err)
79
+ response_codec.encode({error: err.class.name, message: err.message})
80
+ end
81
+
82
+ def body_codec
83
+ Codec[request.media_type || settings.default_mime_type]
84
+ end
85
+
86
+ def response_codec
87
+ Codec[accept_type || settings.default_mime_type]
88
+ end
89
+
90
+ def accept_type
91
+ accept = env['HTTP_ACCEPT'] and accept.split(/\s*[;,]\s*/, 2).first.downcase
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,25 @@
1
+ module Stargate
2
+ module Server
3
+ # Public: Base server error.
4
+ Error = Class.new(::Stargate::Error)
5
+
6
+ # Public: Base class for not-found type errors.
7
+ NotFoundError = Class.new(Error)
8
+
9
+ # Public: Raised when trying to get non-existent registry version.
10
+ RegistryVersionUndefinedError = Class.new(Error)
11
+
12
+ # Public: Raised when requested class is not exposed/registered.
13
+ ClassNotExposedError = Class.new(NotFoundError)
14
+
15
+ # Public: Raised when called method is not exposed.
16
+ MethodNotExposedError = Class.new(NotFoundError)
17
+
18
+ # Public: Raised when trying to register the same class twice within single registry.
19
+ ClassAlreadyRegisteredError = Class.new(Error)
20
+
21
+ # Public: Raised when trying to return (and pack into payload) data of unregistered or unsupported type.
22
+ # For example when exposed class method returns value of type different than itself, hash or basic.
23
+ UnregisteredClassExposureError = Class.new(Error)
24
+ end
25
+ end
@@ -0,0 +1,26 @@
1
+ module Stargate
2
+ module Server
3
+ class Registry
4
+ include Enumerable
5
+
6
+ attr_reader :versions
7
+
8
+ def initialize(&block)
9
+ @versions = {}
10
+ instance_eval(&block) if block_given?
11
+ end
12
+
13
+ def version(number, &block)
14
+ @versions[number] ||= RegistryVersion.new(number, &block)
15
+ end
16
+
17
+ def [](version)
18
+ versions[version] or raise RegistryVersionUndefinedError, "No such registry version: #{version}"
19
+ end
20
+
21
+ def each(&block)
22
+ versions.each(&block)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,59 @@
1
+ require 'forwardable'
2
+
3
+ module Stargate
4
+ module Server
5
+ class RegistryVersion
6
+ include Logging
7
+ include Enumerable
8
+ include Serialization
9
+
10
+ attr_reader :version
11
+
12
+ attr_reader :definitions
13
+
14
+ def initialize(version = 1, &block)
15
+ @version = version
16
+ @definitions = []
17
+
18
+ instance_eval(&block) if block_given?
19
+ end
20
+
21
+ def serve(klass, params = {}, &block)
22
+ raise ClassAlreadyRegisteredError, "Class already registered: #{klass.name}" if registered?(klass)
23
+
24
+ params.symbolize_keys!
25
+ klass_name = params[:as] || klass.name
26
+ log.debug("Registering class", local_class_name: klass.name, serve_as: klass_name)
27
+ @definitions << Metadata.new(klass, klass_name, &block)
28
+
29
+ self
30
+ end
31
+
32
+ def registered?(klass)
33
+ @definitions.find { |metadata| metadata.klass == klass }
34
+ end
35
+
36
+ def metadata_for(obj)
37
+ metadata = @definitions.find { |metadata| metadata.klass == obj.class }
38
+ metadata or raise ClassNotExposedError, "Class not registered: #{obj.class.name}"
39
+ end
40
+
41
+ def [](klass_name)
42
+ metadata = @definitions.find { |metadata| metadata.name == klass_name }
43
+ metadata or raise ClassNotExposedError, "Class not registered under name: #{klass_name}"
44
+ end
45
+
46
+ def each(&block)
47
+ @definitions.each(&block)
48
+ end
49
+
50
+ def size
51
+ @definitions.size
52
+ end
53
+
54
+ def serialize
55
+ { version: @version, definitions: @definitions.map(&:serialize) }
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,3 @@
1
+ module Stargate
2
+ VERSION = '0.1.1'
3
+ end
@@ -0,0 +1,41 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'stargate/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "stargate"
8
+ spec.version = Stargate::VERSION
9
+ spec.authors = ["jobandtalent", "Kris Kovalik"]
10
+ spec.email = ["kris.kovalik@jobandtalent.com", "hi@kkvlk.me"]
11
+
12
+ spec.summary = %q{A portal to remote services.}
13
+ spec.description = %q{Stargate opens a portal to call remote methods via simple and reliable RPC protocols.}
14
+ spec.homepage = "https://github.com/jobandtalent/stargate"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = "exe"
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.10"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ spec.add_development_dependency "minitest", "~> 5.8"
24
+ spec.add_development_dependency "minitest-reporters", "~> 1.1"
25
+ spec.add_development_dependency "rack-test", "~> 0.6"
26
+ spec.add_development_dependency "mocha", "~> 1.1"
27
+ spec.add_development_dependency "webmock", "~> 1.22"
28
+ spec.add_development_dependency "puma", "~> 2.11"
29
+ spec.add_development_dependency "vcr", "~> 3.0"
30
+ spec.add_development_dependency "rake-bump", "~> 0.4", ">= 0.4.6"
31
+
32
+ spec.add_dependency "json", "~> 1.8"
33
+ spec.add_dependency "msgpack", "~> 0.7"
34
+ spec.add_dependency "bencode", "~> 0.8"
35
+ spec.add_dependency "activemodel", ">= 3.0"
36
+ spec.add_dependency "activesupport", ">= 3.0"
37
+ spec.add_dependency "sinatra", "~> 1.4"
38
+ spec.add_dependency "rest-client", "~> 1.8"
39
+ spec.add_dependency "log4r", "~> 1.1"
40
+ spec.add_dependency "perfume", "~> 0.3", ">= 0.3.1"
41
+ end