unrestful 0.1.0
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/README.md +137 -0
- data/Rakefile +32 -0
- data/app/assets/config/unrestful_manifest.js +2 -0
- data/app/assets/javascripts/unrestful/application.js +15 -0
- data/app/assets/stylesheets/unrestful/application.css +15 -0
- data/app/controllers/unrestful/application_controller.rb +5 -0
- data/app/controllers/unrestful/endpoints_controller.rb +104 -0
- data/app/controllers/unrestful/jobs_controller.rb +64 -0
- data/app/helpers/unrestful/application_helper.rb +4 -0
- data/app/jobs/unrestful/application_job.rb +4 -0
- data/app/mailers/unrestful/application_mailer.rb +6 -0
- data/app/models/unrestful/application_record.rb +5 -0
- data/app/views/layouts/unrestful/application.html.erb +16 -0
- data/config/routes.rb +5 -0
- data/lib/tasks/unrestful_tasks.rake +4 -0
- data/lib/unrestful/async_job.rb +111 -0
- data/lib/unrestful/engine.rb +5 -0
- data/lib/unrestful/errors.rb +9 -0
- data/lib/unrestful/fail_response.rb +26 -0
- data/lib/unrestful/json_web_token.rb +36 -0
- data/lib/unrestful/jwt_secured.rb +34 -0
- data/lib/unrestful/response.rb +11 -0
- data/lib/unrestful/rpc_controller.rb +64 -0
- data/lib/unrestful/success_response.rb +20 -0
- data/lib/unrestful/utils.rb +19 -0
- data/lib/unrestful/version.rb +3 -0
- data/lib/unrestful.rb +25 -0
- metadata +142 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3a72f2af5b6292b01574673437b1e1102ec301d6dadc818b56a3e5e2f98beeaf
|
4
|
+
data.tar.gz: a8bb7550136979bb330de73d7f4b303121cf5e2685504b2ce9848f847c118527
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 34dec6df9aae1c4fb211cfee6d8dd1069aad5f9938b1d3058e1095d5dd5e6026c1d4987402987041d93935d7fcdd1069100af2aa4e994f92253152e4e20b8c14
|
7
|
+
data.tar.gz: 36b3738b1dc28431c44941665cb5c9c1e8c81df6540565a22bb2a82c3311507ad43616679e58c9df3acb98f75ee3d91c25b5ae0acff139db1d59d0fbdf3fb3f9
|
data/README.md
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
<img src="http://cdn2-cloud66-com.s3.amazonaws.com/images/oss-sponsorship.png" width=150/>
|
2
|
+
|
3
|
+
# Unrestful
|
4
|
+
|
5
|
+
REST is not fit for all use cases. Most of RPC frameworks are too heavy and complicated and require a lot of Ops involvement.
|
6
|
+
|
7
|
+
Unrestful is a lightweight simple RPC framework for Rails that can sit next to your existing application. It supports the following:
|
8
|
+
|
9
|
+
- Simple procedure calls over HTTP
|
10
|
+
- Streaming
|
11
|
+
- Async jobs and async job log streaming and status tracking
|
12
|
+
|
13
|
+
## Dependencies
|
14
|
+
|
15
|
+
Unrestful requires Rails 5.2 (can work with earlier versions) and Redis.
|
16
|
+
In development environments, Unrestful requires a multi-threaded web server like Puma. (it won't work with Webrick).
|
17
|
+
|
18
|
+
## Usage
|
19
|
+
|
20
|
+
Mount Unrestful on your Rails app:
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
Rails.application.routes.draw do
|
24
|
+
# ...
|
25
|
+
mount Unrestful::Engine => "/mount/path"
|
26
|
+
# ...
|
27
|
+
end
|
28
|
+
```
|
29
|
+
|
30
|
+
This will add the following paths to your application:
|
31
|
+
|
32
|
+
```
|
33
|
+
/mount/path/rpc/:service/:method
|
34
|
+
/mount/path/jobs/status/:job_id
|
35
|
+
/mount/path/jobs/live/:job_id
|
36
|
+
```
|
37
|
+
|
38
|
+
You can start your Rails app as normal.
|
39
|
+
|
40
|
+
## Services and Method
|
41
|
+
|
42
|
+
Unrestful looks for files under `app/models/rpc` to find the RPC method. Any class should be derived from `::Unrestful::RpcController` to be considered. Here is an example:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
require 'unrestful'
|
46
|
+
|
47
|
+
module Rpc
|
48
|
+
class Account < ::Unrestful::RpcController
|
49
|
+
include Unrestful::JwtSecured
|
50
|
+
scopes({
|
51
|
+
'switch_owner' => ['write:account'],
|
52
|
+
'migrate' => ['read:account'],
|
53
|
+
'long_one' => ['read:account']
|
54
|
+
})
|
55
|
+
live(['migrate'])
|
56
|
+
async(['long_one'])
|
57
|
+
|
58
|
+
before_method :authenticate_request!
|
59
|
+
#after_method :do_something
|
60
|
+
|
61
|
+
def switch_owner(from:, to:)
|
62
|
+
foo = ::AcmeFoo.new
|
63
|
+
foo.from = from
|
64
|
+
foo.to = to
|
65
|
+
|
66
|
+
return foo
|
67
|
+
end
|
68
|
+
|
69
|
+
def migrate(repeat:)
|
70
|
+
repeat.to_i.times {
|
71
|
+
write "hello\n"
|
72
|
+
sleep 1
|
73
|
+
}
|
74
|
+
|
75
|
+
return nil
|
76
|
+
end
|
77
|
+
|
78
|
+
def long_one
|
79
|
+
return { not_done_yet: true }
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
84
|
+
```
|
85
|
+
|
86
|
+
NOTE: All parameters on all RPC methods should be named.
|
87
|
+
|
88
|
+
`POST` or `GET` parameters will be used to call the RPC method using their names. For example, `{ "from": "you", "to": "me" }` as a HTTP POST payload on `rpc/account/switch_owner` will be used to call the method with the corresponding parameters.
|
89
|
+
|
90
|
+
NOTE: Both `rpc/accounts` and `rpc/account` are accepted.
|
91
|
+
|
92
|
+
The three methods in the example above, support the 3 types of RPC calls Unrestful supports:
|
93
|
+
|
94
|
+
### Synchronous calls
|
95
|
+
|
96
|
+
Sync calls are called and return a value within the same HTTP session. `switch_owner` is an example of that. Any returned value will be wrapped in `Unrestful::Response` and sent to the client (this could be a `SuccessResponse` or `FailResponse` if there is an exception).
|
97
|
+
|
98
|
+
### Live calls
|
99
|
+
|
100
|
+
Live calls are calls that hold the client and send live logs of progress down the wire. They might be cancelled mid-flow if the client disconnects. Live methods should be named in the `live` class method and can use the `write` method to send live information back to the client.
|
101
|
+
|
102
|
+
### Asynchronous calls
|
103
|
+
|
104
|
+
Async calls are like sync calls, but return a job id which can be used to track the background job's progress and perhaps follow its logs. Use the `jobs/status` and `jobs/live` end points for those purposes. Async calls should be named in the `async` class method and can use `@job` (`Unrestful::AsyncJob`) to update job status or publish logs for the clients.
|
105
|
+
|
106
|
+
## Code
|
107
|
+
|
108
|
+
Most of the code for Unrestful is in 2 controllers: `Unrestful::EndpointsController` and `Unrestful::JobsController`. Most fo your questions will be answered by looking at those 2 methods!
|
109
|
+
|
110
|
+
## Authorization
|
111
|
+
|
112
|
+
By default Unrestful doesn't impose any authentication or authorization on the callers. However it comes with a prewritten JWT authorizer which can be used by using `include Unrestful::JwtSecured` in your own RPCController. This will look for a JWT on the header, will validate it and return the appropriate response.
|
113
|
+
|
114
|
+
The simplest way to use Unrestful with JWT is to use a tool like Auth0. Create you API and App and use it to generate and use the tokens when making calls to Unrestful.
|
115
|
+
|
116
|
+
## Installation
|
117
|
+
Add this line to your application's Gemfile:
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
gem 'unrestful'
|
121
|
+
```
|
122
|
+
|
123
|
+
And then execute:
|
124
|
+
```bash
|
125
|
+
$ bundle
|
126
|
+
```
|
127
|
+
|
128
|
+
Or install it yourself as:
|
129
|
+
```bash
|
130
|
+
$ gem install unrestful
|
131
|
+
```
|
132
|
+
|
133
|
+
## Contributing
|
134
|
+
Contribution directions go here.
|
135
|
+
|
136
|
+
## License
|
137
|
+
The gem is available as open source under the terms of the [Apache 2.0 License](https://opensource.org/licenses/Apache-2.0).
|
data/Rakefile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'Unrestful'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
|
18
|
+
load 'rails/tasks/engine.rake'
|
19
|
+
|
20
|
+
load 'rails/tasks/statistics.rake'
|
21
|
+
|
22
|
+
require 'bundler/gem_tasks'
|
23
|
+
|
24
|
+
require 'rake/testtask'
|
25
|
+
|
26
|
+
Rake::TestTask.new(:test) do |t|
|
27
|
+
t.libs << 'test'
|
28
|
+
t.pattern = 'test/**/*_test.rb'
|
29
|
+
t.verbose = false
|
30
|
+
end
|
31
|
+
|
32
|
+
task default: :test
|
@@ -0,0 +1,15 @@
|
|
1
|
+
// This is a manifest file that'll be compiled into application.js, which will include all the files
|
2
|
+
// listed below.
|
3
|
+
//
|
4
|
+
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
|
5
|
+
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
|
6
|
+
//
|
7
|
+
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
|
8
|
+
// compiled file. JavaScript code in this file should be added after the last require_* statement.
|
9
|
+
//
|
10
|
+
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
|
11
|
+
// about supported directives.
|
12
|
+
//
|
13
|
+
//= require rails-ujs
|
14
|
+
//= require activestorage
|
15
|
+
//= require_tree .
|
@@ -0,0 +1,15 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
11
|
+
* It is generally better to create a new file per style scope.
|
12
|
+
*
|
13
|
+
*= require_tree .
|
14
|
+
*= require_self
|
15
|
+
*/
|
@@ -0,0 +1,104 @@
|
|
1
|
+
Dir[File.join(Rails.root, 'app', 'rpc', '*.rb')].each { |file| require file }
|
2
|
+
require 'net/http'
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
module Unrestful
|
6
|
+
class EndpointsController < ApplicationController
|
7
|
+
include ActionController::Live
|
8
|
+
protect_from_forgery unless: -> { request.format.json? }
|
9
|
+
|
10
|
+
INVALID_PARAMS = [:method, :service, :controller, :action, :endpoint]
|
11
|
+
|
12
|
+
def endpoint
|
13
|
+
method = params[:method]
|
14
|
+
service = params[:service]
|
15
|
+
service_class = service.camelize.singularize
|
16
|
+
|
17
|
+
arguments = params.to_unsafe_h.symbolize_keys.reject { |x| INVALID_PARAMS.include? x }
|
18
|
+
|
19
|
+
klass = "::Rpc::#{service_class}".constantize
|
20
|
+
|
21
|
+
raise NameError, "#{klass} is not a Unrestful::RpcController" unless klass <= ::Unrestful::RpcController
|
22
|
+
actor = klass.new
|
23
|
+
|
24
|
+
live = actor.live_methods.include? method
|
25
|
+
async = actor.async_methods.include? method
|
26
|
+
|
27
|
+
actor.instance_variable_set(:@service, service)
|
28
|
+
actor.instance_variable_set(:@method, method)
|
29
|
+
actor.instance_variable_set(:@request, request)
|
30
|
+
actor.instance_variable_set(:@response, response)
|
31
|
+
actor.instance_variable_set(:@live, live)
|
32
|
+
actor.instance_variable_set(:@async, async)
|
33
|
+
|
34
|
+
# only public methods
|
35
|
+
raise "#{klass} doesn't have a method called #{method}" unless actor.respond_to? method
|
36
|
+
|
37
|
+
|
38
|
+
response.headers['X-Live'] = live ? 'true' : 'false'
|
39
|
+
response.headers['X-Async'] = async ? 'true' : 'false'
|
40
|
+
|
41
|
+
return if request.head?
|
42
|
+
|
43
|
+
if async
|
44
|
+
@job = AsyncJob.new
|
45
|
+
response.headers['X-Async-JobID'] = @job.job_id
|
46
|
+
@job.update(AsyncJob::ALLOCATED)
|
47
|
+
actor.instance_variable_set(:@job, @job)
|
48
|
+
end
|
49
|
+
|
50
|
+
response.headers['Content-Type'] = 'text/event-stream' if live
|
51
|
+
|
52
|
+
actor.before_callbacks
|
53
|
+
if arguments.count == 0
|
54
|
+
payload = actor.send(method)
|
55
|
+
else
|
56
|
+
payload = actor.send(method, arguments)
|
57
|
+
end
|
58
|
+
|
59
|
+
raise LiveError if live && !payload.nil?
|
60
|
+
|
61
|
+
unless live
|
62
|
+
if payload.nil?
|
63
|
+
render json: Unrestful::SuccessResponse.render({}.to_json)
|
64
|
+
else
|
65
|
+
render json: Unrestful::SuccessResponse.render(payload.as_json)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
actor.after_callbacks
|
70
|
+
rescue NameError => exc
|
71
|
+
not_found(exc: exc)
|
72
|
+
rescue ArgumentError => exc
|
73
|
+
fail(exc: exc)
|
74
|
+
rescue ::Unrestful::FailError => exc
|
75
|
+
fail(exc: exc)
|
76
|
+
rescue ::Unrestful::Error => exc
|
77
|
+
fail(exc: exc)
|
78
|
+
rescue IOError
|
79
|
+
# ignore as this could be the client disconnecting during streaming
|
80
|
+
rescue => exc
|
81
|
+
fail(exc: exc, status: :internal_server_error)
|
82
|
+
ensure
|
83
|
+
response.stream.close if live
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def not_found(exc:)
|
89
|
+
if !Rails.env.development?
|
90
|
+
fail(exc: exc, status: :not_found)
|
91
|
+
else
|
92
|
+
raise exc
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def fail(exc:, status: :bad_request, message: nil)
|
97
|
+
raise ArgumentError if exc.nil? && message.nil?
|
98
|
+
msg = exc.nil? ? message : exc.message
|
99
|
+
@job.update(AsyncJob::FAILED, message: msg) unless @job.nil?
|
100
|
+
|
101
|
+
render json: Unrestful::FailResponse.render(msg, exc: exc) , status: status
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Unrestful
|
2
|
+
class JobsController < ApplicationController
|
3
|
+
include ActionController::Live
|
4
|
+
include Unrestful::Utils
|
5
|
+
include Unrestful::JwtSecured
|
6
|
+
|
7
|
+
before_action :authenticate_request!
|
8
|
+
|
9
|
+
def status
|
10
|
+
job = AsyncJob.new(job_id: params[:job_id])
|
11
|
+
render(json: Unrestful::FailResponse.render("job #{job.job_id} doesn't exist"), status: :not_found) and return unless job.valid?
|
12
|
+
|
13
|
+
render json: job.as_json
|
14
|
+
end
|
15
|
+
|
16
|
+
def live
|
17
|
+
job = AsyncJob.new(job_id: params[:job_id])
|
18
|
+
response.headers['Content-Type'] = 'text/event-stream'
|
19
|
+
|
20
|
+
# this might be messy but will breakout of redis subscriptions when
|
21
|
+
# the app needs to be shutdown
|
22
|
+
trap(:INT) { raise StreamInterrupted }
|
23
|
+
|
24
|
+
# this is to keep redis connection alive during long sessions
|
25
|
+
ticker = safe_thread "ticker:#{job.job_id}" do
|
26
|
+
loop { job.redis.publish("unrestful:heartbeat", 1); sleep 5 }
|
27
|
+
end
|
28
|
+
sender = safe_thread "sender:#{job.job_id}" do
|
29
|
+
job.subscribe do |on|
|
30
|
+
on.message do |chn, message|
|
31
|
+
# we need to add a newline at the end or
|
32
|
+
# it will get stuck in the buffer
|
33
|
+
msg = message.end_with?("\n") ? message : "#{message}\n"
|
34
|
+
response.stream.write msg
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
ticker.join
|
39
|
+
sender.join
|
40
|
+
rescue Redis::TimeoutError
|
41
|
+
# ignore this
|
42
|
+
rescue AsyncError => exc
|
43
|
+
render json: Unrestful::FailResponse.render(exc.message, exc: exc) , status: :not_found
|
44
|
+
rescue IOError
|
45
|
+
# ignore as this could be the client disconnecting during streaming
|
46
|
+
job.unsubscribe if job
|
47
|
+
rescue StreamInterrupted
|
48
|
+
job.unsubscribe if job
|
49
|
+
ensure
|
50
|
+
ticker.kill if ticker
|
51
|
+
sender.kill if sender
|
52
|
+
response.stream.close
|
53
|
+
job.close if job
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
# overwriting this as scopes don't apply to this controller
|
59
|
+
def scope_included
|
60
|
+
true
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Unrestful</title>
|
5
|
+
<%= csrf_meta_tags %>
|
6
|
+
<%= csp_meta_tag %>
|
7
|
+
|
8
|
+
<%= stylesheet_link_tag "unrestful/application", media: "all" %>
|
9
|
+
<%= javascript_include_tag "unrestful/application" %>
|
10
|
+
</head>
|
11
|
+
<body>
|
12
|
+
|
13
|
+
<%= yield %>
|
14
|
+
|
15
|
+
</body>
|
16
|
+
</html>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,5 @@
|
|
1
|
+
Unrestful::Engine.routes.draw do
|
2
|
+
get 'jobs/status/:job_id', controller: :jobs, action: :status, as: :job_status
|
3
|
+
get 'jobs/live/:job_id', controller: :jobs, action: :live, as: :job_live
|
4
|
+
match 'rpc/:service/:method', controller: :endpoints, action: :endpoint, as: :endpoint, via: [:get, :post]
|
5
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'redis'
|
2
|
+
|
3
|
+
module Unrestful
|
4
|
+
class AsyncJob
|
5
|
+
include ActiveModel::Serializers::JSON
|
6
|
+
|
7
|
+
ALLOCATED = 0
|
8
|
+
RUNNING = 1
|
9
|
+
FAILED = 2
|
10
|
+
SUCCESS = 3
|
11
|
+
|
12
|
+
KEY_TIMEOUT = 3600
|
13
|
+
KEY_LENGTH = 10
|
14
|
+
CHANNEL_TIMEOUT = 10
|
15
|
+
|
16
|
+
attr_reader :job_id
|
17
|
+
|
18
|
+
def attributes
|
19
|
+
{
|
20
|
+
job_id: job_id,
|
21
|
+
state: state,
|
22
|
+
last_message: last_message,
|
23
|
+
ttl: ttl
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(job_id: nil)
|
28
|
+
if job_id.nil?
|
29
|
+
@job_id = SecureRandom.hex(KEY_LENGTH)
|
30
|
+
else
|
31
|
+
@job_id = job_id
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def update(state, message: '')
|
36
|
+
raise ArgumentError, 'failed states must have a message' if message.blank? && state == FAILED
|
37
|
+
|
38
|
+
redis.set(job_key, state)
|
39
|
+
redis.set(job_message, message) unless message.blank?
|
40
|
+
|
41
|
+
if state == ALLOCATED
|
42
|
+
redis.expire(job_key, KEY_TIMEOUT)
|
43
|
+
redis.expire(job_message, KEY_TIMEOUT)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def ttl
|
48
|
+
redis.ttl(job_key)
|
49
|
+
end
|
50
|
+
|
51
|
+
def state
|
52
|
+
redis.get(job_key)
|
53
|
+
end
|
54
|
+
|
55
|
+
def last_message
|
56
|
+
redis.get(job_message)
|
57
|
+
end
|
58
|
+
|
59
|
+
def delete
|
60
|
+
redis.del(job_key)
|
61
|
+
redis.del(job_message)
|
62
|
+
end
|
63
|
+
|
64
|
+
def subscribe(timeout: CHANNEL_TIMEOUT, &block)
|
65
|
+
raise AsyncError, "job #{job_key} doesn't exist" unless valid?
|
66
|
+
|
67
|
+
redis.subscribe_with_timeout(timeout, job_channel, &block)
|
68
|
+
end
|
69
|
+
|
70
|
+
def publish(message)
|
71
|
+
raise AsyncError, "job #{job_key} doesn't exist" unless valid?
|
72
|
+
|
73
|
+
redis.publish(job_channel, message)
|
74
|
+
end
|
75
|
+
|
76
|
+
def valid?
|
77
|
+
redis.exists(job_key)
|
78
|
+
end
|
79
|
+
|
80
|
+
def unsubscribe
|
81
|
+
redis.unsubscribe(job_channel)
|
82
|
+
rescue
|
83
|
+
# ignore unsub errors
|
84
|
+
end
|
85
|
+
|
86
|
+
def redis
|
87
|
+
@redis ||= Redis.new(url: Unrestful.configuration.redis_address)
|
88
|
+
end
|
89
|
+
|
90
|
+
def close
|
91
|
+
redis.unsubscribe(job_channel) if redis.subscribed?
|
92
|
+
ensure
|
93
|
+
@redis.quit
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def job_key
|
99
|
+
"unrestful:job:state:#{@job_id}"
|
100
|
+
end
|
101
|
+
|
102
|
+
def job_channel
|
103
|
+
"unrestful:job:channel:#{@job_id}"
|
104
|
+
end
|
105
|
+
|
106
|
+
def job_message
|
107
|
+
"unrestful:job:message:#{@job_id}"
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require_relative 'response'
|
2
|
+
require 'json/add/exception'
|
3
|
+
|
4
|
+
module Unrestful
|
5
|
+
class FailResponse < Unrestful::Response
|
6
|
+
|
7
|
+
attr_accessor :message
|
8
|
+
attr_accessor :exception
|
9
|
+
|
10
|
+
def self.render(message, exc: nil)
|
11
|
+
obj = Unrestful::FailResponse.new
|
12
|
+
obj.message = message
|
13
|
+
obj.exception = exc if !exc.nil? && Rails.env.development?
|
14
|
+
obj.ok = false
|
15
|
+
|
16
|
+
return obj.as_json
|
17
|
+
end
|
18
|
+
|
19
|
+
def as_json
|
20
|
+
result = { message: message }
|
21
|
+
result.merge!({ exception: exception }) unless exception.nil?
|
22
|
+
super.merge(result)
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'net/http'
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
module Unrestful
|
6
|
+
class JsonWebToken
|
7
|
+
ISSUER = 'https://cloud66.eu.auth0.com/'
|
8
|
+
LEEWAY = 30
|
9
|
+
AUDIENCE = ['central.admin.api.v2.development']
|
10
|
+
|
11
|
+
def self.verify(token)
|
12
|
+
JWT.decode(token, nil,
|
13
|
+
true,
|
14
|
+
algorithm: 'RS256',
|
15
|
+
iss: ISSUER,
|
16
|
+
verify_iss: true,
|
17
|
+
aud: AUDIENCE,
|
18
|
+
verify_aud: true) do |header|
|
19
|
+
jwks_hash[header['kid']]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.jwks_hash
|
24
|
+
jwks_raw = Net::HTTP.get URI("#{ISSUER}.well-known/jwks.json")
|
25
|
+
jwks_keys = Array(JSON.parse(jwks_raw)['keys'])
|
26
|
+
Hash[
|
27
|
+
jwks_keys.map do |k|
|
28
|
+
[
|
29
|
+
k['kid'],
|
30
|
+
OpenSSL::X509::Certificate.new(Base64.decode64(k['x5c'].first)).public_key
|
31
|
+
]
|
32
|
+
end
|
33
|
+
]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'jwt'
|
3
|
+
|
4
|
+
module Unrestful
|
5
|
+
module JwtSecured
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def authenticate_request!
|
11
|
+
@auth_payload, @auth_header = auth_token
|
12
|
+
|
13
|
+
raise AuthError, 'Insufficient scope' unless scope_included
|
14
|
+
end
|
15
|
+
|
16
|
+
def http_token
|
17
|
+
if request.headers['Authorization'].present?
|
18
|
+
request.headers['Authorization'].split(' ').last
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def auth_token
|
23
|
+
JsonWebToken.verify(http_token)
|
24
|
+
end
|
25
|
+
|
26
|
+
def scope_included
|
27
|
+
if self.class.assigned_scopes[@method] == nil
|
28
|
+
false
|
29
|
+
else
|
30
|
+
(String(@auth_payload['scope']).split(' ') & (self.class.assigned_scopes[@method])).any?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Unrestful
|
2
|
+
class RpcController
|
3
|
+
|
4
|
+
attr_reader :service
|
5
|
+
attr_reader :method
|
6
|
+
attr_reader :request
|
7
|
+
attr_reader :response
|
8
|
+
attr_reader :live
|
9
|
+
attr_reader :async
|
10
|
+
attr_reader :job
|
11
|
+
|
12
|
+
class_attribute :before_method_callbacks, default: ActiveSupport::HashWithIndifferentAccess.new
|
13
|
+
class_attribute :after_method_callbacks, default: ActiveSupport::HashWithIndifferentAccess.new
|
14
|
+
class_attribute :assigned_scopes, default: ActiveSupport::HashWithIndifferentAccess.new
|
15
|
+
class_attribute :live_methods, default: []
|
16
|
+
class_attribute :async_methods, default: []
|
17
|
+
|
18
|
+
def before_callbacks
|
19
|
+
self.class.before_method_callbacks.each do |k, v|
|
20
|
+
# no checks for now
|
21
|
+
self.send(k)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def after_callbacks
|
26
|
+
self.class.after_method_callbacks.each do |k, v|
|
27
|
+
self.send(k)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def write(message)
|
32
|
+
raise NotLiveError unless live
|
33
|
+
msg = message.end_with?("\n") ? message : "#{message}\n"
|
34
|
+
response.stream.write msg
|
35
|
+
end
|
36
|
+
|
37
|
+
protected
|
38
|
+
|
39
|
+
def self.before_method(method, options = {})
|
40
|
+
self.before_method_callbacks = { method => options }
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.after_method(method, options = {})
|
44
|
+
self.after_method_callbacks = { method => options }
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.scopes(scope_list)
|
48
|
+
self.assigned_scopes = ActiveSupport::HashWithIndifferentAccess.new(scope_list)
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.live(live_list)
|
52
|
+
self.live_methods = live_list
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.async(async_list)
|
56
|
+
self.async_methods = async_list
|
57
|
+
end
|
58
|
+
|
59
|
+
def fail!(message = "")
|
60
|
+
raise ::Unrestful::FailError, message
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require_relative 'response'
|
2
|
+
|
3
|
+
module Unrestful
|
4
|
+
class SuccessResponse < Unrestful::Response
|
5
|
+
|
6
|
+
attr_accessor :payload
|
7
|
+
|
8
|
+
def self.render(payload)
|
9
|
+
obj = Unrestful::SuccessResponse.new
|
10
|
+
obj.payload = payload
|
11
|
+
obj.ok = true
|
12
|
+
|
13
|
+
return obj.as_json
|
14
|
+
end
|
15
|
+
|
16
|
+
def as_json
|
17
|
+
super.merge({ payload: payload })
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Unrestful
|
2
|
+
module Utils
|
3
|
+
|
4
|
+
def watchdog(last_words)
|
5
|
+
yield
|
6
|
+
rescue Exception => exc
|
7
|
+
Rails.logger.debug "#{last_words}: #{exc}"
|
8
|
+
#raise exc
|
9
|
+
end
|
10
|
+
|
11
|
+
def safe_thread(name, &block)
|
12
|
+
Thread.new do
|
13
|
+
Thread.current['unrestful_name'.freeze] = name
|
14
|
+
watchdog(name, &block)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
data/lib/unrestful.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require "unrestful/engine"
|
2
|
+
|
3
|
+
Dir[File.join(__dir__, 'unrestful', '*.rb')].each { |file| require file }
|
4
|
+
|
5
|
+
module Unrestful
|
6
|
+
def self.configure(options = {}, &block)
|
7
|
+
@config = Unrestful::Config.new(options)
|
8
|
+
|
9
|
+
@config
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.configuration
|
13
|
+
@config || Unrestful::Config.new({})
|
14
|
+
end
|
15
|
+
|
16
|
+
class Config
|
17
|
+
def initialize(options)
|
18
|
+
@options = options
|
19
|
+
end
|
20
|
+
|
21
|
+
def redis_address
|
22
|
+
@options[:redis_address] || ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: unrestful
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Khash Sajadi
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-07-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 5.2.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 5.2.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: jwt
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.2'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: redis
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '4.1'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '4.1'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: puma
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: sqlite3
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: Sometimes you need an API but not a RESTful one. You also don't want
|
84
|
+
the whole gRPC or Thrift stack in your Rails app. Unrestful is the answer!
|
85
|
+
email:
|
86
|
+
- khash@cloud66.com
|
87
|
+
executables: []
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- README.md
|
92
|
+
- Rakefile
|
93
|
+
- app/assets/config/unrestful_manifest.js
|
94
|
+
- app/assets/javascripts/unrestful/application.js
|
95
|
+
- app/assets/stylesheets/unrestful/application.css
|
96
|
+
- app/controllers/unrestful/application_controller.rb
|
97
|
+
- app/controllers/unrestful/endpoints_controller.rb
|
98
|
+
- app/controllers/unrestful/jobs_controller.rb
|
99
|
+
- app/helpers/unrestful/application_helper.rb
|
100
|
+
- app/jobs/unrestful/application_job.rb
|
101
|
+
- app/mailers/unrestful/application_mailer.rb
|
102
|
+
- app/models/unrestful/application_record.rb
|
103
|
+
- app/views/layouts/unrestful/application.html.erb
|
104
|
+
- config/routes.rb
|
105
|
+
- lib/tasks/unrestful_tasks.rake
|
106
|
+
- lib/unrestful.rb
|
107
|
+
- lib/unrestful/async_job.rb
|
108
|
+
- lib/unrestful/engine.rb
|
109
|
+
- lib/unrestful/errors.rb
|
110
|
+
- lib/unrestful/fail_response.rb
|
111
|
+
- lib/unrestful/json_web_token.rb
|
112
|
+
- lib/unrestful/jwt_secured.rb
|
113
|
+
- lib/unrestful/response.rb
|
114
|
+
- lib/unrestful/rpc_controller.rb
|
115
|
+
- lib/unrestful/success_response.rb
|
116
|
+
- lib/unrestful/utils.rb
|
117
|
+
- lib/unrestful/version.rb
|
118
|
+
homepage: https://github.com/khash/unrestful
|
119
|
+
licenses:
|
120
|
+
- Apache-2.0
|
121
|
+
metadata: {}
|
122
|
+
post_install_message:
|
123
|
+
rdoc_options: []
|
124
|
+
require_paths:
|
125
|
+
- lib
|
126
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
132
|
+
requirements:
|
133
|
+
- - ">="
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
version: '0'
|
136
|
+
requirements: []
|
137
|
+
rubyforge_project:
|
138
|
+
rubygems_version: 2.7.9
|
139
|
+
signing_key:
|
140
|
+
specification_version: 4
|
141
|
+
summary: Unrestful is a simple RPC framework for Rails
|
142
|
+
test_files: []
|