graphql_rails 0.7.0 → 1.2.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.
- checksums.yaml +4 -4
- data/.hound.yml +1 -0
- data/.rubocop.yml +3 -3
- data/.ruby-version +1 -1
- data/.travis.yml +2 -2
- data/CHANGELOG.md +35 -0
- data/Gemfile +3 -2
- data/Gemfile.lock +181 -71
- data/docs/README.md +40 -8
- data/docs/_sidebar.md +5 -0
- data/docs/components/controller.md +295 -9
- data/docs/components/decorator.md +69 -0
- data/docs/components/model.md +267 -6
- data/docs/components/routes.md +28 -0
- data/docs/getting_started/quick_start.md +10 -3
- data/docs/index.html +1 -1
- data/docs/logging_and_monitoring/logging_and_monitoring.md +35 -0
- data/docs/other_tools/query_runner.md +49 -0
- data/docs/other_tools/schema_dump.md +29 -0
- data/docs/testing/testing.md +3 -1
- data/graphql_rails.gemspec +5 -4
- data/lib/generators/graphql_rails/install_generator.rb +50 -0
- data/lib/generators/graphql_rails/templates/example_users_controller.erb +19 -0
- data/lib/generators/graphql_rails/templates/graphql_application_controller.erb +8 -0
- data/lib/generators/graphql_rails/templates/graphql_controller.erb +20 -0
- data/lib/generators/graphql_rails/templates/graphql_router.erb +19 -0
- data/lib/generators/graphql_rails/templates/graphql_router_spec.erb +21 -0
- data/lib/graphql_rails.rb +6 -0
- data/lib/graphql_rails/attributes/attributable.rb +22 -17
- data/lib/graphql_rails/attributes/attribute.rb +67 -3
- data/lib/graphql_rails/attributes/attribute_name_parser.rb +4 -4
- data/lib/graphql_rails/attributes/input_attribute.rb +33 -15
- data/lib/graphql_rails/attributes/input_type_parser.rb +62 -0
- data/lib/graphql_rails/attributes/type_name_info.rb +38 -0
- data/lib/graphql_rails/attributes/type_parseable.rb +132 -0
- data/lib/graphql_rails/attributes/type_parser.rb +59 -53
- data/lib/graphql_rails/concerns/service.rb +19 -0
- data/lib/graphql_rails/controller.rb +42 -21
- data/lib/graphql_rails/controller/action.rb +12 -67
- data/lib/graphql_rails/controller/action_configuration.rb +70 -28
- data/lib/graphql_rails/controller/build_controller_action_resolver.rb +52 -0
- data/lib/graphql_rails/controller/build_controller_action_resolver/controller_action_resolver.rb +28 -0
- data/lib/graphql_rails/controller/configuration.rb +56 -3
- data/lib/graphql_rails/controller/log_controller_action.rb +71 -0
- data/lib/graphql_rails/controller/request.rb +29 -8
- data/lib/graphql_rails/controller/request/format_errors.rb +58 -0
- data/lib/graphql_rails/decorator.rb +41 -0
- data/lib/graphql_rails/decorator/relation_decorator.rb +75 -0
- data/lib/graphql_rails/errors/custom_execution_error.rb +22 -0
- data/lib/graphql_rails/errors/execution_error.rb +6 -7
- data/lib/graphql_rails/errors/system_error.rb +14 -0
- data/lib/graphql_rails/errors/validation_error.rb +1 -5
- data/lib/graphql_rails/input_configurable.rb +47 -0
- data/lib/graphql_rails/integrations.rb +19 -0
- data/lib/graphql_rails/integrations/lograge.rb +39 -0
- data/lib/graphql_rails/integrations/sentry.rb +34 -0
- data/lib/graphql_rails/model.rb +26 -4
- data/lib/graphql_rails/model/add_fields_to_graphql_type.rb +45 -0
- data/lib/graphql_rails/model/build_connection_type.rb +52 -0
- data/lib/graphql_rails/model/{configuration → build_connection_type}/count_items.rb +5 -5
- data/lib/graphql_rails/model/build_enum_type.rb +39 -10
- data/lib/graphql_rails/model/build_graphql_input_type.rb +8 -4
- data/lib/graphql_rails/model/call_graphql_model_method.rb +72 -0
- data/lib/graphql_rails/model/configurable.rb +6 -2
- data/lib/graphql_rails/model/configuration.rb +30 -16
- data/lib/graphql_rails/model/find_or_build_graphql_type.rb +64 -0
- data/lib/graphql_rails/model/find_or_build_graphql_type_class.rb +46 -0
- data/lib/graphql_rails/model/input.rb +11 -7
- data/lib/graphql_rails/query_runner.rb +68 -0
- data/lib/graphql_rails/railtie.rb +10 -0
- data/lib/graphql_rails/router.rb +40 -13
- data/lib/graphql_rails/router/resource_routes_builder.rb +10 -9
- data/lib/graphql_rails/router/route.rb +21 -6
- data/lib/graphql_rails/router/schema_builder.rb +30 -11
- data/lib/graphql_rails/rspec_controller_helpers.rb +6 -4
- data/lib/graphql_rails/tasks/dump_graphql_schema.rb +57 -0
- data/lib/graphql_rails/tasks/schema.rake +14 -0
- data/lib/graphql_rails/version.rb +1 -1
- metadata +70 -19
- data/lib/graphql_rails/controller/controller_function.rb +0 -50
- data/lib/graphql_rails/controller/format_results.rb +0 -36
- data/lib/graphql_rails/model/build_graphql_type.rb +0 -37
data/lib/graphql_rails/controller/build_controller_action_resolver/controller_action_resolver.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'graphql_rails/controller/request'
|
4
|
+
|
5
|
+
module GraphqlRails
|
6
|
+
class Controller
|
7
|
+
class BuildControllerActionResolver
|
8
|
+
# Resolver which includes controller specific methods.
|
9
|
+
# Used to simplify resolver build for each controller action
|
10
|
+
class ControllerActionResolver < GraphQL::Schema::Resolver
|
11
|
+
def self.controller(controller_class = nil)
|
12
|
+
@controller = controller_class if controller_class
|
13
|
+
@controller
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.controller_action_name(name = nil)
|
17
|
+
@controller_action_name = name if name
|
18
|
+
@controller_action_name
|
19
|
+
end
|
20
|
+
|
21
|
+
def resolve(**inputs)
|
22
|
+
request = Request.new(object, inputs, context)
|
23
|
+
self.class.controller.new(request).call(self.class.controller_action_name)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -3,14 +3,20 @@
|
|
3
3
|
require 'active_support/core_ext/string/inflections'
|
4
4
|
require 'graphql_rails/controller/action_configuration'
|
5
5
|
require 'graphql_rails/controller/action_hook'
|
6
|
+
require 'graphql_rails/errors/error'
|
6
7
|
|
7
8
|
module GraphqlRails
|
8
9
|
class Controller
|
9
10
|
# stores all graphql_rails contoller specific config
|
10
11
|
class Configuration
|
12
|
+
class InvalidActionConfiguration < GraphqlRails::Error; end
|
13
|
+
|
14
|
+
LIB_REGEXP = %r{/graphql_rails/lib/}
|
15
|
+
|
11
16
|
attr_reader :action_by_name
|
12
17
|
|
13
|
-
def initialize
|
18
|
+
def initialize(controller)
|
19
|
+
@controller = controller
|
14
20
|
@hooks = {
|
15
21
|
before: {},
|
16
22
|
after: {},
|
@@ -18,6 +24,7 @@ module GraphqlRails
|
|
18
24
|
}
|
19
25
|
|
20
26
|
@action_by_name = {}
|
27
|
+
@action_default = nil
|
21
28
|
end
|
22
29
|
|
23
30
|
def initialize_copy(other)
|
@@ -31,6 +38,12 @@ module GraphqlRails
|
|
31
38
|
end
|
32
39
|
end
|
33
40
|
|
41
|
+
def dup_with(controller:)
|
42
|
+
dup.tap do |new_config|
|
43
|
+
new_config.instance_variable_set(:@controller, controller)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
34
47
|
def action_hooks_for(hook_type, action_name)
|
35
48
|
hooks[hook_type].values.select { |hook| hook.applicable_for?(action_name) }
|
36
49
|
end
|
@@ -43,13 +56,53 @@ module GraphqlRails
|
|
43
56
|
ActionHook.new(name: hook_name, **options, &block)
|
44
57
|
end
|
45
58
|
|
59
|
+
def action_default
|
60
|
+
@action_default ||= ActionConfiguration.new(name: :default, controller: nil)
|
61
|
+
yield(@action_default) if block_given?
|
62
|
+
@action_default
|
63
|
+
end
|
64
|
+
|
46
65
|
def action(method_name)
|
47
|
-
|
66
|
+
action_name = method_name.to_s.underscore
|
67
|
+
@action_by_name[action_name] ||= action_default.dup_with(
|
68
|
+
name: action_name,
|
69
|
+
controller: controller,
|
70
|
+
defined_at: dynamic_source_location
|
71
|
+
)
|
72
|
+
yield(@action_by_name[action_name]) if block_given?
|
73
|
+
@action_by_name[action_name]
|
74
|
+
end
|
75
|
+
|
76
|
+
def action_config(method_name)
|
77
|
+
action_name = method_name.to_s.underscore
|
78
|
+
@action_by_name.fetch(action_name) { raise_invalid_config_error(action_name) }
|
79
|
+
end
|
80
|
+
|
81
|
+
def model(model = nil)
|
82
|
+
action_default.model(model)
|
48
83
|
end
|
49
84
|
|
50
85
|
private
|
51
86
|
|
52
|
-
attr_reader :hooks
|
87
|
+
attr_reader :hooks, :controller
|
88
|
+
|
89
|
+
def dynamic_source_location
|
90
|
+
project_trace = \
|
91
|
+
caller
|
92
|
+
.dup
|
93
|
+
.drop_while { |path| !path.match?(LIB_REGEXP) }
|
94
|
+
.drop_while { |path| path.match?(LIB_REGEXP) }
|
95
|
+
|
96
|
+
project_trace.first
|
97
|
+
end
|
98
|
+
|
99
|
+
def raise_invalid_config_error(action_name)
|
100
|
+
error_message = \
|
101
|
+
"Missing action configuration for #{controller}##{action_name}. " \
|
102
|
+
"Please define it with `action(:#{action_name})`."
|
103
|
+
|
104
|
+
raise InvalidActionConfiguration, error_message
|
105
|
+
end
|
53
106
|
end
|
54
107
|
end
|
55
108
|
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'graphql_rails/errors/execution_error'
|
4
|
+
|
5
|
+
module GraphqlRails
|
6
|
+
class Controller
|
7
|
+
# logs controller start and end times
|
8
|
+
class LogControllerAction
|
9
|
+
require 'graphql_rails/concerns/service'
|
10
|
+
|
11
|
+
include ::GraphqlRails::Service
|
12
|
+
|
13
|
+
START_PROCESSING_KEY = 'start_processing.graphql_action_controller'
|
14
|
+
PROCESS_ACTION_KEY = 'process_action.graphql_action_controller'
|
15
|
+
|
16
|
+
def initialize(controller_name:, action_name:, params:, graphql_request:)
|
17
|
+
@controller_name = controller_name
|
18
|
+
@action_name = action_name
|
19
|
+
@params = params
|
20
|
+
@graphql_request = graphql_request
|
21
|
+
end
|
22
|
+
|
23
|
+
def call
|
24
|
+
ActiveSupport::Notifications.instrument(START_PROCESSING_KEY, default_payload)
|
25
|
+
ActiveSupport::Notifications.instrument(PROCESS_ACTION_KEY, default_payload) do |payload|
|
26
|
+
yield.tap do
|
27
|
+
payload[:status] = status
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
attr_reader :controller_name, :action_name, :params, :graphql_request
|
35
|
+
|
36
|
+
def default_payload
|
37
|
+
{
|
38
|
+
controller: controller_name,
|
39
|
+
action: action_name,
|
40
|
+
params: filtered_params
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
def status
|
45
|
+
graphql_request.errors.present? ? 500 : 200
|
46
|
+
end
|
47
|
+
|
48
|
+
def filtered_params
|
49
|
+
@filtered_params ||=
|
50
|
+
if filter_parameters.empty?
|
51
|
+
params
|
52
|
+
else
|
53
|
+
filter_options = Rails.configuration.filter_parameters
|
54
|
+
parameter_filter_class.new(filter_options).filter(params)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def filter_parameters
|
59
|
+
return [] if !defined?(Rails) || Rails.application.nil?
|
60
|
+
|
61
|
+
Rails.application.config.filter_parameters || []
|
62
|
+
end
|
63
|
+
|
64
|
+
def parameter_filter_class
|
65
|
+
return ActiveSupport::ParameterFilter if Object.const_defined?('ActiveSupport::ParameterFilter')
|
66
|
+
|
67
|
+
ActionDispatch::Http::ParameterFilter
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -1,11 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'graphql_rails/errors/execution_error'
|
4
|
-
|
5
3
|
module GraphqlRails
|
6
4
|
class Controller
|
7
5
|
# Contains all info related with single request to controller
|
8
6
|
class Request
|
7
|
+
require 'graphql_rails/controller/request/format_errors'
|
8
|
+
|
9
9
|
attr_accessor :object_to_return
|
10
10
|
attr_reader :errors, :context
|
11
11
|
|
@@ -16,12 +16,9 @@ module GraphqlRails
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def errors=(new_errors)
|
19
|
-
@errors = new_errors
|
19
|
+
@errors = FormatErrors.call(not_formatted_errors: new_errors)
|
20
20
|
|
21
|
-
|
22
|
-
error_message = error.is_a?(String) ? error : error.message
|
23
|
-
context.add_error(ExecutionError.new(error_message))
|
24
|
-
end
|
21
|
+
@errors.each { |error| context.add_error(error) }
|
25
22
|
end
|
26
23
|
|
27
24
|
def no_object_to_return?
|
@@ -29,12 +26,36 @@ module GraphqlRails
|
|
29
26
|
end
|
30
27
|
|
31
28
|
def params
|
32
|
-
inputs.to_h
|
29
|
+
deep_transform_values(inputs.to_h) do |val|
|
30
|
+
graphql_object_to_hash(val)
|
31
|
+
end
|
33
32
|
end
|
34
33
|
|
35
34
|
private
|
36
35
|
|
37
36
|
attr_reader :graphql_object, :inputs
|
37
|
+
|
38
|
+
def graphql_object_to_hash(object)
|
39
|
+
if object.is_a?(GraphQL::Dig)
|
40
|
+
object.to_h
|
41
|
+
elsif object.is_a?(Array)
|
42
|
+
object.map { |item| graphql_object_to_hash(item) }
|
43
|
+
else
|
44
|
+
object
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def deep_transform_values(hash, &block)
|
49
|
+
return hash unless hash.is_a?(Hash)
|
50
|
+
|
51
|
+
hash.transform_values do |val|
|
52
|
+
if val.is_a?(Hash)
|
53
|
+
deep_transform_values(val, &block)
|
54
|
+
else
|
55
|
+
yield(val)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
38
59
|
end
|
39
60
|
end
|
40
61
|
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'graphql_rails/concerns/service'
|
4
|
+
require 'graphql_rails/errors/execution_error'
|
5
|
+
require 'graphql_rails/errors/validation_error'
|
6
|
+
require 'graphql_rails/errors/custom_execution_error'
|
7
|
+
|
8
|
+
module GraphqlRails
|
9
|
+
class Controller
|
10
|
+
class Request
|
11
|
+
# Converts user provided free-form errors in to meaningfull graphql error classes
|
12
|
+
class FormatErrors
|
13
|
+
include Service
|
14
|
+
|
15
|
+
def initialize(not_formatted_errors:)
|
16
|
+
@not_formatted_errors = not_formatted_errors
|
17
|
+
end
|
18
|
+
|
19
|
+
def call
|
20
|
+
if validation_errors?
|
21
|
+
formatted_validation_errors
|
22
|
+
else
|
23
|
+
not_formatted_errors.map { |error| format_error(error) }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :not_formatted_errors
|
30
|
+
|
31
|
+
def validation_errors?
|
32
|
+
defined?(ActiveModel) &&
|
33
|
+
defined?(ActiveModel::Errors) &&
|
34
|
+
not_formatted_errors.is_a?(ActiveModel::Errors)
|
35
|
+
end
|
36
|
+
|
37
|
+
def formatted_validation_errors
|
38
|
+
not_formatted_errors.map do |field, message|
|
39
|
+
ValidationError.new(message, field)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def format_error(error)
|
44
|
+
if error.is_a?(String)
|
45
|
+
ExecutionError.new(error)
|
46
|
+
elsif error.is_a?(GraphQL::ExecutionError)
|
47
|
+
error
|
48
|
+
elsif CustomExecutionError.accepts?(error)
|
49
|
+
message = error[:message] || error['message']
|
50
|
+
CustomExecutionError.new(message, error.except(:message, 'message'))
|
51
|
+
elsif error.respond_to?(:message)
|
52
|
+
ExecutionError.new(error.message)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphqlRails
|
4
|
+
# adds `.decorate` class method to any class. Handy when using with paginated responses
|
5
|
+
#
|
6
|
+
# usage:
|
7
|
+
# class FriendDecorator < SimpleDecorator
|
8
|
+
# include GraphqlRails::Decorator
|
9
|
+
#
|
10
|
+
# graphql.attribute :full_name
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# class User
|
14
|
+
# has_many :friends
|
15
|
+
# graphql.attribute :decorated_friends, paginated: true, type: 'FriendDecorator!'
|
16
|
+
#
|
17
|
+
# def decorated_friends
|
18
|
+
# FriendDecorator.decorate(friends)
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
module Decorator
|
22
|
+
require 'active_support/concern'
|
23
|
+
require 'graphql_rails/decorator/relation_decorator'
|
24
|
+
|
25
|
+
extend ActiveSupport::Concern
|
26
|
+
|
27
|
+
class_methods do
|
28
|
+
def decorate(object, *args)
|
29
|
+
if Decorator::RelationDecorator.decorates?(object)
|
30
|
+
Decorator::RelationDecorator.new(relation: object, decorator: self, decorator_args: args)
|
31
|
+
elsif object.nil?
|
32
|
+
nil
|
33
|
+
elsif object.is_a?(Array)
|
34
|
+
object.map { |item| new(item, *args) }
|
35
|
+
else
|
36
|
+
new(object, *args)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphqlRails
|
4
|
+
module Decorator
|
5
|
+
# wrapps active record relation and returns decorated object instead
|
6
|
+
class RelationDecorator
|
7
|
+
delegate :map, :each, to: :to_a
|
8
|
+
delegate :limit_value, :offset_value, :count, :size, to: :relation
|
9
|
+
|
10
|
+
def self.decorates?(object)
|
11
|
+
(defined?(ActiveRecord) && object.is_a?(ActiveRecord::Relation)) ||
|
12
|
+
defined?(Mongoid) && object.is_a?(Mongoid::Criteria)
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(decorator:, relation:, decorator_args: [])
|
16
|
+
@relation = relation
|
17
|
+
@decorator = decorator
|
18
|
+
@decorator_args = decorator_args
|
19
|
+
end
|
20
|
+
|
21
|
+
%i[where limit order group offset from select having all unscope].each do |method_name|
|
22
|
+
define_method method_name do |*args, &block|
|
23
|
+
chainable_method(method_name, *args, &block)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
%i[first second last].each do |method_name|
|
28
|
+
define_method method_name do |*args, &block|
|
29
|
+
decoratable_object_method(method_name, *args, &block)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
%i[find_each].each do |method_name|
|
34
|
+
define_method method_name do |*args, &block|
|
35
|
+
decoratable_block_method(method_name, *args, &block)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_a
|
40
|
+
@to_a ||= relation.to_a.map { |it| decorator.new(it, *decorator_args) }
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
attr_reader :relation, :decorator, :decorator_args
|
46
|
+
|
47
|
+
def decoratable_object_method(method_name, *args, &block)
|
48
|
+
object = relation.public_send(method_name, *args, &block)
|
49
|
+
decorate(object)
|
50
|
+
end
|
51
|
+
|
52
|
+
def decorate(object_or_list)
|
53
|
+
return object_or_list if object_or_list.blank?
|
54
|
+
|
55
|
+
if object_or_list.is_a?(Array)
|
56
|
+
object_or_list.map { |it| decorator.new(it, *decorator_args) }
|
57
|
+
else
|
58
|
+
decorator.new(object_or_list, *decorator_args)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def decoratable_block_method(method_name, *args)
|
63
|
+
relation.public_send(method_name, *args) do |object, *other_args|
|
64
|
+
decorated_object = decorate(object)
|
65
|
+
yield(decorated_object, *other_args)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def chainable_method(method_name, *args, &block)
|
70
|
+
new_relation = relation.public_send(method_name, *args, &block)
|
71
|
+
self.class.new(decorator: decorator, relation: new_relation, decorator_args: decorator_args)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphqlRails
|
4
|
+
# base class which is returned in case something bad happens. Contains all error rendering tructure
|
5
|
+
class CustomExecutionError < ExecutionError
|
6
|
+
attr_reader :extra_graphql_data
|
7
|
+
|
8
|
+
def self.accepts?(error)
|
9
|
+
error.is_a?(Hash) &&
|
10
|
+
(error.key?(:message) || error.key?('message'))
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(message, extra_graphql_data = {})
|
14
|
+
super(message)
|
15
|
+
@extra_graphql_data = extra_graphql_data.stringify_keys
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_h
|
19
|
+
super
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|