activerpc 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 10ba3417ba62f8c6113543c46d4fc9f869a85c18a7fdc91160fc9a918043dc64
4
+ data.tar.gz: d59077929216fca8f7ab8067cfe643d7c72fdeca5f0aa359db955bd932a098d6
5
+ SHA512:
6
+ metadata.gz: 2abecebe8eea82db866c8e51789956fde73c34af67a51355e7c919b66259bd3e8eb036b920b4749f8e30c857f057aaad646d72956db9b2c1f987293c15204d51
7
+ data.tar.gz: 1a77d1a284b54598eab87f67c2bca9007946b059edbd47a94362c0691b2586a7769b74b6283ef5007541454d5269b8a2e644d52d6f6ebc6dfab75cbcb4b6e1a1
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2019 John Maxwell
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # ActiveRpc
2
+ Rails-native JSONRPC 2.0 Server Library
3
+
4
+ ## Usage
5
+ ActiveRpc is a simple mechanism to service JSONRPC requests from within
6
+ a Rails installation without sacrificing your existing Rails workflow.
7
+ It supports custom before/after/around actions on the RPC controller,
8
+ and insists upon validation of payloads for inbound method invocations,
9
+ using standard Rails tools and toolchains.
10
+
11
+ ## Installation
12
+ Add this line to your application's Gemfile:
13
+
14
+ ```ruby
15
+ gem 'activerpc'
16
+ ```
17
+
18
+ And then execute:
19
+ ```bash
20
+ $ bundle
21
+ ```
22
+
23
+ Or install it yourself as:
24
+ ```bash
25
+ $ gem install activerpc
26
+ ```
27
+
28
+ Once you've installed the gem, you can add an initializer to configure things
29
+ such as controller lifecycle actions.
30
+
31
+ ```ruby
32
+ ActiveRpc.configure do |config|
33
+ config.before_action = MyBeforeAction # something that responds to `.before`
34
+ end
35
+ ```
36
+
37
+ ## Usage
38
+ The JSONRPC spec supports two kinds of parameters being passed to a method. We
39
+ feel strongly that only the Hash/Object form should be used rather than the
40
+ Array/positional form. The reason for this is that it is significantly less
41
+ brittle as changes happen over time, and much easier to reason about what the job
42
+ is whilst in flight.
43
+
44
+ Further than that, we also believe that all invocations should be subject to
45
+ suitable validation before processing. So...
46
+
47
+ ```ruby
48
+ class MyRpcMethod < ActiveRpc::Operation
49
+ operation_name 'rpc.method.name'
50
+ fields :name, :age
51
+
52
+ validates :name, presence: true
53
+ validates :age, presence: true, numericality: true, only_integer: true, greater_than: 18
54
+
55
+ def call
56
+ "Hello #{name}, you are #{age} years old."
57
+ end
58
+ end
59
+ ```
60
+
61
+ ActiveRpc will instantiate your method class, and if it passes validation
62
+ it will then call `#call` on it, returning the outcome of that method as the
63
+ JSONRPC result field.
64
+
65
+ JSONRPC supports returning application-semantic errors, to trigger one of those
66
+ you should raise an error in your `#call` method.
67
+
68
+ ```ruby
69
+ def call
70
+ raise OperationFailure.new(code: my_code, message: 'no thanks') if name == 'Nigel'
71
+
72
+ "Hello #{name}, you are #{age} years old."
73
+ end
74
+ ```
75
+
76
+ Please note, in development mode you might have issues with the operation classes
77
+ being autoloaded. The new bootloader in Rails 6 will solve this, but until then you
78
+ can add something similar to the below to an initialiser.
79
+
80
+ ```ruby
81
+ # Require all operatons in app/operations as dependencies for autoloading
82
+ Rails.application.config.to_prepare do
83
+ Dir[Rails.root.join('app', 'operations', '*.rb')].each do |file|
84
+ require_dependency(file)
85
+ end
86
+ end
87
+ ```
88
+
89
+ ## Contributing
90
+ Fork the project, make some changes and open a PR! For major changes of direction
91
+ please talk to us first, we can't guarantee to merge PRs that don't reflect the
92
+ usage we have of this project.
93
+
94
+ ## License
95
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,32 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Activerpc'
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("test/dummy/Rakefile", __dir__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+ load 'rails/tasks/statistics.rake'
21
+
22
+ require 'bundler/gem_tasks'
23
+
24
+ require 'rake/testtask'
25
+
26
+ Rake::TestTask.new(:test) do |t|
27
+ t.libs << 'test'
28
+ t.pattern = 'test/**/*_test.rb'
29
+ t.verbose = false
30
+ end
31
+
32
+ task default: :test
@@ -0,0 +1,56 @@
1
+ module ActiveRpc
2
+ class RpcController < ActionController::Base
3
+ after_action ActiveRpc.config.after_action
4
+ around_action ActiveRpc.config.around_action
5
+ before_action ActiveRpc.config.before_action
6
+
7
+ def create
8
+ res = nil
9
+ begin
10
+ res = case body
11
+ when Array then body.map(&method(:process_item)).map(&:to_hash)
12
+ when Hash then process_item(body).to_hash
13
+ end
14
+ rescue JSON::ParserError => ex
15
+ res = ActiveRpc::Response.new do |r|
16
+ r.error = Errors::ParseError.new(message: ex.to_s)
17
+ end.to_hash
18
+ end
19
+
20
+ render status: :ok, json: res
21
+ end
22
+
23
+ private def process_item(item)
24
+ req = ActiveRpc::Request.new(id: item['id'], method: item['method'], params: item['params'])
25
+ res = ActiveRpc::Response.from_request(req)
26
+
27
+ begin
28
+ raise TypeError, 'invalid JSON-RPC request' unless req.valid?
29
+
30
+ executor = ActiveRpc.get_executor(req.method)
31
+ raise NoMethodError, "undefined operation `#{req.method}'" unless executor
32
+
33
+ ex = executor.new(req.params)
34
+ raise ArgumentError, 'invalid payload' unless ex.valid?
35
+ res.result = ex.call
36
+ rescue TypeError => ex
37
+ res.error = Errors::ClientError.new(message: ex.to_s)
38
+ rescue NoMethodError => ex
39
+ res.error = Errors::NoMethodError.new(message: ex.to_s)
40
+ rescue ArgumentError => ex
41
+ res.error = Errors::ArgumentError.new(message: ex.to_s)
42
+ rescue OperationFailure => ex
43
+ res.error = ex.rpc_error
44
+ # todo: handle rpc-level errors
45
+ rescue => ex
46
+ res.error = Errors::InternalError.new(message: ex.to_s)
47
+ end
48
+
49
+ res
50
+ end
51
+
52
+ private def body
53
+ @body ||= JSON.load(request.body.read)
54
+ end
55
+ end
56
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,3 @@
1
+ ActiveRpc::Engine.routes.draw do
2
+ post '/', to: 'rpc#create'
3
+ end
@@ -0,0 +1,5 @@
1
+ module ActiveRpc
2
+ class Configuration
3
+ attr_accessor :before_action, :after_action, :around_action
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module ActiveRpc
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace ActiveRpc
4
+ end
5
+ end
@@ -0,0 +1,77 @@
1
+ module ActiveRpc
2
+ class OperationFailure < RuntimeError
3
+ attr_reader :code, :message, :data
4
+
5
+ def initialize(code:, message: nil, data: nil)
6
+ @code = code
7
+ @message = message
8
+ @data = data
9
+ end
10
+
11
+ def rpc_error
12
+ Errors::OperationFailure.new(code: code, message: message, data: data)
13
+ end
14
+ end
15
+
16
+ class Error
17
+ attr_accessor :message, :data
18
+ attr_reader :code
19
+ def initialize(code:, data: nil, message: nil)
20
+ @message = message
21
+ @code = code
22
+ @data = data
23
+ end
24
+
25
+ def to_hash
26
+ {
27
+ 'code' => code,
28
+ 'data' => data,
29
+ 'message' => message
30
+ }.compact
31
+ end
32
+ end
33
+
34
+ module Errors
35
+ class ClientError < Error
36
+ def initialize(message:)
37
+ super(message: message, code: -32600)
38
+ end
39
+ end
40
+
41
+ class NoMethodError < Error
42
+ def initialize(message:)
43
+ super(message: message, code: -32601)
44
+ end
45
+ end
46
+
47
+ class ParseError < Error
48
+ def initialize(message:)
49
+ super(message: message, code: -32700)
50
+ end
51
+ end
52
+
53
+ class ArgumentError < Error
54
+ def initialize(message:)
55
+ super(message: message, code: -32602)
56
+ end
57
+ end
58
+
59
+ class InternalError < Error
60
+ def initialize(message:)
61
+ super(message: message, code: -32603)
62
+ end
63
+ end
64
+
65
+ class ServerError < Error
66
+ def initialize(message:)
67
+ super(message: message, code: -32000)
68
+ end
69
+ end
70
+
71
+ class OperationFailure < Error
72
+ def initialize(code:, message:, data: nil)
73
+ super(message: message, code: code, data: data)
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,21 @@
1
+ module ActiveRpc
2
+ class Operation
3
+ include ActiveModel::Model
4
+ include ActiveModel::Validations
5
+ include ActiveModel::Validations::Callbacks
6
+
7
+ class << self
8
+ def fields(*names)
9
+ attr_accessor(*names)
10
+ end
11
+
12
+ def operation_name(value)
13
+ ActiveRpc.operation_map[value] = self
14
+ end
15
+ end
16
+
17
+ def call
18
+ raise 'Must Implement'
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ module ActiveRpc
2
+ class Request
3
+ attr_reader :id, :method, :params
4
+
5
+ def initialize(id:, method:, params:)
6
+ @id, @method, @params = id, method, params
7
+ end
8
+
9
+ def valid?
10
+ [
11
+ id.present?,
12
+ id.is_a?(String) || id.is_a?(Integer),
13
+ method.present?,
14
+ method.is_a?(String),
15
+ params.nil? || params.is_a?(Array) || params.is_a?(Hash),
16
+ ].all?
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,27 @@
1
+ module ActiveRpc
2
+ class Response
3
+ attr_accessor :error, :result
4
+ attr_reader :id
5
+
6
+ def initialize(id:)
7
+ @id = id
8
+ end
9
+
10
+ def to_hash
11
+ {
12
+ 'jsonrpc' => '2.0',
13
+ 'id' => id
14
+ }.tap do |h|
15
+ if error
16
+ h['error'] = error.to_hash
17
+ else
18
+ h['result'] = result
19
+ end
20
+ end
21
+ end
22
+
23
+ def self.from_request(req)
24
+ new(id: req.id)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,3 @@
1
+ module ActiveRpc
2
+ VERSION = '0.1.0'
3
+ end
data/lib/active_rpc.rb ADDED
@@ -0,0 +1,24 @@
1
+ require 'active_rpc/engine'
2
+ require 'active_rpc/errors'
3
+ require 'active_rpc/configuration'
4
+ require 'active_rpc/operation'
5
+ require 'active_rpc/request'
6
+ require 'active_rpc/response'
7
+
8
+ module ActiveRpc
9
+ cattr_accessor :config
10
+
11
+ def self.configure
12
+ self.config ||= Configuration.new
13
+ yield(config)
14
+ end
15
+
16
+
17
+ def self.get_executor(method)
18
+ operation_map[method]
19
+ end
20
+
21
+ def self.operation_map
22
+ @operation_map ||= {}
23
+ end
24
+ end
data/lib/activerpc.rb ADDED
@@ -0,0 +1 @@
1
+ require "active_rpc"
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activerpc
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - John Maxwell
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-03-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 4.2.0
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '7.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: 4.2.0
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '7.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: sqlite3
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.3'
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 1.3.6
43
+ type: :development
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '1.3'
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 1.3.6
53
+ description: Simple, dependable JSONRpc 2.0 Server library for Rails
54
+ email:
55
+ - john@musicglue.com
56
+ executables: []
57
+ extensions: []
58
+ extra_rdoc_files: []
59
+ files:
60
+ - MIT-LICENSE
61
+ - README.md
62
+ - Rakefile
63
+ - app/controllers/active_rpc/rpc_controller.rb
64
+ - config/routes.rb
65
+ - lib/active_rpc.rb
66
+ - lib/active_rpc/configuration.rb
67
+ - lib/active_rpc/engine.rb
68
+ - lib/active_rpc/errors.rb
69
+ - lib/active_rpc/operation.rb
70
+ - lib/active_rpc/request.rb
71
+ - lib/active_rpc/response.rb
72
+ - lib/active_rpc/version.rb
73
+ - lib/activerpc.rb
74
+ homepage: https://github.com/musicglue/activerpc
75
+ licenses:
76
+ - MIT
77
+ metadata: {}
78
+ post_install_message:
79
+ rdoc_options: []
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ requirements: []
93
+ rubyforge_project:
94
+ rubygems_version: 2.7.6
95
+ signing_key:
96
+ specification_version: 4
97
+ summary: JSONRpc 2.0 Server library for Rails
98
+ test_files: []