carrot_rpc 0.2.3.pre
Sign up to get free protection for your applications and to get access to all the features.
- 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
|