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 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: []