carrot_rpc 0.2.3.pre
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 +7 -0
- data/.gitignore +50 -0
- data/.rspec +2 -0
- data/.rubocop.yml +35 -0
- data/.travis.yml +3 -0
- data/CHANGELOG.md +149 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +9 -0
- data/LICENSE +22 -0
- data/LICENSE.txt +21 -0
- data/README.md +172 -0
- data/Rakefile +7 -0
- data/bin/carrot_rpc +19 -0
- data/bin/console +20 -0
- data/bin/setup +7 -0
- data/carrot_rpc.gemspec +49 -0
- data/circle.yml +8 -0
- data/lib/carrot_rpc/cli.rb +110 -0
- data/lib/carrot_rpc/client_server.rb +25 -0
- data/lib/carrot_rpc/configuration.rb +17 -0
- data/lib/carrot_rpc/error/code.rb +17 -0
- data/lib/carrot_rpc/error.rb +43 -0
- data/lib/carrot_rpc/hash_extensions.rb +22 -0
- data/lib/carrot_rpc/rpc_client.rb +117 -0
- data/lib/carrot_rpc/rpc_server/jsonapi_resources/actions.rb +108 -0
- data/lib/carrot_rpc/rpc_server/jsonapi_resources.rb +172 -0
- data/lib/carrot_rpc/rpc_server.rb +77 -0
- data/lib/carrot_rpc/server_runner/autoload_rails.rb +41 -0
- data/lib/carrot_rpc/server_runner/logger.rb +31 -0
- data/lib/carrot_rpc/server_runner/pid.rb +142 -0
- data/lib/carrot_rpc/server_runner/signals.rb +21 -0
- data/lib/carrot_rpc/server_runner.rb +156 -0
- data/lib/carrot_rpc/tagged_log.rb +24 -0
- data/lib/carrot_rpc/version.rb +3 -0
- data/lib/carrot_rpc.rb +46 -0
- data/logs/.gitkeep +0 -0
- metadata +182 -0
@@ -0,0 +1,110 @@
|
|
1
|
+
# Command-line interface for {CarrotRpc}
|
2
|
+
module CarrotRpc::CLI
|
3
|
+
def self.add_common_options(option_parser)
|
4
|
+
option_parser.separator ""
|
5
|
+
|
6
|
+
option_parser.separator "Common options:"
|
7
|
+
option_parser.on("-h", "--help") do
|
8
|
+
puts option_parser.to_s
|
9
|
+
exit
|
10
|
+
end
|
11
|
+
|
12
|
+
option_parser.on("-v", "--version") do
|
13
|
+
puts CarrotRpc::VERSION
|
14
|
+
exit
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# There are just too many options in the Process options category and they can't really be broken down more
|
19
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
20
|
+
|
21
|
+
# Add "Process options" to `option_parser`.
|
22
|
+
#
|
23
|
+
# @param option_parser [OptionParser]
|
24
|
+
# @return [OptionParser]
|
25
|
+
def self.add_process_options(option_parser)
|
26
|
+
option_parser.separator ""
|
27
|
+
|
28
|
+
option_parser.separator "Process options:"
|
29
|
+
option_parser.on("-d", "--daemonize", "run daemonized in the background (default: false)") do
|
30
|
+
CarrotRpc.configuration.daemonize = true
|
31
|
+
end
|
32
|
+
|
33
|
+
option_parser.on(" ", "--pidfile PIDFILE", "the pid filename") do |value|
|
34
|
+
CarrotRpc.configuration.pidfile = value
|
35
|
+
end
|
36
|
+
|
37
|
+
option_parser.on("-s", "--runloop_sleep VALUE", Float, "Configurable sleep time in the runloop") do |value|
|
38
|
+
CarrotRpc.configuration.runloop_sleep = value
|
39
|
+
end
|
40
|
+
|
41
|
+
option_parser.on(
|
42
|
+
" ",
|
43
|
+
"--autoload_rails value",
|
44
|
+
"loads rails env by default. Uses Rails Logger by default."
|
45
|
+
) do |value|
|
46
|
+
pv = value == "false" ? false : true
|
47
|
+
CarrotRpc.configuration.autoload_rails = pv
|
48
|
+
end
|
49
|
+
|
50
|
+
option_parser.on(" ", "--logfile VALUE", "relative path and name for Log file. Overrides Rails logger.") do |value|
|
51
|
+
CarrotRpc.configuration.logfile = File.expand_path("../../#{value}", __FILE__)
|
52
|
+
end
|
53
|
+
|
54
|
+
option_parser.on(
|
55
|
+
" ",
|
56
|
+
"--loglevel VALUE",
|
57
|
+
"levels of loggin: DEBUG < INFO < WARN < ERROR < FATAL < UNKNOWN"
|
58
|
+
) do |value|
|
59
|
+
CarrotRpc.configuration.loglevel = Logger.const_get(value) || 0
|
60
|
+
end
|
61
|
+
|
62
|
+
# Optional. Defaults to using the ENV['RABBITMQ_URL']
|
63
|
+
option_parser.on(
|
64
|
+
" ",
|
65
|
+
"--rabbitmq_url VALUE",
|
66
|
+
"connection string to RabbitMQ 'amqp://user:pass@host:10000/vhost'"
|
67
|
+
) do |value|
|
68
|
+
CarrotRpc.configuration.bunny = Bunny.new(value)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.add_ruby_options(option_parser)
|
73
|
+
option_parser.separator ""
|
74
|
+
|
75
|
+
option_parser.separator "Ruby options:"
|
76
|
+
|
77
|
+
option_parser.on("-I", "--include PATH", "an additional $LOAD_PATH") do |value|
|
78
|
+
$LOAD_PATH.unshift(*value.split(":").map { |v| File.expand_path(v) })
|
79
|
+
end
|
80
|
+
|
81
|
+
option_parser.on("--debug", "set $DEBUG to true") do
|
82
|
+
$DEBUG = true
|
83
|
+
end
|
84
|
+
|
85
|
+
option_parser.on("--warn", "enable warnings") do
|
86
|
+
$-w = true
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
91
|
+
|
92
|
+
def self.option_parser
|
93
|
+
option_parser = OptionParser.new
|
94
|
+
option_parser.banner = "RPC Server Runner for RabbitMQ RPC Services."
|
95
|
+
option_parser.separator ""
|
96
|
+
option_parser.separator "Usage: server [options]"
|
97
|
+
|
98
|
+
add_process_options(option_parser)
|
99
|
+
add_ruby_options(option_parser)
|
100
|
+
add_common_options(option_parser)
|
101
|
+
|
102
|
+
option_parser.separator ""
|
103
|
+
|
104
|
+
option_parser
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.parse_options(args = ARGV)
|
108
|
+
option_parser.parse!(args)
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# Common functionality for Client and Server.
|
2
|
+
module CarrotRpc::ClientServer
|
3
|
+
# @overload queue_name(new_name)
|
4
|
+
# @note Default naming not performed. Class must pass queue name.
|
5
|
+
#
|
6
|
+
# Allows for class level definition of queue name.
|
7
|
+
#
|
8
|
+
# @param new_name [String] the queue name for the class.
|
9
|
+
# @return [String] `new_name`
|
10
|
+
#
|
11
|
+
# @overload queue_name
|
12
|
+
# The current queue name previously set with `#queue_name(new_name)`.
|
13
|
+
#
|
14
|
+
# @return [String]
|
15
|
+
def queue_name(*args)
|
16
|
+
if args.length == 0
|
17
|
+
@queue_name
|
18
|
+
elsif args.length == 1
|
19
|
+
@queue_name = args[0]
|
20
|
+
else
|
21
|
+
fail ArgumentError,
|
22
|
+
"queue_name(new_name) :: new_name or queue_name() :: current_name are the only ways to call queue_name"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# Global configuration for {CarrotRpc}. Access with {CarrotRpc.configuration}.
|
2
|
+
class CarrotRpc::Configuration
|
3
|
+
attr_accessor :logger, :logfile, :loglevel, :daemonize, :pidfile, :runloop_sleep, :autoload_rails, :bunny
|
4
|
+
|
5
|
+
# logfile - set logger to a file. overrides rails logger.
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@logfile = nil
|
9
|
+
@loglevel = Logger::DEBUG
|
10
|
+
@logger = nil
|
11
|
+
@daemonize = false
|
12
|
+
@pidfile = nil
|
13
|
+
@runloop_sleep = 0
|
14
|
+
@autoload_rails = true
|
15
|
+
@bunny = nil
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# An enum of predefined {RpcServer::Server#code}
|
2
|
+
module CarrotRpc::Error::Code
|
3
|
+
# Internal JSON-RPC error.
|
4
|
+
INTERNAL_ERROR = -32_603
|
5
|
+
|
6
|
+
# Invalid method parameter(s).
|
7
|
+
INVALID_PARAMS = -32_602
|
8
|
+
|
9
|
+
# The JSON sent is not a valid Request object.
|
10
|
+
INVALID_REQUEST = -32_600
|
11
|
+
|
12
|
+
# The method does not exist / is not available.
|
13
|
+
METHOD_NOT_FOUND = -32_601
|
14
|
+
|
15
|
+
# Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text.
|
16
|
+
PARSE_ERROR = -32_700
|
17
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# Error raised by an {RpcServer} method to signal that a
|
2
|
+
# {http://www.jsonrpc.org/specification#error_object JSON RPC 2.0 Response Error object} should be the reply.
|
3
|
+
class CarrotRpc::Error < StandardError
|
4
|
+
autoload :Code, "carrot_rpc/error/code"
|
5
|
+
|
6
|
+
# @return [Integer]A Number that indicates the error type that occurred. Some codes are
|
7
|
+
# {http://www.jsonrpc.org/specification#error_object predefined}.
|
8
|
+
attr_reader :code
|
9
|
+
|
10
|
+
# @return [Object, nil] A Primitive or Structured value that contains additional information about the error.
|
11
|
+
# This may be omitted. The value of this member is defined by the Server (e.g. detailed error information,
|
12
|
+
# nested errors etc.).
|
13
|
+
attr_reader :data
|
14
|
+
|
15
|
+
# @param code [Integer] A Number that indicates the error type that occurred. Favor using the
|
16
|
+
# {http://www.jsonrpc.org/specification#error_object predefined codes}.
|
17
|
+
# @param message [String] A String providing a short description of the error. The message SHOULD be limited to a
|
18
|
+
# concise single sentence.
|
19
|
+
# @param data [Object, nil] A Primitive or Structured value that contains additional information about the error.
|
20
|
+
# This may be omitted. The value of this member is defined by the Server (e.g. detailed error information,
|
21
|
+
# nested errors etc.).
|
22
|
+
def initialize(code:, message:, data: nil)
|
23
|
+
@code = code
|
24
|
+
@data = data
|
25
|
+
super(message)
|
26
|
+
end
|
27
|
+
|
28
|
+
# A properly formatted {http://www.jsonrpc.org/specification#error_object JSON RPC Error object}.
|
29
|
+
#
|
30
|
+
# @return [Hash{code: String, message: String}, Hash{code: String, data: Object, message: String}]
|
31
|
+
def serialized_message
|
32
|
+
serialized = {
|
33
|
+
code: code,
|
34
|
+
message: message
|
35
|
+
}
|
36
|
+
|
37
|
+
if data
|
38
|
+
serialized[:data] = data
|
39
|
+
end
|
40
|
+
|
41
|
+
serialized
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# Refine the Hash class with new methods and functionality.
|
2
|
+
module CarrotRpc::HashExtensions
|
3
|
+
refine Hash do
|
4
|
+
# Utility method to rename keys in a hash
|
5
|
+
# @param [String] find the text to look for in a keys
|
6
|
+
# @param [String] replace the text to replace the found text
|
7
|
+
# @return [Hash] a new hash
|
8
|
+
def rename_keys(find, replace, new_hash = {})
|
9
|
+
each do |k, v|
|
10
|
+
new_key = k.to_s.gsub(find, replace)
|
11
|
+
|
12
|
+
new_hash[new_key] = if v.is_a? Hash
|
13
|
+
v.rename_keys(find, replace)
|
14
|
+
else
|
15
|
+
v
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
new_hash
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require "securerandom"
|
2
|
+
|
3
|
+
# Generic class for all RPC Consumers. Use as a base class to build other RPC Consumers for related functionality.
|
4
|
+
# Let's define a naming convention here for subclasses becuase I don't want to write a Confluence doc.
|
5
|
+
# All subclasses should have the following naming convention: <Name>RpcConsumer ex: PostRpcConsumer
|
6
|
+
class CarrotRpc::RpcClient
|
7
|
+
using CarrotRpc::HashExtensions
|
8
|
+
|
9
|
+
attr_reader :channel, :server_queue, :logger
|
10
|
+
|
11
|
+
extend CarrotRpc::ClientServer
|
12
|
+
|
13
|
+
# Use defaults for application level connection to RabbitMQ
|
14
|
+
# All RPC data goes over the same queue. I think that's ok....
|
15
|
+
def initialize(config: nil)
|
16
|
+
config ||= CarrotRpc.configuration
|
17
|
+
@channel = config.bunny.create_channel
|
18
|
+
@logger = config.logger
|
19
|
+
# auto_delete => false keeps the queue around until RabbitMQ restarts or explicitly deleted
|
20
|
+
@server_queue = @channel.queue(self.class.queue_name, auto_delete: false)
|
21
|
+
|
22
|
+
# Setup a direct exchange.
|
23
|
+
@exchange = @channel.default_exchange
|
24
|
+
end
|
25
|
+
|
26
|
+
# Starts the connection to listen for messages.
|
27
|
+
def start
|
28
|
+
# Empty queue name ends up creating a randomly named queue by RabbitMQ
|
29
|
+
# Exclusive => queue will be deleted when connection closes. Allows for automatic "cleanup".
|
30
|
+
@reply_queue = @channel.queue("", exclusive: true)
|
31
|
+
|
32
|
+
# setup a hash for results with a Queue object as a value
|
33
|
+
@results = Hash.new { |h, k| h[k] = Queue.new }
|
34
|
+
|
35
|
+
# setup subscribe block to Service
|
36
|
+
# block => false is a non blocking IO option.
|
37
|
+
@reply_queue.subscribe(block: false) do |_delivery_info, properties, payload|
|
38
|
+
result = JSON.parse(payload).rename_keys("-", "_").with_indifferent_access
|
39
|
+
@results[properties[:correlation_id]].push(result[:result])
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# params is an array of method argument values
|
44
|
+
# programmer implementing this class must know about the remote service
|
45
|
+
# the remote service must have documented the methods and arguments in order for this pattern to work.
|
46
|
+
# TODO: change to a hash to account for keyword arguments???
|
47
|
+
#
|
48
|
+
# @param remote_method [String, Symbol] the method to be called on current receiver
|
49
|
+
# @param params [Hash] the arguments for the method being called.
|
50
|
+
# @return [Object] the result of the method call.
|
51
|
+
def remote_call(remote_method, params)
|
52
|
+
correlation_id = SecureRandom.uuid
|
53
|
+
publish(correlation_id: correlation_id, method: remote_method, params: params.rename_keys("_", "-"))
|
54
|
+
wait_for_result(correlation_id)
|
55
|
+
end
|
56
|
+
|
57
|
+
def wait_for_result(correlation_id)
|
58
|
+
# `pop` is `Queue#pop`, so it is blocking on the receiving thread and this must happend before the `Hash.delete` or
|
59
|
+
# the receiving thread won't be able to find the correlation_id in @results
|
60
|
+
result = @results[correlation_id].pop
|
61
|
+
@results.delete correlation_id # remove item from hash. prevents memory leak.
|
62
|
+
result
|
63
|
+
end
|
64
|
+
|
65
|
+
def publish(correlation_id:, method:, params:)
|
66
|
+
message = message(
|
67
|
+
correlation_id: correlation_id,
|
68
|
+
params: params,
|
69
|
+
method: method
|
70
|
+
)
|
71
|
+
# Reply To => make sure the service knows where to send it's response.
|
72
|
+
# Correlation ID => identify the results that belong to the unique call made
|
73
|
+
@exchange.publish(message.to_json, routing_key: @server_queue.name, correlation_id: correlation_id,
|
74
|
+
reply_to: @reply_queue.name)
|
75
|
+
end
|
76
|
+
|
77
|
+
def message(correlation_id:, method:, params:)
|
78
|
+
{
|
79
|
+
id: correlation_id,
|
80
|
+
jsonrpc: "2.0",
|
81
|
+
method: method,
|
82
|
+
params: params.except(:controller, :action)
|
83
|
+
}
|
84
|
+
end
|
85
|
+
|
86
|
+
# Convience method as a resource alias for index action.
|
87
|
+
# To customize, override the method in your class.
|
88
|
+
#
|
89
|
+
# @param params [Hash] the arguments for the method being called.
|
90
|
+
def index(params)
|
91
|
+
remote_call("index", params)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Convience method as a resource alias for show action.
|
95
|
+
# To customize, override the method in your class.
|
96
|
+
#
|
97
|
+
# @param params [Hash] the arguments for the method being called.
|
98
|
+
def show(params)
|
99
|
+
remote_call("show", params)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Convience method as a resource alias for create action.
|
103
|
+
# To customize, override the method in your class.
|
104
|
+
#
|
105
|
+
# @param params [Hash] the arguments for the method being called.
|
106
|
+
def create(params)
|
107
|
+
remote_call("create", params)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Convience method as a resource alias for update action.
|
111
|
+
# To customize, override the method in your class.
|
112
|
+
#
|
113
|
+
# @param params [Hash] the arguments for the method being called.
|
114
|
+
def update(params)
|
115
|
+
remote_call("update", params)
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# The common CRUD actions for {CarrotRpc::RpcServer::JSONAPIResources}
|
2
|
+
module CarrotRpc::RpcServer::JSONAPIResources::Actions
|
3
|
+
#
|
4
|
+
# CONSTANTS
|
5
|
+
#
|
6
|
+
|
7
|
+
# Set of allowed actions for JSONAPI::Resources
|
8
|
+
NAME_SET = Set.new(
|
9
|
+
[
|
10
|
+
# Mimic behaviour of `POST <collection>` routes
|
11
|
+
:create,
|
12
|
+
# Mimic behaviour of `POST <collection>/<id>/relationships/<relation>` routes
|
13
|
+
:create_relationship,
|
14
|
+
# Mimic behavior of `DELETE <collection>/<id>` routes
|
15
|
+
:destroy,
|
16
|
+
# Mimic behavior of `DELETE <collection>/<id>/relationships/<relationship>` routes
|
17
|
+
:destroy_relationship,
|
18
|
+
# Mimics behavior of `GET <collection>/<id>/<relationship>` routes
|
19
|
+
:get_related_resource,
|
20
|
+
# Mimic behavior of `GET <collection>` routes
|
21
|
+
:index,
|
22
|
+
# Mimic behavior of `GET <collection>/<id>` routes
|
23
|
+
:show,
|
24
|
+
# Mimic behavior of `GET <collection>/<id>/relationships/<relationship>` routes
|
25
|
+
:show_relationship,
|
26
|
+
# Mimic behavior of `PATCH|PUT <collection>/<id>` routes
|
27
|
+
:update,
|
28
|
+
# Mimic behavior of `PATCH|PUT <collection>/<id>/relationships/<relationship>` routes
|
29
|
+
:update_relationship
|
30
|
+
]
|
31
|
+
).freeze
|
32
|
+
|
33
|
+
#
|
34
|
+
# Module Methods
|
35
|
+
#
|
36
|
+
|
37
|
+
# Defines an action method, `name` on `action_module`.
|
38
|
+
#
|
39
|
+
# @param action_module [Module] Module where action methods are defined so that they can be called with `super` if
|
40
|
+
# overridden.
|
41
|
+
# @param name [Symbol] an element of `NAME_SET`.
|
42
|
+
# @return [void]
|
43
|
+
def self.define_action_method(action_module, name)
|
44
|
+
action_module.send(:define_method, name) do |params|
|
45
|
+
process_request_params(
|
46
|
+
ActionController::Parameters.new(
|
47
|
+
params.merge(
|
48
|
+
action: name,
|
49
|
+
controller: controller
|
50
|
+
)
|
51
|
+
)
|
52
|
+
)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
#
|
57
|
+
# Instance Methods
|
58
|
+
#
|
59
|
+
|
60
|
+
# Adds actions in `names` to the current class.
|
61
|
+
#
|
62
|
+
# The actions are added to a mixin module `self::Actions`, so that the action methods can be overridden and `super`
|
63
|
+
# will work.
|
64
|
+
#
|
65
|
+
# @example Adding only show actions
|
66
|
+
# extend RpcServer::JSONAPIResources::Actions
|
67
|
+
# include RpcServer::JSONAPIResources
|
68
|
+
#
|
69
|
+
# actions :create,
|
70
|
+
# :destroy,
|
71
|
+
# :index,
|
72
|
+
# :show,
|
73
|
+
# :update
|
74
|
+
#
|
75
|
+
# @param names [Array<Symbol>] a array of a subset of {NAME_SET}.
|
76
|
+
# @return [void]
|
77
|
+
# @raise (see #valid_actions)
|
78
|
+
def actions(*names)
|
79
|
+
valid_actions!(names)
|
80
|
+
|
81
|
+
# an include module so that `super` works if the method is overridden
|
82
|
+
action_module = Module.new
|
83
|
+
|
84
|
+
names.each do |name|
|
85
|
+
CarrotRpc::RpcServer::JSONAPIResources::Actions.define_action_method(action_module, name)
|
86
|
+
end
|
87
|
+
|
88
|
+
const_set(:Actions, action_module)
|
89
|
+
|
90
|
+
include action_module
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
# Checks that all `names` are valid action names.
|
96
|
+
#
|
97
|
+
# @raise [ArgumentError] if any element of `names` is not an element of `NAME_SET`.
|
98
|
+
def valid_actions!(names)
|
99
|
+
given_name_set = Set.new(names)
|
100
|
+
unknown_name_set = given_name_set - NAME_SET
|
101
|
+
|
102
|
+
unless unknown_name_set.empty?
|
103
|
+
fail ArgumentError,
|
104
|
+
"#{unknown_name_set.to_a.sort.to_sentence} are not elements of known actions " \
|
105
|
+
"(#{NAME_SET.to_a.sort.to_sentence})"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Allows a {CarrotRpc::RpcServer} subclass to behave the same as a controller that does
|
4
|
+
# `include JSONAPI::ActsAsResourceController`
|
5
|
+
module CarrotRpc::RpcServer::JSONAPIResources
|
6
|
+
autoload :Actions, "carrot_rpc/rpc_server/jsonapi_resources/actions"
|
7
|
+
|
8
|
+
# The base "meta" to include in the top-level of all JSON API documents.
|
9
|
+
#
|
10
|
+
# @param request [JSONAPI::Request] the current request. `JSONAPI::Request#warnings` are merged into the
|
11
|
+
# {#base_response_meta}.
|
12
|
+
# @return [Hash]
|
13
|
+
def base_meta(request)
|
14
|
+
if request.nil? || request.warnings.empty?
|
15
|
+
base_response_meta
|
16
|
+
else
|
17
|
+
base_response_meta.merge(warnings: request.warnings)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# The base "links" to include the top-level of all JSON API documents before any operation result links are added.
|
22
|
+
#
|
23
|
+
# @return [Hash] Defaults to `{}`.
|
24
|
+
def base_response_links
|
25
|
+
{}
|
26
|
+
end
|
27
|
+
|
28
|
+
# The base "meta" to include in the top-level of all JSON API documents before being merged with any request warnings
|
29
|
+
# in {#base_meta}.
|
30
|
+
#
|
31
|
+
# @return [Hash] Defaults to `{}`.
|
32
|
+
def base_response_meta
|
33
|
+
{}
|
34
|
+
end
|
35
|
+
|
36
|
+
# The operations processor in the configuration or override this to use another operations processor
|
37
|
+
#
|
38
|
+
# @return [JSONAPI::OperationsProcessor]
|
39
|
+
def create_operations_processor
|
40
|
+
JSONAPI.configuration.operations_processor.new
|
41
|
+
end
|
42
|
+
|
43
|
+
# The JSON API Document for the `operation_results` and `request`.
|
44
|
+
#
|
45
|
+
# @param operation_results [JSONAPI::OperationResults] The result of processing the `request`.
|
46
|
+
# @param request [JSONAPI::Request] the request to respond to.
|
47
|
+
# @return [JSONAPI::ResponseDocument]
|
48
|
+
def create_response_document(operation_results:, request:) # rubocop:disable Metrics/MethodLength
|
49
|
+
JSONAPI::ResponseDocument.new(
|
50
|
+
operation_results,
|
51
|
+
primary_resource_klass: resource_klass,
|
52
|
+
include_directives: request ? request.include_directives : nil,
|
53
|
+
fields: request ? request.fields : nil,
|
54
|
+
base_url: base_url,
|
55
|
+
key_formatter: key_formatter,
|
56
|
+
route_formatter: route_formatter,
|
57
|
+
base_meta: base_meta(request),
|
58
|
+
base_links: base_response_links,
|
59
|
+
resource_serializer_klass: resource_serializer_klass,
|
60
|
+
request: request,
|
61
|
+
serialization_options: serialization_options
|
62
|
+
)
|
63
|
+
end
|
64
|
+
|
65
|
+
# @note Override this to process other exceptions. Be sure to either call `super(exception)` or handle
|
66
|
+
# `JSONAPI::Exceptions::Error` and `raise` unhandled exceptions.
|
67
|
+
#
|
68
|
+
# @param exception [Exception] the original, exception that was caught
|
69
|
+
# @param request [JSONAPI::Request] the request that triggered the `exception`
|
70
|
+
# @return (see #render_errors)
|
71
|
+
def handle_exceptions(exception, request:)
|
72
|
+
case exception
|
73
|
+
when JSONAPI::Exceptions::Error
|
74
|
+
render_errors(exception.errors, request: request)
|
75
|
+
else
|
76
|
+
internal_server_error = JSONAPI::Exceptions::InternalServerError.new(exception)
|
77
|
+
logger.error { # rubocop:disable Style/BlockDelimiters
|
78
|
+
"Internal Server Error: #{exception.message} #{exception.backtrace.join("\n")}"
|
79
|
+
}
|
80
|
+
render_errors(internal_server_error.errors)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# @note Override if you want to set a per controller key format.
|
85
|
+
#
|
86
|
+
# Control by setting in an initializer:
|
87
|
+
# JSONAPI.configuration.json_key_format = :camelized_key
|
88
|
+
#
|
89
|
+
# @return [JSONAPI::KeyFormatter]
|
90
|
+
def key_formatter
|
91
|
+
JSONAPI.configuration.key_formatter
|
92
|
+
end
|
93
|
+
|
94
|
+
# Processes the params as a request and renders a response.
|
95
|
+
#
|
96
|
+
# @param params [ActionController::Parameter{action: Symbol, controller: String}] **MUST** set `:action` to the action
|
97
|
+
# name so that `JSONAPI::Request#setup_action` can dispatch to the correct `setup_*_action` method. **MUST** set
|
98
|
+
# `:controller` to a URL name for the controller, such as `"api/v1/partner"`, so that the resource can be looked up
|
99
|
+
# by the controller name.
|
100
|
+
# @return [Hash] rendered, but not encoded JSON.
|
101
|
+
def process_request_params(params) # rubocop:disable Metrics/MethodLength
|
102
|
+
request = JSONAPI::Request.new(
|
103
|
+
params,
|
104
|
+
context: {},
|
105
|
+
key_formatter: key_formatter,
|
106
|
+
server_error_callbacks: []
|
107
|
+
)
|
108
|
+
|
109
|
+
if !request.errors.empty?
|
110
|
+
render_errors(request.errors, request: request)
|
111
|
+
else
|
112
|
+
operation_results = create_operations_processor.process(request)
|
113
|
+
render_results(
|
114
|
+
operation_results: operation_results,
|
115
|
+
request: request
|
116
|
+
)
|
117
|
+
end
|
118
|
+
rescue => e
|
119
|
+
handle_exceptions(e, request: request)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Renders the `errors` as a JSON API errors Document.
|
123
|
+
#
|
124
|
+
# @param errors [Array<JSONAPI::Error>] errors to use in a JSON API errors Document
|
125
|
+
# @param request [JSONAPI::Request] the request that caused the `errors`.
|
126
|
+
# @return [Hash] rendered, but not encoded JSON
|
127
|
+
def render_errors(errors, request:)
|
128
|
+
operation_results = JSONAPI::OperationResults.new
|
129
|
+
result = JSONAPI::ErrorsOperationResult.new(errors[0].status, errors)
|
130
|
+
operation_results.add_result(result)
|
131
|
+
|
132
|
+
render_results(
|
133
|
+
operation_results: operation_results,
|
134
|
+
request: request
|
135
|
+
)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Renders the `operation_results` as a JSON API Document.
|
139
|
+
#
|
140
|
+
# @param operation_results [JSONAPI::OperationResults] a collection of results from various operations.
|
141
|
+
# @param request [JSONAPI::Request] the original request that generated the `operation_results`.
|
142
|
+
# @return [Hash] rendered, but not encoded JSON
|
143
|
+
def render_results(operation_results:, request:)
|
144
|
+
response_document = create_response_document(
|
145
|
+
operation_results: operation_results,
|
146
|
+
request: request
|
147
|
+
)
|
148
|
+
response_document.contents.as_json
|
149
|
+
end
|
150
|
+
|
151
|
+
# Class to serialize `JSONAPI::Resource`s to JSON API documents.
|
152
|
+
#
|
153
|
+
# @return [Class<JSONAPI::ResourceSerializer>]
|
154
|
+
def resource_serializer_klass
|
155
|
+
@resource_serializer_klass ||= JSONAPI::ResourceSerializer
|
156
|
+
end
|
157
|
+
|
158
|
+
# Control by setting in an initializer:
|
159
|
+
# JSONAPI.configuration.route = :camelized_route
|
160
|
+
#
|
161
|
+
# @return [JSONAPI::RouteFormatter]
|
162
|
+
def route_formatter
|
163
|
+
JSONAPI.configuration.route_formatter
|
164
|
+
end
|
165
|
+
|
166
|
+
# Options passed to `resource_serializer_klass` instance when serializing the JSON API document.
|
167
|
+
#
|
168
|
+
# @return [Hash] Defaults to `{}`
|
169
|
+
def serialization_options
|
170
|
+
{}
|
171
|
+
end
|
172
|
+
end
|