the-active-rpc 1.0.1

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.
@@ -0,0 +1,191 @@
1
+ module ActiveRpc
2
+ module Rpc
3
+ module Concerns
4
+ # The RequestProcessor concern provides methods for processing gRPC requests
5
+ # with standardized error handling and parameter validation.
6
+ #
7
+ # @example
8
+ # def get_user
9
+ # process_request do
10
+ # validate_required_params!(request.message, :id)
11
+ # user = find_record(User, request.message.id)
12
+ # user.to_rpc_response
13
+ # end
14
+ # end
15
+ module RequestProcessor
16
+ extend ActiveSupport::Concern
17
+
18
+ # Process a request with a block, handling common errors
19
+ def process_request
20
+ ActiveRecord::Base.transaction do
21
+ yield
22
+ end
23
+ rescue ActiveRecord::RecordNotFound => e
24
+ fail!(:not_found, :record_not_found, e.message)
25
+ rescue ActiveRecord::RecordInvalid => e
26
+ # For validation errors, we now return structured responses instead of failing
27
+ # This should be handled by the individual controller methods
28
+ raise e
29
+ rescue ArgumentError => e
30
+ fail!(:invalid_argument, :invalid_request, e.message)
31
+ rescue UncaughtThrowError => e
32
+ # Handle throw(:abort) from callbacks
33
+ if e.tag == :abort
34
+ fail!(:aborted, :operation_aborted, "Operation aborted")
35
+ else
36
+ # Re-raise if it's not an :abort throw
37
+ raise e
38
+ end
39
+ rescue StandardError => e
40
+ Rails.logger.error("Error processing request: #{e.message}\n#{e.backtrace.join("\n")}")
41
+ fail!(:internal, :unexpected_error, "An unexpected error occurred")
42
+ end
43
+
44
+ # Extract parameters from a request payload
45
+ def extract_params(payload, *param_names)
46
+ # Step 1: Determine which fields to extract and get _fields metadata
47
+ if payload.respond_to?(:_fields) && !payload._fields.empty?
48
+ # Use _fields metadata (only top-level fields)
49
+ fields_to_extract = payload._fields
50
+ .reject { |path| path.include?('.') || path.include?('[') }
51
+ # Pass the full _fields metadata for nested conversion
52
+ fields_metadata = payload._fields
53
+ elsif param_names.empty?
54
+ # Extract all available fields from protobuf descriptor
55
+ if payload.class.respond_to?(:descriptor)
56
+ fields_to_extract = payload.class.descriptor.map(&:name)
57
+ else
58
+ fields_to_extract = []
59
+ end
60
+ fields_metadata = nil
61
+ else
62
+ # Extract only requested fields
63
+ fields_to_extract = param_names.map(&:to_s)
64
+ fields_metadata = nil
65
+ end
66
+
67
+ # Step 2: Extract the fields
68
+ result = {}
69
+ fields_to_extract.each do |field_name|
70
+ field_sym = field_name.to_sym
71
+ if payload.respond_to?(field_sym)
72
+ field_value = payload.send(field_sym)
73
+ # Pass the field path and metadata for nested conversion
74
+ field_path = field_name
75
+ result[field_sym] = convert_protobuf_to_ruby(field_value, field_path, fields_metadata)
76
+ end
77
+ end
78
+
79
+ result
80
+ end
81
+
82
+ private
83
+
84
+ # Convert protobuf objects to Ruby hashes/arrays recursively
85
+ # WITHOUT using to_h to preserve empty arrays and explicit default values
86
+ # Respects _fields metadata to only extract fields that were originally sent
87
+ def convert_protobuf_to_ruby(value, current_path = '', fields_metadata = nil)
88
+ case value
89
+ when Google::Protobuf::MessageExts
90
+ # Convert protobuf message to hash by extracting only tracked fields
91
+ result = {}
92
+
93
+ if fields_metadata
94
+ # Only extract fields that are in the _fields metadata
95
+ value.class.descriptor.each do |field|
96
+ field_name = field.name
97
+ field_path = current_path.empty? ? field_name : "#{current_path}.#{field_name}"
98
+
99
+ # Check if this field path exists in _fields metadata
100
+ # Match exact path or any child path (but not parent paths)
101
+ if fields_metadata.any? { |path| path == field_path || path.start_with?("#{field_path}.") || path.start_with?("#{field_path}[") }
102
+ field_value = value.send(field_name)
103
+ result[field_name] = convert_protobuf_to_ruby(field_value, field_path, fields_metadata)
104
+ end
105
+ end
106
+ else
107
+ # Fallback: extract all fields when no metadata available
108
+ value.class.descriptor.each do |field|
109
+ field_name = field.name
110
+ field_value = value.send(field_name)
111
+ field_path = current_path.empty? ? field_name : "#{current_path}.#{field_name}"
112
+ result[field_name] = convert_protobuf_to_ruby(field_value, field_path, fields_metadata)
113
+ end
114
+ end
115
+
116
+ result
117
+ when Google::Protobuf::Map
118
+ # Convert map fields while respecting _fields metadata for individual keys
119
+ allowed_entry_paths = if fields_metadata
120
+ fields_metadata.select { |path| path.start_with?("#{current_path}[") }
121
+ end
122
+
123
+ value.each_with_object({}) do |(map_key, map_value), acc|
124
+ entry_path = "#{current_path}[#{map_key}]"
125
+
126
+ if allowed_entry_paths.present?
127
+ next unless allowed_entry_paths.any? { |path| path == entry_path || path.start_with?("#{entry_path}.") }
128
+ end
129
+
130
+ acc[map_key] = convert_protobuf_to_ruby(map_value, entry_path, fields_metadata)
131
+ end
132
+ when Google::Protobuf::RepeatedField
133
+ # Convert repeated field to array and recursively convert elements
134
+ value.map.with_index do |item, index|
135
+ item_path = "#{current_path}[#{index}]"
136
+ convert_protobuf_to_ruby(item, item_path, fields_metadata)
137
+ end
138
+ when Array
139
+ # Convert array elements recursively
140
+ value.map.with_index do |item, index|
141
+ item_path = "#{current_path}[#{index}]"
142
+ convert_protobuf_to_ruby(item, item_path, fields_metadata)
143
+ end
144
+ when Hash
145
+ # Convert hash values recursively
146
+ value.transform_values { |v| convert_protobuf_to_ruby(v, current_path, fields_metadata) }
147
+ else
148
+ # Return primitive values as-is
149
+ value
150
+ end
151
+ end
152
+
153
+ # Validate required parameters
154
+ def validate_required_params!(payload, *param_names)
155
+ missing = param_names.select { |name| !payload.respond_to?(name) || payload.send(name).nil? }
156
+ if missing.any?
157
+ raise ArgumentError, "Missing required parameters: #{missing.join(', ')}"
158
+ end
159
+ end
160
+
161
+ # Find a record or fail with not found
162
+ def find_record(model_class, id)
163
+ record = model_class.find_by(id: id)
164
+ unless record
165
+ fail!(:not_found, :record_not_found, "#{model_class.name} with ID #{id} not found")
166
+ end
167
+ record
168
+ end
169
+
170
+ # Create structured response metadata for validation errors
171
+ def create_validation_error_metadata(record)
172
+ # Share the Rails errors object directly - no conversion needed!
173
+ errors_map = {}
174
+ record.errors.to_hash.each do |field, messages|
175
+ errors_map[field.to_s] = Core::ErrorMessages.new(messages: messages)
176
+ end
177
+
178
+ Core::ResponseMetadata.new(
179
+ success: false,
180
+ errors: errors_map
181
+ )
182
+ end
183
+
184
+ # Create successful response metadata
185
+ def create_success_metadata
186
+ Core::ResponseMetadata.new(success: true)
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,281 @@
1
+ module ActiveRpc
2
+ module Rpc
3
+ module Concerns
4
+ # The ResourceController concern provides high-level methods for common CRUD operations
5
+ # in gRPC controllers.
6
+ #
7
+ # @example
8
+ # def get_user
9
+ # get_resource(User, request.message, transformer: :to_rpc_response)
10
+ # end
11
+ #
12
+ # def list_users
13
+ # list_resources(User, request.message,
14
+ # response_class: ::Core::UserListResponse,
15
+ # transformer: :to_rpc_response
16
+ # )
17
+ # end
18
+ module ResourceController
19
+ extend ActiveSupport::Concern
20
+
21
+ include ActiveRpc::Rpc::Concerns::RequestProcessor
22
+ include ActiveRpc::Rpc::Concerns::QueryBuilder
23
+
24
+ # Get a single resource
25
+ def get_resource(model_class, request_payload, options = {})
26
+ process_request do
27
+ # Validate required parameters
28
+ validate_required_params!(request_payload, :id)
29
+
30
+ # Find the resource with includes if provided
31
+ includes = options[:includes] || []
32
+ resource = model_class.includes(includes).find(request_payload.id)
33
+
34
+ # Transform the resource
35
+ if options[:transformer].is_a?(Proc)
36
+ options[:transformer].call(resource)
37
+ elsif options[:transformer] == :to_rpc_response
38
+ resource.to_rpc_response
39
+ else
40
+ resource
41
+ end
42
+ end
43
+ end
44
+
45
+ # List resources
46
+ def list_resources(model_class, request_payload, options = {})
47
+ process_request do
48
+ execute_query(model_class, request_payload, options)
49
+ end
50
+ end
51
+
52
+ # Create a resource
53
+ def create_resource(model_class, request_payload, options = {})
54
+ process_request do
55
+ # Validate required parameters
56
+ if options[:required_params].is_a?(Array)
57
+ validate_required_params!(request_payload, *options[:required_params])
58
+ end
59
+
60
+ # Extract parameters
61
+ params = if options[:param_extractor].is_a?(Proc)
62
+ options[:param_extractor].call(request_payload)
63
+ else
64
+ extract_params(request_payload, *options[:param_names])
65
+ end
66
+
67
+ # Coerce JSONB columns from JSON string to Hash (schema-aware, generic)
68
+ params = coerce_jsonb_params(model_class, params)
69
+
70
+ # Create the resource
71
+ resource = model_class.new(params.to_h.with_indifferent_access)
72
+
73
+ # Apply before save hook if provided
74
+ if options[:before_save].is_a?(Proc)
75
+ hook_result = options[:before_save].call(resource, request_payload)
76
+ # Check if the hook returned an error response
77
+ return hook_result if hook_result.respond_to?(:success) && hook_result.success == false
78
+ end
79
+
80
+ # Save the resource
81
+ if resource.save
82
+ # Apply after save hook if provided
83
+ if options[:after_save].is_a?(Proc)
84
+ hook_result = options[:after_save].call(resource, request_payload)
85
+ # Check if the hook returned an error response
86
+ return hook_result if hook_result.respond_to?(:success) && hook_result.success == false
87
+ end
88
+
89
+ # Transform the resource with success metadata
90
+ result = if options[:transformer].is_a?(Proc)
91
+ options[:transformer].call(resource)
92
+ elsif options[:transformer] == :to_rpc_response
93
+ resource.reload.to_rpc_response
94
+ else
95
+ resource.reload
96
+ end
97
+
98
+ # Ensure string fields in protobuf receive strings (stringify Hash/Array)
99
+ coerce_string_fields_in_response!(result)
100
+
101
+ # Add success metadata to the response
102
+ result._metadata = create_success_metadata if result.respond_to?(:_metadata=)
103
+
104
+ result
105
+ else
106
+ # Create response with validation error metadata
107
+ result = if options[:transformer].is_a?(Proc)
108
+ options[:transformer].call(resource)
109
+ elsif options[:transformer] == :to_rpc_response
110
+ resource.to_rpc_response
111
+ else
112
+ # Create a basic response object for the resource type
113
+ resource_class_name = resource.class.name
114
+ response_class = "Core::#{resource_class_name}Response".constantize
115
+ response_class.new
116
+ end
117
+
118
+ # Add validation error metadata to the response
119
+ result._metadata = create_validation_error_metadata(resource) if result.respond_to?(:_metadata=)
120
+
121
+ # Ensure string fields in protobuf receive strings (stringify Hash/Array)
122
+ coerce_string_fields_in_response!(result)
123
+
124
+ result
125
+ end
126
+ end
127
+ end
128
+
129
+ # Update a resource
130
+ def update_resource(model_class, request_payload, options = {})
131
+ process_request do
132
+ # Validate required parameters
133
+ validate_required_params!(request_payload, :id)
134
+
135
+ # Find the resource
136
+ resource = find_record(model_class, request_payload.id)
137
+
138
+ # Extract parameters
139
+ params = if options[:param_extractor].is_a?(Proc)
140
+ options[:param_extractor].call(request_payload)
141
+ else
142
+ extract_params(request_payload, *options[:param_names])
143
+ end
144
+
145
+ # Coerce JSONB columns from JSON string to Hash (schema-aware, generic)
146
+ params = coerce_jsonb_params(model_class, params)
147
+
148
+ # Apply before save hook if provided
149
+ if options[:before_save].is_a?(Proc)
150
+ hook_result = options[:before_save].call(resource, request_payload)
151
+ # Check if the hook returned an error response
152
+ return hook_result if hook_result.respond_to?(:success) && hook_result.success == false
153
+ end
154
+
155
+ # Update the resource
156
+ resource.assign_attributes(params.with_indifferent_access)
157
+ if resource.save
158
+ # Apply after save hook if provided
159
+ if options[:after_save].is_a?(Proc)
160
+ hook_result = options[:after_save].call(resource, request_payload)
161
+ # Check if the hook returned an error response
162
+ return hook_result if hook_result.respond_to?(:success) && hook_result.success == false
163
+ end
164
+
165
+ # Transform the resource with success metadata
166
+ result = if options[:transformer].is_a?(Proc)
167
+ options[:transformer].call(resource)
168
+ elsif options[:transformer] == :to_rpc_response
169
+ resource.reload.to_rpc_response
170
+ else
171
+ resource.reload
172
+ end
173
+
174
+ # Ensure string fields in protobuf receive strings (stringify Hash/Array)
175
+ coerce_string_fields_in_response!(result)
176
+
177
+ # Add success metadata to the response
178
+ result._metadata = create_success_metadata if result.respond_to?(:_metadata=)
179
+
180
+ result
181
+ else
182
+ # Create response with validation error metadata
183
+ result = if options[:transformer].is_a?(Proc)
184
+ options[:transformer].call(resource)
185
+ elsif options[:transformer] == :to_rpc_response
186
+ resource.to_rpc_response
187
+ else
188
+ # Create a basic response object for the resource type
189
+ resource_class_name = resource.class.name
190
+ response_class = "Core::#{resource_class_name}Response".constantize
191
+ response_class.new
192
+ end
193
+
194
+ # Add validation error metadata to the response
195
+ result._metadata = create_validation_error_metadata(resource) if result.respond_to?(:_metadata=)
196
+
197
+ # Ensure string fields in protobuf receive strings (stringify Hash/Array)
198
+ coerce_string_fields_in_response!(result)
199
+
200
+ result
201
+ end
202
+ end
203
+ end
204
+
205
+ # Delete a resource
206
+ def delete_resource(model_class, request_payload, options = {})
207
+ process_request do
208
+ # Validate required parameters
209
+ validate_required_params!(request_payload, :id)
210
+
211
+ # Find the resource
212
+ resource = find_record(model_class, request_payload.id)
213
+
214
+ # Apply before destroy hook if provided
215
+ options[:before_destroy].call(resource, request_payload) if options[:before_destroy].is_a?(Proc)
216
+
217
+ # Destroy the resource
218
+ resource.destroy!
219
+
220
+ # Apply after destroy hook if provided
221
+ options[:after_destroy].call(resource, request_payload) if options[:after_destroy].is_a?(Proc)
222
+
223
+ # Return success response
224
+ options[:success_response] || { success: true }
225
+ end
226
+ end
227
+
228
+ private
229
+
230
+ # Convert jsonb column params from JSON string to Hash, generically
231
+ def coerce_jsonb_params(model_class, params)
232
+ return params unless params.is_a?(Hash)
233
+
234
+ jsonb_columns = model_class.columns.select { |c| c.type == :jsonb }.map(&:name)
235
+ return params if jsonb_columns.empty?
236
+
237
+ params.transform_keys(&:to_sym).tap do |h|
238
+ jsonb_columns.each do |col|
239
+ key = col.to_sym
240
+ next unless h.key?(key) && h[key].is_a?(String)
241
+
242
+ begin
243
+ parsed = JSON.parse(h[key])
244
+ h[key] = parsed if parsed.is_a?(Hash) || parsed.is_a?(Array)
245
+ rescue JSON::ParserError
246
+ # leave as-is
247
+ end
248
+ end
249
+ end
250
+ end
251
+
252
+ # Ensure any protobuf string fields receive strings, not Hash/Array (stringify if needed)
253
+ def coerce_string_fields_in_response!(message)
254
+ return message unless message.respond_to?(:class) && message.class.respond_to?(:descriptor)
255
+
256
+ message.class.descriptor.each do |field|
257
+ name = field.name
258
+ value = message.send(name) if message.respond_to?(name)
259
+
260
+ case field.type
261
+ when :string
262
+ message.send("#{name}=", value.to_json) if value.is_a?(Hash) || value.is_a?(Array)
263
+ when :message
264
+ if value.respond_to?(:class) && value.class.respond_to?(:descriptor)
265
+ coerce_string_fields_in_response!(value)
266
+ end
267
+ when :repeated
268
+ if value.is_a?(Array)
269
+ value.each do |v|
270
+ coerce_string_fields_in_response!(v) if v.respond_to?(:class) && v.class.respond_to?(:descriptor)
271
+ end
272
+ end
273
+ end
274
+ end
275
+
276
+ message
277
+ end
278
+ end
279
+ end
280
+ end
281
+ end
@@ -0,0 +1,92 @@
1
+ module ActiveRpc
2
+ module Rpc
3
+ module Concerns
4
+ # The Scopable concern provides methods for applying named scopes
5
+ # to ActiveRecord queries in gRPC controllers.
6
+ #
7
+ # @example
8
+ # def list_users
9
+ # process_request do
10
+ # base_query = User.all
11
+ # query = apply_scopes(base_query, request.message)
12
+ # # ...
13
+ # end
14
+ # end
15
+ module Scopable
16
+ extend ActiveSupport::Concern
17
+
18
+ # Apply scopes to a query
19
+ def apply_scopes(query, params)
20
+ return query unless params.respond_to?(:scope) && params.scope.present?
21
+
22
+ # Handle both new JSON format and old individual parameters for backward compatibility
23
+ scope_hash = params.scope.to_h
24
+ scope_params = if scope_hash.key?('args')
25
+ # New format: JSON-serialized scope arguments
26
+ begin
27
+ JSON.parse(scope_hash['args'])
28
+ rescue JSON::ParserError => e
29
+ Rails.logger.error("[Scopable] Failed to parse scope args: #{e.message}")
30
+ {}
31
+ end
32
+ else
33
+ # Old format: individual string parameters (backward compatibility)
34
+ scope_hash
35
+ end
36
+
37
+ # Get available scopes
38
+ available_scopes = get_available_scopes(query.model)
39
+
40
+ # Apply each scope
41
+ scope_params.each do |scope_name, scope_value|
42
+ # Skip if the scope is not available and we have defined scopes
43
+ scope_available = available_scopes.empty? ||
44
+ available_scopes.key?(scope_name.to_sym) ||
45
+ available_scopes.key?(scope_name.to_s)
46
+
47
+ unless scope_available
48
+ Rails.logger.debug("[Scopable] Skipping scope #{scope_name} - not in available_scopes")
49
+ next
50
+ end
51
+
52
+ unless query.respond_to?(scope_name)
53
+ Rails.logger.debug("[Scopable] Skipping scope #{scope_name} - query doesn't respond to it")
54
+ next
55
+ end
56
+
57
+ begin
58
+ # Handle regular scopes
59
+ method = query.method(scope_name)
60
+ if method.arity.abs <= 1
61
+ Rails.logger.debug("[Scopable] Applying scope: #{scope_name}(#{scope_value.class})")
62
+ query = scope_value.present? ? query.public_send(scope_name, scope_value) : query.public_send(scope_name)
63
+ else
64
+ Rails.logger.debug("[Scopable] Skipping scope #{scope_name} - arity too high: #{method.arity}")
65
+ end
66
+ rescue => e
67
+ Rails.logger.error("[Scopable] Error applying scope '#{scope_name}': #{e.message}")
68
+ # Continue with other scopes even if one fails
69
+ end
70
+ end
71
+
72
+ query
73
+ end
74
+
75
+ private
76
+
77
+ # Get available scopes for a model
78
+ def get_available_scopes(model_class)
79
+ if model_class.respond_to?(:active_rpc_config) && model_class.active_rpc_config
80
+ resource = model_class.active_rpc_config[:resource].to_s.underscore
81
+ scopes_method = "#{resource}_scopes"
82
+
83
+ return model_class.send(scopes_method) if model_class.respond_to?(scopes_method)
84
+ end
85
+
86
+ # Default empty scopes if none are defined
87
+ {}
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,30 @@
1
+ module ActiveRpc
2
+ module Rpc
3
+ module Concerns
4
+ # The Serializable concern provides methods for transforming ActiveRecord models
5
+ # into gRPC response objects.
6
+ #
7
+ # @example
8
+ # def get_user
9
+ # process_request do
10
+ # user = find_record(User, request.message.id)
11
+ # serialize_record(user, serializer: UserSerializer)
12
+ # end
13
+ # end
14
+ module Serializable
15
+ extend ActiveSupport::Concern
16
+
17
+ # Transform a record using a serializer
18
+ def serialize_record(record, options = {})
19
+ serializer_class = options[:serializer] || "#{record.class.name}Serializer".constantize
20
+ serializer_class.new(record).to_h
21
+ end
22
+
23
+ # Transform a collection using a serializer
24
+ def serialize_collection(collection, options = {})
25
+ collection.map { |record| serialize_record(record, options) }
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,71 @@
1
+ module ActiveRpc
2
+ module Rpc
3
+ module Concerns
4
+ # The Sortable concern provides methods for applying sorting
5
+ # to ActiveRecord queries in gRPC controllers.
6
+ #
7
+ # @example
8
+ # def list_users
9
+ # process_request do
10
+ # base_query = User.all
11
+ # query = apply_sorting(base_query, request.message)
12
+ # # ...
13
+ # end
14
+ # end
15
+ module Sortable
16
+ extend ActiveSupport::Concern
17
+
18
+ # Apply sorting to a query
19
+ def apply_sorting(query, params)
20
+ return query unless params.respond_to?(:sort) && params.sort.present?
21
+
22
+ begin
23
+ # Parse the sort parameter
24
+ field, direction = params.sort.split(' ')
25
+ direction ||= 'asc'
26
+
27
+ # Sanitize the direction
28
+ direction = %w[asc desc].include?(direction.downcase) ? direction.downcase : 'asc'
29
+
30
+ # Get query configuration
31
+ query_config = get_query_config(query.model)
32
+ sortable_attrs = query_config[:sortable] || []
33
+
34
+ # Check if the field is sortable or if we have no defined sortable attributes
35
+ if sortable_attrs.empty? || sortable_attrs.include?(field.to_sym) || sortable_attrs.include?(field.to_s)
36
+ # Apply sorting
37
+ query.order("#{field} #{direction}")
38
+ else
39
+ Rails.logger.error("Invalid sort field: #{field}")
40
+ query
41
+ end
42
+ rescue => e
43
+ Rails.logger.error("Error applying sorting: #{e.message}")
44
+ # Return original query if sorting fails
45
+ query
46
+ end
47
+ end
48
+
49
+ # Get query configuration for a model
50
+ def get_query_config(model_class)
51
+ if model_class.respond_to?(:active_rpc_config) && model_class.active_rpc_config
52
+ resource = model_class.active_rpc_config[:resource].to_s.underscore
53
+ query_config_method = "#{resource}_query_config"
54
+
55
+ if model_class.respond_to?(query_config_method)
56
+ return model_class.send(query_config_method)
57
+ end
58
+ end
59
+
60
+ # Default configuration if none is defined
61
+ {
62
+ searchable: model_class.column_names,
63
+ filterable: model_class.column_names,
64
+ sortable: model_class.column_names,
65
+ includable: model_class.reflect_on_all_associations.map(&:name)
66
+ }
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,7 @@
1
+ module ActiveRpc
2
+ module Rpc
3
+ class Configuration
4
+ # Server-side gRPC configuration placeholder
5
+ end
6
+ end
7
+ end