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.
- checksums.yaml +7 -0
- data/.editorconfig +12 -0
- data/.gitignore +9 -0
- data/CHANGELOG.md +13 -0
- data/Dockerfile +11 -0
- data/Gemfile +4 -0
- data/README.md +240 -0
- data/Rakefile +17 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/docker-compose.yml +4 -0
- data/examples/client/client.rb +27 -0
- data/examples/server/config.ru +45 -0
- data/examples/server/start +7 -0
- data/lib/stargate.rb +28 -0
- data/lib/stargate/client.rb +12 -0
- data/lib/stargate/client/core_ext/kernel.rb +8 -0
- data/lib/stargate/client/errors.rb +12 -0
- data/lib/stargate/client/injector.rb +37 -0
- data/lib/stargate/client/protocol.rb +72 -0
- data/lib/stargate/client/protocol/http.rb +47 -0
- data/lib/stargate/client/protocol/inproc.rb +28 -0
- data/lib/stargate/client/proxy.rb +52 -0
- data/lib/stargate/client/remote_execution_error.rb +22 -0
- data/lib/stargate/codec.rb +34 -0
- data/lib/stargate/codec/bencode.rb +28 -0
- data/lib/stargate/codec/json.rb +28 -0
- data/lib/stargate/codec/message_pack.rb +28 -0
- data/lib/stargate/errors.rb +4 -0
- data/lib/stargate/marshal.rb +7 -0
- data/lib/stargate/marshal/marshaller.rb +58 -0
- data/lib/stargate/marshal/payload.rb +31 -0
- data/lib/stargate/marshal/unmarshaller.rb +46 -0
- data/lib/stargate/metadata.rb +68 -0
- data/lib/stargate/serialization.rb +21 -0
- data/lib/stargate/server.rb +10 -0
- data/lib/stargate/server/caller.rb +21 -0
- data/lib/stargate/server/engine/inproc.rb +15 -0
- data/lib/stargate/server/engine/sinatra.rb +97 -0
- data/lib/stargate/server/errors.rb +25 -0
- data/lib/stargate/server/registry.rb +26 -0
- data/lib/stargate/server/registry_version.rb +59 -0
- data/lib/stargate/version.rb +3 -0
- data/stargate.gemspec +41 -0
- metadata +367 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 29a29b4893f36ea1c2b1da2977aedfa953b5e681
|
4
|
+
data.tar.gz: b6474307574fee4378d27268273c9360855a6e71
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7f87bd0790583a14ca6f4c288c582a5e3ba718fd248e7372912c378378cdcfc59fedb07ec70f2767ac235aa3999d0097b4abb0ecc1640784334d6348bc25dfc3
|
7
|
+
data.tar.gz: c6862c8cbbec7e54109d34a556ab0404b1f2e82c64b45136a005c13afe242cadd2d8dfdcb7a9e848a6e299277b8e13d035b0628dcb7cd2c1e210bb7beda985f1
|
data/.editorconfig
ADDED
data/.gitignore
ADDED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# Change Log
|
2
|
+
All notable changes to this project will be documented in this file.
|
3
|
+
This project adheres to [Semantic Versioning](http://semver.org/).
|
4
|
+
|
5
|
+
## [Unreleased]
|
6
|
+
|
7
|
+
...
|
8
|
+
|
9
|
+
## [0.1.1]
|
10
|
+
|
11
|
+
### Added
|
12
|
+
|
13
|
+
- Proof of concept of server and client communication with local injections.
|
data/Dockerfile
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,240 @@
|
|
1
|
+
# Stargate
|
2
|
+
|
3
|
+
[](https://codeship.com/projects/XXX)
|
4
|
+
|
5
|
+
Stargate is a portal between ruby (and in the future not any) apps. It facilitates creating internal micro-services
|
6
|
+
and allows to scale your logic pretty much infinitely.
|
7
|
+
|
8
|
+
## Motivation & Foreword
|
9
|
+
|
10
|
+
Writing micro-services comes with huge overhead and problems on all ends:
|
11
|
+
|
12
|
+
* Services often speak pseudo REST interfaces, too complex and too troublesome.
|
13
|
+
* Consumers need client libraries that are either generated boilerplate code or crated libraries that break more often then they actually work.
|
14
|
+
* Testing on both ends is complicated and not really accurate. Often you still have to test your client libs too. Sometimes even generators that create your client libraries must be tested.
|
15
|
+
* Client libraries add layer of abstraction that's completely unnecessary and introduces confusion. Different services speak through different REST "practices". The HTTP verbs and headers holy war keeps going, no peace.
|
16
|
+
|
17
|
+
IMHO simple RPC services are hero solving this problems. They come with different ones, though, JSON-RPC being the best example.
|
18
|
+
|
19
|
+
Here are key objectives I took for this project:
|
20
|
+
|
21
|
+
* Simple protocol and exchange format for the start: HTTP + JSON. Minimum effort.
|
22
|
+
* Replaceable protocols and exchange formats in the long run.
|
23
|
+
* Server versioning out of the box.
|
24
|
+
* No REST practices.
|
25
|
+
* Client code meta-generated from server definitions. One client for all.
|
26
|
+
* Client forced to respect the versioning system.
|
27
|
+
* Easy for testing.
|
28
|
+
|
29
|
+
Finally, the main objective to call remote methods and obtain remote data just as you would call the method locally. Real key for me was to create a system where it doesn't matter if method or
|
30
|
+
class are executed locally or remotely. Fire, get results, forget. No thinking of what's under the hood. The essence of distributed computing without affecting principles of our programming language.
|
31
|
+
|
32
|
+
## Installation
|
33
|
+
|
34
|
+
Add this line to your application's Gemfile:
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
gem 'stargate', '0.1.1'
|
38
|
+
```
|
39
|
+
|
40
|
+
And then execute:
|
41
|
+
|
42
|
+
$ bundle
|
43
|
+
|
44
|
+
Or install it yourself as:
|
45
|
+
|
46
|
+
$ gem install stargate --version 0.1.1
|
47
|
+
|
48
|
+
## Usage
|
49
|
+
|
50
|
+
1. Define your service:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
module NightsWatch
|
54
|
+
class NewBrother
|
55
|
+
def self.oath
|
56
|
+
"I pledge my life and honor to the Night's Watch, for this night and all the nights to come."
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.call(name)
|
60
|
+
Brother.new(name)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class Brother
|
65
|
+
attr_accessor :name
|
66
|
+
|
67
|
+
def initialize(name)
|
68
|
+
@name = name
|
69
|
+
end
|
70
|
+
|
71
|
+
def knowledge
|
72
|
+
if name == "Jon Snow"
|
73
|
+
"nothing"
|
74
|
+
else
|
75
|
+
"something"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
```
|
81
|
+
|
82
|
+
Plain ruby object. Class methods are your entry points.
|
83
|
+
|
84
|
+
**NOTE**: Class method should return either basic type (`String`, `Numeric`, `Float`, `TrueClass`, `FalseClass`, `NilClass`), `Hash` of basic types, object of our class or `Array` of basics or such objects. That's first restriction - if you ask me I already write compliant code like this for years.
|
85
|
+
|
86
|
+
2. Put it on the wire! Add `config.ru` like this one:
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
require 'stargate/server'
|
90
|
+
require 'stargate/server/transport/sinatra'
|
91
|
+
|
92
|
+
class NightsWatch
|
93
|
+
# *snip*
|
94
|
+
end
|
95
|
+
|
96
|
+
registry = Stargate::Server::Registry.new do
|
97
|
+
version 1 do
|
98
|
+
serve NightsWatch::NewBrother do
|
99
|
+
class_methods :call, :oath
|
100
|
+
end
|
101
|
+
|
102
|
+
serve NightsWatch::Brother do
|
103
|
+
class_methods :create, :update
|
104
|
+
attributes :name
|
105
|
+
readers :knowledge
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
run Stargate::Server::Transport::Sinatra.new(registry)
|
111
|
+
```
|
112
|
+
|
113
|
+
Entry points are our class methods that shall be exposed over the network. They are remotely callable. The `accessors` are simple all assessor attributes that should be exposed in case of returning object of that class. Finally `readers` are read only attributes. Final note, the `as` option of `serve` block defines exposed name of the class.
|
114
|
+
|
115
|
+
**NOTE**: Here comes second restriction. You can expose only class methods to be callable. Instances are simply treated as data structures. If you expose message, it's result will be cached at the moment of serialization and only result passed over the wire. How do I call instance methods then? You don't.
|
116
|
+
|
117
|
+
Q: So how can I call `ActiveRecord::Base#save` on model object?
|
118
|
+
A: Forget model objects, what you get is eventually a data structure. The structure is remotely stateless.
|
119
|
+
Q: What does it mean "remotely stateless"?
|
120
|
+
A: It means that you can make local changes to the object, but persistence of those changes must go through a service, either local or remote. Persistence methods like `#save` should be called only at the end of the food chain, in the
|
121
|
+
final service.
|
122
|
+
Q: Show me an example?
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
class Book < ActiveRecord::Base
|
126
|
+
# *snip*
|
127
|
+
end
|
128
|
+
|
129
|
+
class CreateBook
|
130
|
+
def self.call(book)
|
131
|
+
unless book.valid?
|
132
|
+
raise "Uups, the book info is not complete!"
|
133
|
+
end
|
134
|
+
|
135
|
+
book.save
|
136
|
+
end
|
137
|
+
end
|
138
|
+
```
|
139
|
+
|
140
|
+
Q: What? It looks like Rails controller. Why I can't just call save directly?
|
141
|
+
A: Don't think in the space of operations. Think in processes and define them clearly. Yes it's extra code initially.
|
142
|
+
No it's not a boilerplate. This way you can easily test, mock and track your business processes.
|
143
|
+
|
144
|
+
3. Set up your client code:
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
require 'stargate/client'
|
148
|
+
|
149
|
+
module NightsWatch
|
150
|
+
end
|
151
|
+
|
152
|
+
require_remote 'http+json://localhost:9292/v1'
|
153
|
+
|
154
|
+
class NightsWatch::Brother
|
155
|
+
def knowledge_description
|
156
|
+
"#{name} knows #{knowledge}"
|
157
|
+
end
|
158
|
+
|
159
|
+
def hello
|
160
|
+
"Hello, I'm #{name}!"
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
jon = NightsWatch::NewBrother.call('Jon Snow')
|
165
|
+
puts jon.hello
|
166
|
+
puts jon.knowledge_description
|
167
|
+
puts jon.class.name
|
168
|
+
puts '----'
|
169
|
+
|
170
|
+
sam = NightsWatch::NewBrother.call('Samwell Tarly')
|
171
|
+
puts sam.knowledge_description
|
172
|
+
puts '----'
|
173
|
+
|
174
|
+
puts NightsWatch::NewBrother.oath
|
175
|
+
```
|
176
|
+
|
177
|
+
As you can see, single client handles everything. Also the way you connect to remote service is pretty much like requiring any other file or library. Client automatically fetches definitions of exposed classes from remote location (this is done once and can be easily cached for the eventuality of service downtime) and injects all registered classes into global namespace. Now injected classes behave pretty much like they'd be defined locally, with the only exception that under the hood they call remote API to execute stuff and obtain results. As you can also see in `NightsWatch::Brother` proxy class, we can easily extend these classes with plain ruby code.
|
178
|
+
|
179
|
+
**NOTE**: One probably unsupported or at least troublesome thing at the moment is inheritance from the proxy class.
|
180
|
+
|
181
|
+
That's it! The services are stateless and can be trivially load balanced or put behind the proxy. The client is one and only and doesn't require you to write more than few lines of code. No custom stuff to learn or define before making use of remote services.
|
182
|
+
|
183
|
+
Check `examples` directory to try these examples out.
|
184
|
+
|
185
|
+
### Under the hood
|
186
|
+
|
187
|
+
The mechanism is uber simple. Expose two endpoints (with whatever protocol you prefer) on the server side without affecting original code. In case of HTTP transport that would be:
|
188
|
+
|
189
|
+
```
|
190
|
+
GET /v{version}/definitions.json
|
191
|
+
POST /v{version}/{klass_name}.{method}
|
192
|
+
```
|
193
|
+
|
194
|
+
First returns JSON (or however encoded) definitions of exposed classes. Later is an entry point for calling class methods of exposed classes. It takes encoded arguments as POST request body.
|
195
|
+
|
196
|
+
That's it for the server!
|
197
|
+
|
198
|
+
Now the client at `require_remote` time fetches definitions and injects configured classes inheriting from `Stargate::Client::Proxy`. All exposed methods are defined dynamically and they pass through to remote calls.
|
199
|
+
|
200
|
+
## Development
|
201
|
+
|
202
|
+
You have two options to work with this project. The [docker flow](#setup-with-docker) is suggested since solves problems of compatibility of tools.
|
203
|
+
|
204
|
+
### Manual Setup
|
205
|
+
|
206
|
+
First off, make sure you have **Ruby 2.2+** and latest version of **Bundler** on your machine. After checking out the repo, you can install dependencies and prepare the project with:
|
207
|
+
|
208
|
+
$ bin/setup
|
209
|
+
|
210
|
+
Now you can run tests:
|
211
|
+
|
212
|
+
$ bundle exec rake spec
|
213
|
+
|
214
|
+
You can also connect to interactive prompt that will allow you to experiment. To do this, run:
|
215
|
+
|
216
|
+
$ bundle exec bin/console
|
217
|
+
|
218
|
+
To install this gem onto your local machine, run:
|
219
|
+
|
220
|
+
$ bundle exec rake install
|
221
|
+
|
222
|
+
### Setup with Docker
|
223
|
+
|
224
|
+
If you're lazy and don't wanna get into how the setup works, here's something for you. This project comes fully [dockerized](http://docker.io/). Install docker toolchain and then go for:
|
225
|
+
|
226
|
+
$ docker-compose build
|
227
|
+
|
228
|
+
All done, you can do testing and fiddling around:
|
229
|
+
|
230
|
+
$ docker-compose run facebook_integration bash
|
231
|
+
root@xyyyyxx:/usr/local/src/rake-bump# bundle exec rake spec
|
232
|
+
root@xyyyyxx:/usr/local/src/rake-bump# bundle exec bin/console
|
233
|
+
|
234
|
+
### Releasing
|
235
|
+
|
236
|
+
TODO: Add link to rake-bump release instructions...
|
237
|
+
|
238
|
+
## Contributing
|
239
|
+
|
240
|
+
Bug reports and pull requests are welcome [here](https://github.com/jobandtalent/stargate/issues).
|
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rake/bump/tasks"
|
3
|
+
require "rake/testtask"
|
4
|
+
require "stargate/version"
|
5
|
+
|
6
|
+
Rake::Bump::Tasks.new do |t|
|
7
|
+
t.gem_name = 'stargate'
|
8
|
+
t.gem_current_version = Stargate::VERSION
|
9
|
+
end
|
10
|
+
|
11
|
+
Rake::TestTask.new(:test) do |t|
|
12
|
+
t.libs << "test"
|
13
|
+
t.libs << "lib"
|
14
|
+
t.test_files = FileList['test/**/*_test.rb']
|
15
|
+
end
|
16
|
+
|
17
|
+
task :default => :test
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "stargate"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
data/docker-compose.yml
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'stargate/client'
|
2
|
+
|
3
|
+
ENV['NIGHTS_WATCH_URL'] ||= 'http+json://localhost:9292/v1'
|
4
|
+
|
5
|
+
require_remote ENV['NIGHTS_WATCH_URL']
|
6
|
+
|
7
|
+
class NightsWatch::Brother
|
8
|
+
def knowledge_description
|
9
|
+
"#{name} knows #{knowledge}"
|
10
|
+
end
|
11
|
+
|
12
|
+
def hello
|
13
|
+
"Hello, I'm #{name}!"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
jon = NightsWatch::NewBrother.call('Jon Snow')
|
18
|
+
puts jon.hello
|
19
|
+
puts jon.knowledge_description
|
20
|
+
puts jon.class.name
|
21
|
+
puts '----'
|
22
|
+
|
23
|
+
sam = NightsWatch::NewBrother.call('Samwell Tarly')
|
24
|
+
puts sam.knowledge_description
|
25
|
+
puts '----'
|
26
|
+
|
27
|
+
puts NightsWatch::NewBrother.oath
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'stargate/server/engine/sinatra'
|
2
|
+
|
3
|
+
module NightsWatch
|
4
|
+
class NewBrother
|
5
|
+
def self.oath
|
6
|
+
"I pledge my life and honor to the Night's Watch, for this night and all the nights to come."
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.call(name)
|
10
|
+
Brother.new(name)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class Brother
|
15
|
+
attr_accessor :name
|
16
|
+
|
17
|
+
def initialize(name)
|
18
|
+
@name = name
|
19
|
+
end
|
20
|
+
|
21
|
+
def knowledge
|
22
|
+
if name == "Jon Snow"
|
23
|
+
"nothing"
|
24
|
+
else
|
25
|
+
"something"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
registry = Stargate::Server::Registry.new do
|
32
|
+
version 1 do
|
33
|
+
serve NightsWatch::NewBrother do
|
34
|
+
class_methods :call, :oath
|
35
|
+
end
|
36
|
+
|
37
|
+
serve NightsWatch::Brother do
|
38
|
+
class_methods :create, :update
|
39
|
+
attributes :name
|
40
|
+
readers :knowledge
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
run Stargate::Server::Engine::Sinatra.new(registry)
|
data/lib/stargate.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'gallus'
|
3
|
+
require 'active_support/core_ext/hash'
|
4
|
+
require 'active_support/core_ext/module'
|
5
|
+
require 'active_support/core_ext/string/inflections'
|
6
|
+
|
7
|
+
module Stargate
|
8
|
+
include Gallus::PackageLogging
|
9
|
+
|
10
|
+
# Logging proxy, just in case someday you'd like to switch to different logging lib.
|
11
|
+
Logging = Gallus::Logging
|
12
|
+
|
13
|
+
# Public: Base error.
|
14
|
+
Error = Class.new(StandardError)
|
15
|
+
|
16
|
+
# In-process registry.
|
17
|
+
INPROC = {}
|
18
|
+
|
19
|
+
# Configure logging defaults.
|
20
|
+
ENV['STARGATE_LOG_LEVEL'] = 'DEBUG' if ENV['DEBUG'].to_i > 0
|
21
|
+
log.level = ENV.fetch('STARGATE_LOG_LEVEL', 'INFO')
|
22
|
+
|
23
|
+
require 'stargate/version'
|
24
|
+
require 'stargate/serialization'
|
25
|
+
require 'stargate/codec'
|
26
|
+
require 'stargate/marshal'
|
27
|
+
require 'stargate/metadata'
|
28
|
+
end
|