unrestful 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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,2 @@
1
+ //= link_directory ../javascripts/unrestful .js
2
+ //= link_directory ../stylesheets/unrestful .css
@@ -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,5 @@
1
+ module Unrestful
2
+ class ApplicationController < ActionController::Base
3
+ protect_from_forgery with: :exception
4
+ end
5
+ end
@@ -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,4 @@
1
+ module Unrestful
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Unrestful
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module Unrestful
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: 'from@example.com'
4
+ layout 'mailer'
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module Unrestful
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -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,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :unrestful do
3
+ # # Task goes here
4
+ # 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,5 @@
1
+ module Unrestful
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Unrestful
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ module Unrestful
2
+ class Error < StandardError; end
3
+ class FailError < Error; end
4
+ class AuthError < Error; end
5
+ class NotLiveError < Error; end
6
+ class LiveError < Error; end
7
+ class AsyncError < Error; end
8
+ class StreamInterrupted < Error; end
9
+ 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,11 @@
1
+ module Unrestful
2
+ class Response
3
+
4
+ attr_accessor :ok
5
+
6
+ def as_json
7
+ { ok: @ok }
8
+ end
9
+
10
+ end
11
+ 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
@@ -0,0 +1,3 @@
1
+ module Unrestful
2
+ VERSION = '0.1.0'
3
+ 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: []