├── spec
└── dummy
│ ├── log
│ └── .keep
│ ├── lib
│ └── assets
│ │ └── .keep
│ ├── public
│ ├── favicon.ico
│ ├── apple-touch-icon.png
│ ├── apple-touch-icon-precomposed.png
│ ├── 500.html
│ ├── 422.html
│ └── 404.html
│ ├── .ruby-gemset
│ ├── app
│ ├── assets
│ │ ├── images
│ │ │ └── .keep
│ │ ├── javascripts
│ │ │ ├── channels
│ │ │ │ └── .keep
│ │ │ ├── cable.js
│ │ │ └── application.js
│ │ ├── config
│ │ │ └── manifest.js
│ │ └── stylesheets
│ │ │ └── application.css
│ ├── models
│ │ ├── concerns
│ │ │ └── .keep
│ │ ├── application_record.rb
│ │ └── acme_foo.rb
│ ├── controllers
│ │ ├── concerns
│ │ │ └── .keep
│ │ └── application_controller.rb
│ ├── views
│ │ └── layouts
│ │ │ ├── mailer.text.erb
│ │ │ ├── mailer.html.erb
│ │ │ └── application.html.erb
│ ├── helpers
│ │ └── application_helper.rb
│ ├── jobs
│ │ └── application_job.rb
│ ├── channels
│ │ └── application_cable
│ │ │ ├── channel.rb
│ │ │ └── connection.rb
│ ├── mailers
│ │ └── application_mailer.rb
│ └── rpc
│ │ └── account.rb
│ ├── .ruby-version
│ ├── package.json
│ ├── config
│ ├── routes.rb
│ ├── spring.rb
│ ├── environment.rb
│ ├── initializers
│ │ ├── mime_types.rb
│ │ ├── filter_parameter_logging.rb
│ │ ├── application_controller_renderer.rb
│ │ ├── cookies_serializer.rb
│ │ ├── backtrace_silencers.rb
│ │ ├── wrap_parameters.rb
│ │ ├── assets.rb
│ │ ├── inflections.rb
│ │ └── content_security_policy.rb
│ ├── cable.yml
│ ├── boot.rb
│ ├── public.pem
│ ├── application.rb
│ ├── database.yml
│ ├── locales
│ │ └── en.yml
│ ├── storage.yml
│ ├── puma.rb
│ ├── key.pem
│ └── environments
│ │ ├── test.rb
│ │ ├── development.rb
│ │ └── production.rb
│ ├── bin
│ ├── rake
│ ├── bundle
│ ├── rails
│ ├── yarn
│ ├── update
│ └── setup
│ ├── config.ru
│ └── Rakefile
├── .ruby-gemset
├── .ruby-version
├── app
├── assets
│ ├── images
│ │ └── unrestful
│ │ │ └── .keep
│ ├── config
│ │ └── unrestful_manifest.js
│ ├── javascripts
│ │ └── unrestful
│ │ │ └── application.js
│ └── stylesheets
│ │ └── unrestful
│ │ └── application.css
├── helpers
│ └── unrestful
│ │ └── application_helper.rb
├── jobs
│ └── unrestful
│ │ └── application_job.rb
├── models
│ └── unrestful
│ │ └── application_record.rb
├── controllers
│ └── unrestful
│ │ ├── application_controller.rb
│ │ ├── jobs_controller.rb
│ │ └── endpoints_controller.rb
├── mailers
│ └── unrestful
│ │ └── application_mailer.rb
└── views
│ └── layouts
│ └── unrestful
│ └── application.html.erb
├── lib
├── unrestful
│ ├── version.rb
│ ├── engine.rb
│ ├── response.rb
│ ├── errors.rb
│ ├── utils.rb
│ ├── success_response.rb
│ ├── fail_response.rb
│ ├── json_web_token.rb
│ ├── jwt_secured.rb
│ ├── rpc_controller.rb
│ └── async_job.rb
├── tasks
│ └── unrestful_tasks.rake
└── unrestful.rb
├── test
├── unrestful_test.rb
├── integration
│ └── navigation_test.rb
└── test_helper.rb
├── config
└── routes.rb
├── bin
└── rails
├── Gemfile
├── Rakefile
├── unrestful.gemspec
├── .editorconfig
├── .gitignore
├── Gemfile.lock
├── README.md
└── LICENSE
/spec/dummy/log/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.ruby-gemset:
--------------------------------------------------------------------------------
1 | unrestful
2 |
--------------------------------------------------------------------------------
/.ruby-version:
--------------------------------------------------------------------------------
1 | ruby-2.5.5
2 |
--------------------------------------------------------------------------------
/spec/dummy/lib/assets/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/images/unrestful/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/dummy/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/dummy/.ruby-gemset:
--------------------------------------------------------------------------------
1 | unrestful
2 |
--------------------------------------------------------------------------------
/spec/dummy/app/assets/images/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/dummy/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/dummy/.ruby-version:
--------------------------------------------------------------------------------
1 | ruby-2.5.5
2 |
--------------------------------------------------------------------------------
/spec/dummy/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/dummy/app/assets/javascripts/channels/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/dummy/public/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/dummy/app/views/layouts/mailer.text.erb:
--------------------------------------------------------------------------------
1 | <%= yield %>
2 |
--------------------------------------------------------------------------------
/lib/unrestful/version.rb:
--------------------------------------------------------------------------------
1 | module Unrestful
2 | VERSION = '0.1.4'
3 | end
4 |
--------------------------------------------------------------------------------
/spec/dummy/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/spec/dummy/app/jobs/application_job.rb:
--------------------------------------------------------------------------------
1 | class ApplicationJob < ActiveJob::Base
2 | end
3 |
--------------------------------------------------------------------------------
/spec/dummy/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dummy",
3 | "private": true,
4 | "dependencies": {}
5 | }
6 |
--------------------------------------------------------------------------------
/app/helpers/unrestful/application_helper.rb:
--------------------------------------------------------------------------------
1 | module Unrestful
2 | module ApplicationHelper
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/spec/dummy/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | end
3 |
--------------------------------------------------------------------------------
/app/jobs/unrestful/application_job.rb:
--------------------------------------------------------------------------------
1 | module Unrestful
2 | class ApplicationJob < ActiveJob::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/spec/dummy/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | mount Unrestful::Engine => "/api/admin/v2"
3 | end
4 |
--------------------------------------------------------------------------------
/spec/dummy/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require_relative '../config/boot'
3 | require 'rake'
4 | Rake.application.run
5 |
--------------------------------------------------------------------------------
/lib/tasks/unrestful_tasks.rake:
--------------------------------------------------------------------------------
1 | # desc "Explaining what the task does"
2 | # task :unrestful do
3 | # # Task goes here
4 | # end
5 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/application_record.rb:
--------------------------------------------------------------------------------
1 | class ApplicationRecord < ActiveRecord::Base
2 | self.abstract_class = true
3 | end
4 |
--------------------------------------------------------------------------------
/lib/unrestful/engine.rb:
--------------------------------------------------------------------------------
1 | module Unrestful
2 | class Engine < ::Rails::Engine
3 | isolate_namespace Unrestful
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/app/assets/config/unrestful_manifest.js:
--------------------------------------------------------------------------------
1 | //= link_directory ../javascripts/unrestful .js
2 | //= link_directory ../stylesheets/unrestful .css
3 |
--------------------------------------------------------------------------------
/spec/dummy/app/channels/application_cable/channel.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Channel < ActionCable::Channel::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/spec/dummy/app/channels/application_cable/connection.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Connection < ActionCable::Connection::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/spec/dummy/bin/bundle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
3 | load Gem.bin_path('bundler', 'bundle')
4 |
--------------------------------------------------------------------------------
/spec/dummy/app/mailers/application_mailer.rb:
--------------------------------------------------------------------------------
1 | class ApplicationMailer < ActionMailer::Base
2 | default from: 'from@example.com'
3 | layout 'mailer'
4 | end
5 |
--------------------------------------------------------------------------------
/spec/dummy/config/spring.rb:
--------------------------------------------------------------------------------
1 | %w[
2 | .ruby-version
3 | .rbenv-vars
4 | tmp/restart.txt
5 | tmp/caching-dev.txt
6 | ].each { |path| Spring.watch(path) }
7 |
--------------------------------------------------------------------------------
/app/models/unrestful/application_record.rb:
--------------------------------------------------------------------------------
1 | module Unrestful
2 | class ApplicationRecord < ActiveRecord::Base
3 | self.abstract_class = true
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/dummy/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require_relative 'config/environment'
4 |
5 | run Rails.application
6 |
--------------------------------------------------------------------------------
/spec/dummy/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | APP_PATH = File.expand_path('../config/application', __dir__)
3 | require_relative '../config/boot'
4 | require 'rails/commands'
5 |
--------------------------------------------------------------------------------
/spec/dummy/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require_relative 'application'
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
--------------------------------------------------------------------------------
/spec/dummy/app/assets/config/manifest.js:
--------------------------------------------------------------------------------
1 | //= link_tree ../images
2 | //= link_directory ../javascripts .js
3 | //= link_directory ../stylesheets .css
4 | //= link unrestful_manifest.js
5 |
--------------------------------------------------------------------------------
/test/unrestful_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class Unrestful::Test < ActiveSupport::TestCase
4 | test "truth" do
5 | assert_kind_of Module, Unrestful
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/app/controllers/unrestful/application_controller.rb:
--------------------------------------------------------------------------------
1 | module Unrestful
2 | class ApplicationController < ActionController::Base
3 | protect_from_forgery with: :exception
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/lib/unrestful/response.rb:
--------------------------------------------------------------------------------
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
12 |
--------------------------------------------------------------------------------
/app/mailers/unrestful/application_mailer.rb:
--------------------------------------------------------------------------------
1 | module Unrestful
2 | class ApplicationMailer < ActionMailer::Base
3 | default from: 'from@example.com'
4 | layout 'mailer'
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/test/integration/navigation_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class NavigationTest < ActionDispatch::IntegrationTest
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/acme_foo.rb:
--------------------------------------------------------------------------------
1 | class AcmeFoo
2 | include ActiveModel::Serializers::JSON
3 |
4 | attr_accessor :from, :to
5 |
6 | def attributes
7 | instance_values
8 | end
9 |
10 | end
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new mime types for use in respond_to blocks:
4 | # Mime::Type.register "text/richtext", :rtf
5 |
--------------------------------------------------------------------------------
/spec/dummy/config/cable.yml:
--------------------------------------------------------------------------------
1 | development:
2 | adapter: async
3 |
4 | test:
5 | adapter: async
6 |
7 | production:
8 | adapter: redis
9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
10 | channel_prefix: dummy_production
11 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Configure sensitive parameters which will be filtered from the log file.
4 | Rails.application.config.filter_parameters += [:password]
5 |
--------------------------------------------------------------------------------
/spec/dummy/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require_relative 'config/application'
5 |
6 | Rails.application.load_tasks
7 |
--------------------------------------------------------------------------------
/spec/dummy/config/boot.rb:
--------------------------------------------------------------------------------
1 | # Set up gems listed in the Gemfile.
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__)
3 |
4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
5 | $LOAD_PATH.unshift File.expand_path('../../../lib', __dir__)
6 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/application_controller_renderer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # ActiveSupport::Reloader.to_prepare do
4 | # ApplicationController.renderer.defaults.merge!(
5 | # http_host: 'example.org',
6 | # https: false
7 | # )
8 | # end
9 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/cookies_serializer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Specify a serializer for the signed and encrypted cookie jars.
4 | # Valid options are :json, :marshal, and :hybrid.
5 | Rails.application.config.action_dispatch.cookies_serializer = :json
6 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
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
6 |
--------------------------------------------------------------------------------
/spec/dummy/app/views/layouts/mailer.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
--------------------------------------------------------------------------------
/lib/unrestful/errors.rb:
--------------------------------------------------------------------------------
1 | module Unrestful
2 | class Error < StandardError;
3 | end
4 | class FailError < Error;
5 | end
6 | class AuthError < Error;
7 | end
8 | class NotLiveError < Error;
9 | end
10 | class LiveError < Error;
11 | end
12 | class AsyncError < Error;
13 | end
14 | class StreamInterrupted < Error;
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/spec/dummy/bin/yarn:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | APP_ROOT = File.expand_path('..', __dir__)
3 | Dir.chdir(APP_ROOT) do
4 | begin
5 | exec "yarnpkg", *ARGV
6 | rescue Errno::ENOENT
7 | $stderr.puts "Yarn executable was not detected in the system."
8 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install"
9 | exit 1
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/spec/dummy/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Dummy
5 | <%= csrf_meta_tags %>
6 | <%= csp_meta_tag %>
7 |
8 | <%= stylesheet_link_tag 'application', media: 'all' %>
9 | <%= javascript_include_tag 'application' %>
10 |
11 |
12 |
13 | <%= yield %>
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/views/layouts/unrestful/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Unrestful
5 | <%= csrf_meta_tags %>
6 | <%= csp_meta_tag %>
7 |
8 | <%= stylesheet_link_tag "unrestful/application", media: "all" %>
9 | <%= javascript_include_tag "unrestful/application" %>
10 |
11 |
12 |
13 | <%= yield %>
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/lib/unrestful/utils.rb:
--------------------------------------------------------------------------------
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
20 |
--------------------------------------------------------------------------------
/spec/dummy/app/assets/javascripts/cable.js:
--------------------------------------------------------------------------------
1 | // Action Cable provides the framework to deal with WebSockets in Rails.
2 | // You can generate new channels where WebSocket features live using the `rails generate channel` command.
3 | //
4 | //= require action_cable
5 | //= require_self
6 | //= require_tree ./channels
7 |
8 | (function() {
9 | this.App || (this.App = {});
10 |
11 | App.cable = ActionCable.createConsumer();
12 |
13 | }).call(this);
14 |
--------------------------------------------------------------------------------
/lib/unrestful/success_response.rb:
--------------------------------------------------------------------------------
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
21 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7 | # Rails.backtrace_cleaner.remove_silencers!
8 |
--------------------------------------------------------------------------------
/spec/dummy/config/public.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN PUBLIC KEY-----
2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4LKx/o4SMjqo6M96r7Ls
3 | c8Cya5z4QJfPyw+mTeCoV7o8PPYRk4355XgIxdA1/iv7qyxIgMu88Fk8vIzgw/Wv
4 | eFoeFM93/aS4G8u0OsVJGo6ad40tzvq1fELlgegwVhK3B70OOruYVlu+qfNVwP8+
5 | OL12DrcAsSjH+OlEjHYqNr6PDrsFKCfF3yd27dXLLFlsaUKM1sePM+ka6GSNG2WD
6 | R+Zk9+rre5OH0aXjtsj3p8twzbiv970Crf6A72Ut8h0Umsp/hXSd1AN6Jj0bgjJI
7 | yOR3Q/AFSac7RygVv7BKCzMl/H9Ga/PBbz7qBzN+Cw4edp58Y3YL2wkzQFXtWR/g
8 | SQIDAQAB
9 | -----END PUBLIC KEY-----
10 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # This file contains settings for ActionController::ParamsWrapper which
4 | # is enabled by default.
5 |
6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
7 | ActiveSupport.on_load(:action_controller) do
8 | wrap_parameters format: [:json]
9 | end
10 |
11 | # To enable root element in JSON for ActiveRecord objects.
12 | # ActiveSupport.on_load(:active_record) do
13 | # self.include_root_in_json = true
14 | # end
15 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # This command will automatically be run when you run "rails" with Rails gems
3 | # installed from the root of your application.
4 |
5 | ENGINE_ROOT = File.expand_path('..', __dir__)
6 | ENGINE_PATH = File.expand_path('../lib/unrestful/engine', __dir__)
7 | APP_PATH = File.expand_path('../spec/dummy/config/application', __dir__)
8 |
9 | # Set up gems listed in the Gemfile.
10 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
11 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
12 |
13 | require 'rails/all'
14 | require 'rails/engine/commands'
15 |
--------------------------------------------------------------------------------
/spec/dummy/config/application.rb:
--------------------------------------------------------------------------------
1 | require_relative 'boot'
2 |
3 | require 'rails/all'
4 |
5 | Bundler.require(*Rails.groups)
6 | require "unrestful"
7 |
8 | module Dummy
9 | class Application < Rails::Application
10 | # Initialize configuration defaults for originally generated Rails version.
11 | config.load_defaults 5.2
12 |
13 | # Settings in config/environments/* take precedence over those specified here.
14 | # Application configuration can go into files in config/initializers
15 | # -- all .rb files in that directory are automatically loaded after loading
16 | # the framework and any gems in your application.
17 | end
18 | end
19 |
20 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" }
3 |
4 | # Declare your gem's dependencies in unrestful.gemspec.
5 | # Bundler will treat runtime dependencies like base dependencies, and
6 | # development dependencies will be added by default to the :development group.
7 | gemspec
8 |
9 | # Declare any dependencies that are still in development here instead of in
10 | # your gemspec. These might include edge Rails or gems from your path or
11 | # Git. Remember to move these dependencies to your gemspec before releasing
12 | # your gem to rubygems.org.
13 |
14 | # To use a debugger
15 | gem 'byebug', group: [:development, :test]
16 |
--------------------------------------------------------------------------------
/lib/unrestful/fail_response.rb:
--------------------------------------------------------------------------------
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
27 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/assets.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Version of your assets, change this if you want to expire all your assets.
4 | Rails.application.config.assets.version = '1.0'
5 |
6 | # Add additional assets to the asset load path.
7 | # Rails.application.config.assets.paths << Emoji.images_path
8 | # Add Yarn node_modules folder to the asset load path.
9 | Rails.application.config.assets.paths << Rails.root.join('node_modules')
10 |
11 | # Precompile additional assets.
12 | # application.js, application.css, and all non-JS/CSS in the app/assets
13 | # folder are already added.
14 | # Rails.application.config.assets.precompile += %w( admin.js admin.css )
15 |
--------------------------------------------------------------------------------
/spec/dummy/config/database.yml:
--------------------------------------------------------------------------------
1 | # SQLite version 3.x
2 | # gem install sqlite3
3 | #
4 | # Ensure the SQLite 3 gem is defined in your Gemfile
5 | # gem 'sqlite3'
6 | #
7 | default: &default
8 | adapter: sqlite3
9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
10 | timeout: 5000
11 |
12 | development:
13 | <<: *default
14 | database: db/development.sqlite3
15 |
16 | # Warning: The database defined as "test" will be erased and
17 | # re-generated from your development database when you run "rake".
18 | # Do not set this db to the same as development or production.
19 | test:
20 | <<: *default
21 | database: db/test.sqlite3
22 |
23 | production:
24 | <<: *default
25 | database: db/production.sqlite3
26 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format. Inflections
4 | # are locale specific, and you may define rules for as many different
5 | # locales as you wish. All of these examples are active by default:
6 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
7 | # inflect.plural /^(ox)$/i, '\1en'
8 | # inflect.singular /^(ox)en/i, '\1'
9 | # inflect.irregular 'person', 'people'
10 | # inflect.uncountable %w( fish sheep )
11 | # end
12 |
13 | # These inflection rules are supported but not enabled by default:
14 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
15 | # inflect.acronym 'RESTful'
16 | # end
17 |
--------------------------------------------------------------------------------
/spec/dummy/app/rpc/account.rb:
--------------------------------------------------------------------------------
1 | require 'unrestful'
2 |
3 | module Rpc
4 | class Account < ::Unrestful::RpcController
5 | include Unrestful::JwtSecured
6 | scopes({
7 | switch_owner: ['write:account'],
8 | migrate: ['read:account'],
9 | long_one: ['read:account']
10 | })
11 | live(['migrate'])
12 | async(['long_one'])
13 |
14 | before_method :authenticate_request!
15 | #after_method :do_something
16 |
17 | def switch_owner(from:, to:)
18 | foo = ::AcmeFoo.new
19 | foo.from = from
20 | foo.to = to
21 |
22 | return foo
23 | end
24 |
25 | def migrate(repeat:)
26 | repeat.to_i.times {
27 | write "hello\n"
28 | sleep 1
29 | }
30 |
31 | return nil
32 | end
33 |
34 | def long_one
35 | return { not_done_yet: true }
36 | end
37 |
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
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
33 |
--------------------------------------------------------------------------------
/app/assets/javascripts/unrestful/application.js:
--------------------------------------------------------------------------------
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 .
16 |
--------------------------------------------------------------------------------
/spec/dummy/app/assets/javascripts/application.js:
--------------------------------------------------------------------------------
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 .
16 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/unrestful/application.css:
--------------------------------------------------------------------------------
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 | */
16 |
--------------------------------------------------------------------------------
/spec/dummy/app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
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 | */
16 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | # Configure Rails Environment
2 | ENV["RAILS_ENV"] = "test"
3 |
4 | require_relative "../spec/dummy/config/environment"
5 | ActiveRecord::Migrator.migrations_paths = [File.expand_path("../spec/dummy/db/migrate", __dir__)]
6 | ActiveRecord::Migrator.migrations_paths << File.expand_path('../db/migrate', __dir__)
7 | require "rails/test_help"
8 |
9 | # Filter out Minitest backtrace while allowing backtrace from other libraries
10 | # to be shown.
11 | Minitest.backtrace_filter = Minitest::BacktraceFilter.new
12 |
13 |
14 | # Load fixtures from the engine
15 | if ActiveSupport::TestCase.respond_to?(:fixture_path=)
16 | ActiveSupport::TestCase.fixture_path = File.expand_path("fixtures", __dir__)
17 | ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path
18 | ActiveSupport::TestCase.file_fixture_path = ActiveSupport::TestCase.fixture_path + "/files"
19 | ActiveSupport::TestCase.fixtures :all
20 | end
21 |
--------------------------------------------------------------------------------
/spec/dummy/bin/update:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'fileutils'
3 | include FileUtils
4 |
5 | # path to your application root.
6 | APP_ROOT = File.expand_path('..', __dir__)
7 |
8 | def system!(*args)
9 | system(*args) || abort("\n== Command #{args} failed ==")
10 | end
11 |
12 | chdir APP_ROOT do
13 | # This script is a way to update your development environment automatically.
14 | # Add necessary update steps to this file.
15 |
16 | puts '== Installing dependencies =='
17 | system! 'gem install bundler --conservative'
18 | system('bundle check') || system!('bundle install')
19 |
20 | # Install JavaScript dependencies if using Yarn
21 | # system('bin/yarn')
22 |
23 | puts "\n== Updating database =="
24 | system! 'bin/rails db:migrate'
25 |
26 | puts "\n== Removing old logs and tempfiles =="
27 | system! 'bin/rails log:clear tmp:clear'
28 |
29 | puts "\n== Restarting application server =="
30 | system! 'bin/rails restart'
31 | end
32 |
--------------------------------------------------------------------------------
/spec/dummy/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization
2 | # and are automatically loaded by Rails. If you want to use locales other
3 | # than English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t 'hello'
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t('hello') %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # The following keys must be escaped otherwise they will not be retrieved by
20 | # the default I18n backend:
21 | #
22 | # true, false, on, off, yes, no
23 | #
24 | # Instead, surround them with single quotes.
25 | #
26 | # en:
27 | # 'true': 'foo'
28 | #
29 | # To learn more, please read the Rails Internationalization guide
30 | # available at http://guides.rubyonrails.org/i18n.html.
31 |
32 | en:
33 | hello: "Hello world"
34 |
--------------------------------------------------------------------------------
/lib/unrestful/json_web_token.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require 'net/http'
3 | require 'uri'
4 |
5 | module Unrestful
6 | class JsonWebToken
7 | LEEWAY = 30
8 |
9 | def self.verify(token)
10 | JWT.decode(token, nil,
11 | true,
12 | algorithm: 'RS256',
13 | iss: Unrestful.configuration.issuer,
14 | verify_iss: true,
15 | aud: Unrestful.configuration.audience,
16 | verify_aud: true) do |header|
17 | jwks_hash[header['kid']]
18 | end
19 | end
20 |
21 | def self.jwks_hash
22 | jwks_raw = Net::HTTP.get URI("#{Unrestful.configuration.issuer}.well-known/jwks.json")
23 | jwks_keys = Array(JSON.parse(jwks_raw)['keys'])
24 | Hash[
25 | jwks_keys.map do |k|
26 | [
27 | k['kid'],
28 | OpenSSL::X509::Certificate.new(Base64.decode64(k['x5c'].first)).public_key
29 | ]
30 | end
31 | ]
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/unrestful.gemspec:
--------------------------------------------------------------------------------
1 | $:.push File.expand_path("lib", __dir__)
2 |
3 | # Maintain your gem's version:
4 | require "unrestful/version"
5 |
6 | # Describe your gem and declare its dependencies:
7 | Gem::Specification.new do |s|
8 | s.name = "unrestful"
9 | s.version = Unrestful::VERSION
10 | s.authors = ["Khash Sajadi"]
11 | s.email = ["khash@cloud66.com"]
12 | s.homepage = "https://github.com/khash/unrestful"
13 | s.summary = "Unrestful is a simple RPC framework for Rails"
14 | s.description = "Sometimes you need an API but not a RESTful one. You also don't want the whole gRPC or Thrift stack in your Rails app. Unrestful is the answer!"
15 | s.license = "Apache-2.0"
16 |
17 | s.files = Dir["{app,config,db,lib}/**/*", "APACHE-LICENSE", "Rakefile", "README.md"]
18 |
19 | s.add_dependency 'rails', '~> 5.2.0'
20 | s.add_dependency 'jwt', '~> 2.2'
21 | s.add_dependency 'redis', '~> 4.1'
22 |
23 | s.add_development_dependency 'puma'
24 | s.add_development_dependency 'sqlite3'
25 | end
26 |
--------------------------------------------------------------------------------
/lib/unrestful.rb:
--------------------------------------------------------------------------------
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 | block.call(@config) if block.present?
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 issuer
22 | @options[:issuer] || ENV.fetch("ISSUER")
23 | end
24 |
25 | def issuer=(value)
26 | @options[:issuer] = value
27 | end
28 |
29 | def audience
30 | @options[:audience] || ENV.fetch("AUDIENCE")
31 | end
32 |
33 | def audience=(value)
34 | @options[:audience] = value
35 | end
36 |
37 | def redis_address
38 | @options[:redis_address] || ENV.fetch("REDIS_URL") {"redis://localhost:6379/1"}
39 | end
40 |
41 | def redis_address=(value)
42 | @options[:redis_address] = value
43 | end
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/spec/dummy/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'fileutils'
3 | include FileUtils
4 |
5 | # path to your application root.
6 | APP_ROOT = File.expand_path('..', __dir__)
7 |
8 | def system!(*args)
9 | system(*args) || abort("\n== Command #{args} failed ==")
10 | end
11 |
12 | chdir APP_ROOT do
13 | # This script is a starting point to setup your application.
14 | # Add necessary setup steps to this file.
15 |
16 | puts '== Installing dependencies =='
17 | system! 'gem install bundler --conservative'
18 | system('bundle check') || system!('bundle install')
19 |
20 | # Install JavaScript dependencies if using Yarn
21 | # system('bin/yarn')
22 |
23 | # puts "\n== Copying sample files =="
24 | # unless File.exist?('config/database.yml')
25 | # cp 'config/database.yml.sample', 'config/database.yml'
26 | # end
27 |
28 | puts "\n== Preparing database =="
29 | system! 'bin/rails db:setup'
30 |
31 | puts "\n== Removing old logs and tempfiles =="
32 | system! 'bin/rails log:clear tmp:clear'
33 |
34 | puts "\n== Restarting application server =="
35 | system! 'bin/rails restart'
36 | end
37 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: http://EditorConfig.org
2 | # top-most EditorConfig file
3 | root = true
4 |
5 | # all files
6 | [*]
7 | # Unix-style newlines
8 | end_of_line = lf
9 | # newline ending every file
10 | insert_final_newline = true
11 | # utf-8 charset
12 | charset = utf-8
13 | # remove and trailing whitespace chars
14 | trim_trailing_whitespace = true
15 | # default to tab spacing
16 | indent_style = space
17 | # default to 4 char tabs
18 | indent_size = 2
19 | # no max line length
20 | max_line_length = off
21 | # keep curly on same line if possible
22 | curly_bracket_next_line = false
23 | # https://en.wikipedia.org/wiki/Indent_style#K.26R_style
24 | indent_brace_style = K&R
25 | # "something + something" not "something+something"
26 | spaces_around_operators = true
27 | # "something(true)" not "something ( true )"
28 | spaces_around_brackets = none
29 |
30 | [*.{vue,js,css,scss,html,xml,html.erb}]
31 | indent_style = space
32 | indent_size = 4
33 |
34 | [*.{yml,yml.liquid,yaml,yaml.liquid,sh,sh.erb}]
35 | indent_style = space
36 | indent_size = 2
37 | spaces_around_operators = false
38 |
39 | [*.{rb,rb.erb,rb.static}]
40 | indent_style = space
41 | indent_size = 2
42 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/content_security_policy.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Define an application-wide content security policy
4 | # For further information see the following documentation
5 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
6 |
7 | # Rails.application.config.content_security_policy do |policy|
8 | # policy.default_src :self, :https
9 | # policy.font_src :self, :https, :data
10 | # policy.img_src :self, :https, :data
11 | # policy.object_src :none
12 | # policy.script_src :self, :https
13 | # policy.style_src :self, :https
14 |
15 | # # Specify URI for violation reports
16 | # # policy.report_uri "/csp-violation-report-endpoint"
17 | # end
18 |
19 | # If you are using UJS then enable automatic nonce generation
20 | # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
21 |
22 | # Report CSP violations to a specified URI
23 | # For further information see the following documentation:
24 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
25 | # Rails.application.config.content_security_policy_report_only = true
26 |
--------------------------------------------------------------------------------
/spec/dummy/config/storage.yml:
--------------------------------------------------------------------------------
1 | test:
2 | service: Disk
3 | root: <%= Rails.root.join("tmp/storage") %>
4 |
5 | local:
6 | service: Disk
7 | root: <%= Rails.root.join("storage") %>
8 |
9 | # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
10 | # amazon:
11 | # service: S3
12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
14 | # region: us-east-1
15 | # bucket: your_own_bucket
16 |
17 | # Remember not to checkin your GCS keyfile to a repository
18 | # google:
19 | # service: GCS
20 | # project: your_project
21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
22 | # bucket: your_own_bucket
23 |
24 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
25 | # microsoft:
26 | # service: AzureStorage
27 | # storage_account_name: your_account_name
28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
29 | # container: your_container_name
30 |
31 | # mirror:
32 | # service: Mirror
33 | # primary: local
34 | # mirrors: [ amazon, google, microsoft ]
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.rbc
2 | capybara-*.html
3 | .rspec
4 | /log
5 | /tmp
6 | /db/*.sqlite3
7 | /db/*.sqlite3-journal
8 | /public/system
9 | /coverage/
10 | /spec/tmp
11 | *.orig
12 | rerun.txt
13 | pickle-email-*.html
14 |
15 | config/initializers/secret_token.rb
16 | config/master.key
17 |
18 | # Only include if you have production secrets in this file, which is no longer a Rails default
19 | # config/secrets.yml
20 |
21 | # dotenv
22 | .env
23 |
24 | ## Environment normalization:
25 | /.bundle
26 | /vendor/bundle
27 |
28 | # these should all be checked in to normalize the environment:
29 | # Gemfile.lock, .ruby-version, .ruby-gemset
30 |
31 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
32 | .rvmrc
33 |
34 | # if using bower-rails ignore default bower_components path bower.json files
35 | /vendor/assets/bower_components
36 | *.bowerrc
37 | bower.json
38 |
39 | # Ignore pow environment settings
40 | .powenv
41 |
42 | # Ignore Byebug command history file.
43 | .byebug_history
44 |
45 | # Ignore node_modules
46 | node_modules/
47 |
48 | .bundle/
49 | log/*.log
50 | pkg/
51 | spec/dummy/db/*.sqlite3
52 | spec/dummy/db/*.sqlite3-journal
53 | spec/dummy/log/*.log
54 | spec/dummy/node_modules/
55 | spec/dummy/yarn-error.log
56 | spec/dummy/storage/
57 | spec/dummy/tmp/
58 | /.idea
59 |
60 | unrestful*.gem
61 |
--------------------------------------------------------------------------------
/spec/dummy/config/puma.rb:
--------------------------------------------------------------------------------
1 | # Puma can serve each request in a thread from an internal thread pool.
2 | # The `threads` method setting takes two numbers: a minimum and maximum.
3 | # Any libraries that use thread pools should be configured to match
4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum
5 | # and maximum; this matches the default thread size of Active Record.
6 | #
7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
8 | threads threads_count, threads_count
9 |
10 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000.
11 | #
12 | port ENV.fetch("PORT") { 3000 }
13 |
14 | # Specifies the `environment` that Puma will run in.
15 | #
16 | environment ENV.fetch("RAILS_ENV") { "development" }
17 |
18 | # Specifies the number of `workers` to boot in clustered mode.
19 | # Workers are forked webserver processes. If using threads and workers together
20 | # the concurrency of the application would be max `threads` * `workers`.
21 | # Workers do not work on JRuby or Windows (both of which do not support
22 | # processes).
23 | #
24 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 }
25 |
26 | # Use the `preload_app!` method when specifying a `workers` number.
27 | # This directive tells Puma to first boot the application and load code
28 | # before forking the application. This takes advantage of Copy On Write
29 | # process behavior so workers use less memory.
30 | #
31 | # preload_app!
32 |
33 | # Allow puma to be restarted by `rails restart` command.
34 | plugin :tmp_restart
35 |
--------------------------------------------------------------------------------
/lib/unrestful/jwt_secured.rb:
--------------------------------------------------------------------------------
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 | raise AuthError, 'Insufficient scope' unless scope_included
13 | end
14 |
15 | def http_token
16 | if request.headers['Authorization'].present?
17 | # strip off the Bearer
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 | permissions_required = class_assigned_scopes
28 | permissions_present = @auth_payload['permissions'] || []
29 | # ensure that we have the required permission to call the method
30 | (permissions_present & permissions_required).any?
31 | end
32 |
33 | def class_assigned_scopes
34 | class_assigned_scopes = self.class.assigned_scopes[@method] || []
35 | # ensure that we have a scope defined for this method, and that it has an array value
36 | if class_assigned_scopes.nil? || class_assigned_scopes.empty? || !class_assigned_scopes.is_a?(Array)
37 | raise "#{self.class.name} MUST declare a \"scopes\" hash that INCLUDES key \"#{@method}\" with value of an array of permissions"
38 | end
39 | class_assigned_scopes
40 | rescue NoMethodError
41 | raise "#{self.class.name} MUST implement ::Unrestful::RpcController AND declare \"scopes\" for each method request, with its corresponding array of permissions"
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/spec/dummy/config/key.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIIEpAIBAAKCAQEA4LKx/o4SMjqo6M96r7Lsc8Cya5z4QJfPyw+mTeCoV7o8PPYR
3 | k4355XgIxdA1/iv7qyxIgMu88Fk8vIzgw/WveFoeFM93/aS4G8u0OsVJGo6ad40t
4 | zvq1fELlgegwVhK3B70OOruYVlu+qfNVwP8+OL12DrcAsSjH+OlEjHYqNr6PDrsF
5 | KCfF3yd27dXLLFlsaUKM1sePM+ka6GSNG2WDR+Zk9+rre5OH0aXjtsj3p8twzbiv
6 | 970Crf6A72Ut8h0Umsp/hXSd1AN6Jj0bgjJIyOR3Q/AFSac7RygVv7BKCzMl/H9G
7 | a/PBbz7qBzN+Cw4edp58Y3YL2wkzQFXtWR/gSQIDAQABAoIBAFzQk4OhrdR/tIvO
8 | QFBZKSC7RTf8c/NCgjvPsBNVLFRogj9wKVx49fOafI0xb0wZYPCY7y38eoQRaGw+
9 | CQ4I6z1chDZ2aIsmQkKBB2aLXaIRq66ca4KmvtagT2s0vNqhCmew6TLLkKaDaSOM
10 | dyysgkgvwpdbcna7cLbZrE4U9WT81bbzwOZ2nYQMrYNCEK+rZaOhL2QQKetJCpna
11 | wxKGS8m/Qqic/s6HO7U5E/TsKFJcEmPgRtn6GPmqyEEDdXMHBgTbkvbd0PiJPSdc
12 | /y4XMrAKHQLzHsvPmxkGpWftosO6rh1U1ck/OCE8PP7Wa6o7f5ShM+v1MlxtAv4u
13 | 8ZK7zj0CgYEA8fGl/SxdR6TwP2A6NOarVvIvjGJM5S770+eeQnTpm7Cm5e4w4qwH
14 | qAVuAZz357SVHJcADZlgY1ASsQYeGxZIReTi98g8+H7iaxBRz0fAkBSn5TwJiM80
15 | HoWArjpcE59A/fGUYFt2e5hirtxJJPYW4QxMxnwuFsWx+Kk2u6NtvOsCgYEA7cCN
16 | 4O9l3/lhGr+47ASPnJmS2HXrt9cvgzbagEas8ZYHFW1/S6BPanSrxbZLTj7N/xrO
17 | 9sHMcB+1V8HnK3XRfN1ce1sznm0GJflxn7N0+gcxIKBlfbB9izj/slrjJIuS97W0
18 | WhFjPqEKCWppo4I8Q/ewKt7rmd5RkeHqMTFT+psCgYEA7fA80hODWSY4r9su4z/H
19 | WaasZF94SBxAxVGQLsNTyy2btZzers2IahGM8kEw/Mp3qrpF7xnb1U+2Uio2CWWM
20 | hlBndk+Sxr/iZHCUREnIcuodhC/bIJTGKQ4aUz5Jt3JzNEsVJP5OM5bV1ioGNTdf
21 | oMu75afSm8qpEGc50KcVESkCgYBjIrpGQ6yIEtUxsSXrjA9R7ht0FN+AHcMbBIFh
22 | oZa1eahkf+7nWuYibpm9bEDEVJ3StJv9+ltDmYUlHZ5F2e/LEAZjDWldsvowVW3S
23 | eKLbKqqKfzcyjKgcqFy+QvWZpHVYwrR8JenrEH095dg8rK5ybNJRXfiBhVkf6kKb
24 | 1oS85wKBgQCQj9Oj9M0MzwFhLv29SOe1LG8My0yeJDVyekrEBbIJF5Tp8E0q/iL+
25 | uiB8J7ohEIw53YuYoNGhQtIWTC7DPzIpK84cMA/kDEhHildejTUW1dhJdCuhIxej
26 | 9bcTrJdLX9Yhc/1Es4zM5toecjaSvLoOilxy/u5x+/HkTvghj2M7ow==
27 | -----END RSA PRIVATE KEY-----
28 |
--------------------------------------------------------------------------------
/lib/unrestful/rpc_controller.rb:
--------------------------------------------------------------------------------
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
65 |
--------------------------------------------------------------------------------
/spec/dummy/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
We're sorry, but something went wrong.
62 |
63 |
If you are the application owner check the logs for more information.
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/spec/dummy/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The change you wanted was rejected.
62 |
Maybe you tried to change something you didn't have access to.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/spec/dummy/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The page you were looking for doesn't exist.
62 |
You may have mistyped the address or the page may have moved.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/spec/dummy/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # The test environment is used exclusively to run your application's
5 | # test suite. You never need to work with it otherwise. Remember that
6 | # your test database is "scratch space" for the test suite and is wiped
7 | # and recreated between test runs. Don't rely on the data there!
8 | config.cache_classes = true
9 |
10 | # Do not eager load code on boot. This avoids loading your whole application
11 | # just for the purpose of running a single test. If you are using a tool that
12 | # preloads Rails for running tests, you may have to set it to true.
13 | config.eager_load = false
14 |
15 | # Configure public file server for tests with Cache-Control for performance.
16 | config.public_file_server.enabled = true
17 | config.public_file_server.headers = {
18 | 'Cache-Control' => "public, max-age=#{1.hour.to_i}"
19 | }
20 |
21 | # Show full error reports and disable caching.
22 | config.consider_all_requests_local = true
23 | config.action_controller.perform_caching = false
24 |
25 | # Raise exceptions instead of rendering exception templates.
26 | config.action_dispatch.show_exceptions = false
27 |
28 | # Disable request forgery protection in test environment.
29 | config.action_controller.allow_forgery_protection = false
30 |
31 | # Store uploaded files on the local file system in a temporary directory
32 | config.active_storage.service = :test
33 |
34 | config.action_mailer.perform_caching = false
35 |
36 | # Tell Action Mailer not to deliver emails to the real world.
37 | # The :test delivery method accumulates sent emails in the
38 | # ActionMailer::Base.deliveries array.
39 | config.action_mailer.delivery_method = :test
40 |
41 | # Print deprecation notices to the stderr.
42 | config.active_support.deprecation = :stderr
43 |
44 | # Raises error for missing translations
45 | # config.action_view.raise_on_missing_translations = true
46 | end
47 |
--------------------------------------------------------------------------------
/app/controllers/unrestful/jobs_controller.rb:
--------------------------------------------------------------------------------
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 |
65 |
--------------------------------------------------------------------------------
/spec/dummy/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # In the development environment your application's code is reloaded on
5 | # every request. This slows down response time but is perfect for development
6 | # since you don't have to restart the web server when you make code changes.
7 | config.cache_classes = false
8 |
9 | # Do not eager load code on boot.
10 | config.eager_load = false
11 |
12 | config.preload_frameworks = true
13 | config.allow_concurrency = true
14 |
15 | # Show full error reports.
16 | config.consider_all_requests_local = true
17 |
18 | # Enable/disable caching. By default caching is disabled.
19 | # Run rails dev:cache to toggle caching.
20 | if Rails.root.join('tmp', 'caching-dev.txt').exist?
21 | config.action_controller.perform_caching = true
22 |
23 | config.cache_store = :memory_store
24 | config.public_file_server.headers = {
25 | 'Cache-Control' => "public, max-age=#{2.days.to_i}"
26 | }
27 | else
28 | config.action_controller.perform_caching = false
29 |
30 | config.cache_store = :null_store
31 | end
32 |
33 | # Store uploaded files on the local file system (see config/storage.yml for options)
34 | config.active_storage.service = :local
35 |
36 | # Don't care if the mailer can't send.
37 | config.action_mailer.raise_delivery_errors = false
38 |
39 | config.action_mailer.perform_caching = false
40 |
41 | # Print deprecation notices to the Rails logger.
42 | config.active_support.deprecation = :log
43 |
44 | # Raise an error on page load if there are pending migrations.
45 | config.active_record.migration_error = :page_load
46 |
47 | # Highlight code that triggered database queries in logs.
48 | config.active_record.verbose_query_logs = true
49 |
50 | # Debug mode disables concatenation and preprocessing of assets.
51 | # This option may cause significant delays in view rendering with a large
52 | # number of complex assets.
53 | config.assets.debug = true
54 |
55 | # Suppress logger output for asset requests.
56 | config.assets.quiet = true
57 |
58 | # Raises error for missing translations
59 | # config.action_view.raise_on_missing_translations = true
60 |
61 | # Use an evented file watcher to asynchronously detect changes in source code,
62 | # routes, locales, etc. This feature depends on the listen gem.
63 | # config.file_watcher = ActiveSupport::EventedFileUpdateChecker
64 | end
65 |
--------------------------------------------------------------------------------
/lib/unrestful/async_job.rb:
--------------------------------------------------------------------------------
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
112 |
--------------------------------------------------------------------------------
/app/controllers/unrestful/endpoints_controller.rb:
--------------------------------------------------------------------------------
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
105 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: .
3 | specs:
4 | unrestful (0.1.3)
5 | jwt (~> 2.2)
6 | rails (~> 5.2.0)
7 | redis (~> 4.1)
8 |
9 | GEM
10 | remote: https://rubygems.org/
11 | specs:
12 | actioncable (5.2.3)
13 | actionpack (= 5.2.3)
14 | nio4r (~> 2.0)
15 | websocket-driver (>= 0.6.1)
16 | actionmailer (5.2.3)
17 | actionpack (= 5.2.3)
18 | actionview (= 5.2.3)
19 | activejob (= 5.2.3)
20 | mail (~> 2.5, >= 2.5.4)
21 | rails-dom-testing (~> 2.0)
22 | actionpack (5.2.3)
23 | actionview (= 5.2.3)
24 | activesupport (= 5.2.3)
25 | rack (~> 2.0)
26 | rack-test (>= 0.6.3)
27 | rails-dom-testing (~> 2.0)
28 | rails-html-sanitizer (~> 1.0, >= 1.0.2)
29 | actionview (5.2.3)
30 | activesupport (= 5.2.3)
31 | builder (~> 3.1)
32 | erubi (~> 1.4)
33 | rails-dom-testing (~> 2.0)
34 | rails-html-sanitizer (~> 1.0, >= 1.0.3)
35 | activejob (5.2.3)
36 | activesupport (= 5.2.3)
37 | globalid (>= 0.3.6)
38 | activemodel (5.2.3)
39 | activesupport (= 5.2.3)
40 | activerecord (5.2.3)
41 | activemodel (= 5.2.3)
42 | activesupport (= 5.2.3)
43 | arel (>= 9.0)
44 | activestorage (5.2.3)
45 | actionpack (= 5.2.3)
46 | activerecord (= 5.2.3)
47 | marcel (~> 0.3.1)
48 | activesupport (5.2.3)
49 | concurrent-ruby (~> 1.0, >= 1.0.2)
50 | i18n (>= 0.7, < 2)
51 | minitest (~> 5.1)
52 | tzinfo (~> 1.1)
53 | arel (9.0.0)
54 | builder (3.2.3)
55 | byebug (11.0.1)
56 | concurrent-ruby (1.1.5)
57 | crass (1.0.4)
58 | erubi (1.8.0)
59 | globalid (0.4.2)
60 | activesupport (>= 4.2.0)
61 | i18n (1.6.0)
62 | concurrent-ruby (~> 1.0)
63 | jwt (2.2.1)
64 | loofah (2.2.3)
65 | crass (~> 1.0.2)
66 | nokogiri (>= 1.5.9)
67 | mail (2.7.1)
68 | mini_mime (>= 0.1.1)
69 | marcel (0.3.3)
70 | mimemagic (~> 0.3.2)
71 | method_source (0.9.2)
72 | mimemagic (0.3.3)
73 | mini_mime (1.0.2)
74 | mini_portile2 (2.4.0)
75 | minitest (5.11.3)
76 | nio4r (2.4.0)
77 | nokogiri (1.10.3)
78 | mini_portile2 (~> 2.4.0)
79 | puma (3.12.1)
80 | rack (2.0.7)
81 | rack-test (1.1.0)
82 | rack (>= 1.0, < 3)
83 | rails (5.2.3)
84 | actioncable (= 5.2.3)
85 | actionmailer (= 5.2.3)
86 | actionpack (= 5.2.3)
87 | actionview (= 5.2.3)
88 | activejob (= 5.2.3)
89 | activemodel (= 5.2.3)
90 | activerecord (= 5.2.3)
91 | activestorage (= 5.2.3)
92 | activesupport (= 5.2.3)
93 | bundler (>= 1.3.0)
94 | railties (= 5.2.3)
95 | sprockets-rails (>= 2.0.0)
96 | rails-dom-testing (2.0.3)
97 | activesupport (>= 4.2.0)
98 | nokogiri (>= 1.6)
99 | rails-html-sanitizer (1.1.0)
100 | loofah (~> 2.2, >= 2.2.2)
101 | railties (5.2.3)
102 | actionpack (= 5.2.3)
103 | activesupport (= 5.2.3)
104 | method_source
105 | rake (>= 0.8.7)
106 | thor (>= 0.19.0, < 2.0)
107 | rake (12.3.3)
108 | redis (4.1.2)
109 | sprockets (3.7.2)
110 | concurrent-ruby (~> 1.0)
111 | rack (> 1, < 3)
112 | sprockets-rails (3.2.1)
113 | actionpack (>= 4.0)
114 | activesupport (>= 4.0)
115 | sprockets (>= 3.0.0)
116 | sqlite3 (1.4.1)
117 | thor (0.20.3)
118 | thread_safe (0.3.6)
119 | tzinfo (1.2.5)
120 | thread_safe (~> 0.1)
121 | websocket-driver (0.7.1)
122 | websocket-extensions (>= 0.1.0)
123 | websocket-extensions (0.1.4)
124 |
125 | PLATFORMS
126 | ruby
127 |
128 | DEPENDENCIES
129 | byebug
130 | puma
131 | sqlite3
132 | unrestful!
133 |
134 | BUNDLED WITH
135 | 2.0.2
136 |
--------------------------------------------------------------------------------
/spec/dummy/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # Code is not reloaded between requests.
5 | config.cache_classes = true
6 |
7 | # Eager load code on boot. This eager loads most of Rails and
8 | # your application in memory, allowing both threaded web servers
9 | # and those relying on copy on write to perform better.
10 | # Rake tasks automatically ignore this option for performance.
11 | config.eager_load = true
12 |
13 | # Full error reports are disabled and caching is turned on.
14 | config.consider_all_requests_local = false
15 | config.action_controller.perform_caching = true
16 |
17 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
18 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
19 | # config.require_master_key = true
20 |
21 | # Disable serving static files from the `/public` folder by default since
22 | # Apache or NGINX already handles this.
23 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
24 |
25 | # Compress JavaScripts and CSS.
26 | config.assets.js_compressor = :uglifier
27 | # config.assets.css_compressor = :sass
28 |
29 | # Do not fallback to assets pipeline if a precompiled asset is missed.
30 | config.assets.compile = false
31 |
32 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
33 |
34 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
35 | # config.action_controller.asset_host = 'http://assets.example.com'
36 |
37 | # Specifies the header that your server uses for sending files.
38 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
39 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
40 |
41 | # Store uploaded files on the local file system (see config/storage.yml for options)
42 | config.active_storage.service = :local
43 |
44 | # Mount Action Cable outside main process or domain
45 | # config.action_cable.mount_path = nil
46 | # config.action_cable.url = 'wss://example.com/cable'
47 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
48 |
49 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
50 | # config.force_ssl = true
51 |
52 | # Use the lowest log level to ensure availability of diagnostic information
53 | # when problems arise.
54 | config.log_level = :debug
55 |
56 | # Prepend all log lines with the following tags.
57 | config.log_tags = [ :request_id ]
58 |
59 | # Use a different cache store in production.
60 | # config.cache_store = :mem_cache_store
61 |
62 | # Use a real queuing backend for Active Job (and separate queues per environment)
63 | # config.active_job.queue_adapter = :resque
64 | # config.active_job.queue_name_prefix = "dummy_#{Rails.env}"
65 |
66 | config.action_mailer.perform_caching = false
67 |
68 | # Ignore bad email addresses and do not raise email delivery errors.
69 | # Set this to true and configure the email server for immediate delivery to raise delivery errors.
70 | # config.action_mailer.raise_delivery_errors = false
71 |
72 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
73 | # the I18n.default_locale when a translation cannot be found).
74 | config.i18n.fallbacks = true
75 |
76 | # Send deprecation notices to registered listeners.
77 | config.active_support.deprecation = :notify
78 |
79 | # Use default logging formatter so that PID and timestamp are not suppressed.
80 | config.log_formatter = ::Logger::Formatter.new
81 |
82 | # Use a different logger for distributed setups.
83 | # require 'syslog/logger'
84 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
85 |
86 | if ENV["RAILS_LOG_TO_STDOUT"].present?
87 | logger = ActiveSupport::Logger.new(STDOUT)
88 | logger.formatter = config.log_formatter
89 | config.logger = ActiveSupport::TaggedLogging.new(logger)
90 | end
91 |
92 | # Do not dump schema after migrations.
93 | config.active_record.dump_schema_after_migration = false
94 | end
95 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
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).
138 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------