activerpc 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 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: []