carrot_rpc 0.2.3.pre

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