thunderoperation 0.2.0

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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +3 -0
  3. data/config/default.yml +11 -0
  4. data/lib/rubocop/cop/thunderoperation/call_handlers.rb +56 -0
  5. data/lib/rubocop/cop/thunderoperation/def_outside_steps.rb +39 -0
  6. data/lib/rubocop/cop/thunderoperation/flow_required.rb +24 -0
  7. data/lib/rubocop/cop/thunderoperation/missing_steps.rb +42 -0
  8. data/lib/rubocop/cop/thunderoperation/operation_helpers.rb +124 -0
  9. data/lib/rubocop/cop/thunderoperation/operation_section_order.rb +51 -0
  10. data/lib/rubocop/cop/thunderoperation/steps_cannot_call_steps.rb +36 -0
  11. data/lib/rubocop/cop/thunderoperation/steps_order.rb +52 -0
  12. data/lib/rubocop/cop/thunderoperation/unknown_step.rb +61 -0
  13. data/lib/rubocop/cop/thunderoperation/unused_step.rb +29 -0
  14. data/lib/thunderoperation/adapter.rb +23 -0
  15. data/lib/thunderoperation/adapters/framer.rb +64 -0
  16. data/lib/thunderoperation/adapters/interruptable.rb +19 -0
  17. data/lib/thunderoperation/adapters/rich_parameters.rb +42 -0
  18. data/lib/thunderoperation/adapters/validations.rb +49 -0
  19. data/lib/thunderoperation/base.rb +144 -0
  20. data/lib/thunderoperation/clauses/base.rb +11 -0
  21. data/lib/thunderoperation/clauses/transaction.rb +11 -0
  22. data/lib/thunderoperation/clauses.rb +11 -0
  23. data/lib/thunderoperation/concerns/has_adapter.rb +47 -0
  24. data/lib/thunderoperation/config.rb +30 -0
  25. data/lib/thunderoperation/exceptions/trail.rb +27 -0
  26. data/lib/thunderoperation/exceptions.rb +32 -0
  27. data/lib/thunderoperation/flow/builder.rb +31 -0
  28. data/lib/thunderoperation/flow.rb +57 -0
  29. data/lib/thunderoperation/handlers.rb +43 -0
  30. data/lib/thunderoperation/lookup.rb +13 -0
  31. data/lib/thunderoperation/parameters/array_parameter.rb +48 -0
  32. data/lib/thunderoperation/parameters/base.rb +92 -0
  33. data/lib/thunderoperation/parameters/hash_parameter.rb +154 -0
  34. data/lib/thunderoperation/parameters.rb +11 -0
  35. data/lib/thunderoperation/response.rb +62 -0
  36. data/lib/thunderoperation/rubocop.rb +31 -0
  37. data/lib/thunderoperation/steps/base.rb +44 -0
  38. data/lib/thunderoperation/steps/clause.rb +32 -0
  39. data/lib/thunderoperation/steps/method.rb +34 -0
  40. data/lib/thunderoperation/steps/render.rb +22 -0
  41. data/lib/thunderoperation/steps/success.rb +29 -0
  42. data/lib/thunderoperation/steps.rb +33 -0
  43. data/lib/thunderoperation/steps_definition.rb +75 -0
  44. data/lib/thunderoperation/version.rb +5 -0
  45. data/lib/thunderoperation.rb +36 -0
  46. metadata +108 -0
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThunderOperation
4
+ module Adapters
5
+ class RichParameters < Adapter
6
+ def execute(_params, operation_instance, _options, &block)
7
+ operation_instance._load_params
8
+
9
+ block.call
10
+ end
11
+
12
+ adapted do
13
+ def self.param(*, &)
14
+ @param_ivars = nil
15
+ base_parameter.param(*, &)
16
+ end
17
+
18
+ def self.base_parameter
19
+ @base_parameter ||= Parameters::HashParameter.new
20
+ end
21
+
22
+ def self.param_ivars
23
+ @param_ivars ||= base_parameter.output_keys.to_h { |key| [key, :"@#{key}"] }.freeze
24
+ end
25
+
26
+ def _reportable_params
27
+ return unless defined?(@sanitized_params) && @sanitized_params
28
+
29
+ self.class.base_parameter.filter(@sanitized_params)
30
+ end
31
+
32
+ def _load_params
33
+ @sanitized_params = self.class.base_parameter.sanitize(@params || {})
34
+
35
+ self.class.param_ivars.each do |key, ivar|
36
+ instance_variable_set(ivar, @sanitized_params[key]) if @sanitized_params.key?(key)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThunderOperation
4
+ module Adapters
5
+ class Validations < Adapter
6
+ def execute(_params, _operation_instance, _options, &block)
7
+ validate_steps!
8
+
9
+ response = block.call
10
+ validate_return!(response) if check_return?(response)
11
+
12
+ response
13
+ end
14
+
15
+ private
16
+
17
+ def validate_steps!
18
+ steps = operation_class.steps
19
+ return if steps.equal?(StepsDefinition::NONE)
20
+
21
+ unknown = operation_class.flow.method_names - steps.names
22
+ return if unknown.empty?
23
+
24
+ raise Exceptions::UnknownStep,
25
+ "#{operation_class.name}: flow calls #{unknown.join(', ')}, not defined in `steps do ... end`"
26
+ end
27
+
28
+ # WARN: AI MADE
29
+ # Only with ThunderOperation.config.validate_returns, and not when an adapter renders the payload instead.
30
+ def check_return?(response)
31
+ response.success? && ThunderOperation.config.validate_returns &&
32
+ operation_class.returns.is_a?(Parameters::Base) && !operation_class.renders_returns?
33
+ end
34
+
35
+ # A missing payload fails unless the returns is optional: `success` without a payload is a mistake too.
36
+ def validate_return!(response)
37
+ returns = operation_class.returns
38
+ if response.payload.nil? && !returns.optional?
39
+ raise Exceptions::ParameterError.new("Invalid value for 'returns': expected #{returns.type}, got NilClass",
40
+ field: 'returns')
41
+ end
42
+
43
+ returns.check!(response.payload, path: 'returns')
44
+ rescue Exceptions::ParameterError => e
45
+ raise Exceptions::ReturnTypeError, "#{operation_class.name}: #{e.message} (#{response.payload.inspect})"
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThunderOperation
4
+ class Base
5
+ include Concerns::HasAdapter
6
+
7
+ class OptionsProxy
8
+ def initialize(operation_class, options)
9
+ @operation_class = operation_class
10
+ @options = options
11
+ end
12
+
13
+ def call(params = nil, **kwargs, &block)
14
+ @operation_class.execute(params || kwargs, @options, &block)
15
+ end
16
+
17
+ def call!(params = nil, **kwargs, &block)
18
+ @operation_class.execute(params || kwargs, @options.merge(insecure_mode: true), &block)
19
+ end
20
+ end
21
+
22
+ NO_OPTIONS = {}.freeze
23
+ INSECURE_OPTIONS = { insecure_mode: true }.freeze
24
+
25
+ attr_accessor :_current_step
26
+
27
+ def initialize(params, options = {})
28
+ @params = params
29
+ @options = options
30
+ end
31
+
32
+ class << self
33
+ def flow(&block)
34
+ return @flow = Flow.build(&block) if block
35
+
36
+ @flow || (superclass.respond_to?(:flow) ? superclass.flow : Flow::EMPTY)
37
+ end
38
+
39
+ def steps(&block)
40
+ return @steps = StepsDefinition.build(self, &block) if block
41
+
42
+ @steps || StepsDefinition::NONE
43
+ end
44
+
45
+ # WARN: AI MADE
46
+ # What the operation returns, declared as a param: `returns Book::Relation`, `returns Hash do field ... end`,
47
+ # `returns Array do items Hash do ... end end`. A Symbol names a builder an adapter adds.
48
+ def returns(type = nil, options = {}, &block)
49
+ return @returns = returns_builder(type).call(self, &block) if type.is_a?(Symbol)
50
+ return @returns = Parameters::Base.from_args(*[type, options].compact, &block) if type || block
51
+
52
+ @returns || (superclass.respond_to?(:returns) ? superclass.returns : nil)
53
+ end
54
+
55
+ # True when an adapter renders the payload through `returns` (Framer) instead of only checking it.
56
+ def renders_returns? = false
57
+
58
+ def returns_builders
59
+ @returns_builders ||= superclass.respond_to?(:returns_builders) ? superclass.returns_builders.dup : {}
60
+ end
61
+
62
+ def call(params = nil, **kwargs, &block)
63
+ execute(params || kwargs, NO_OPTIONS, &block)
64
+ end
65
+
66
+ def call!(params = nil, **kwargs, &block)
67
+ execute(params || kwargs, INSECURE_OPTIONS, &block)
68
+ end
69
+
70
+ def failure_key_prefix
71
+ name.to_s.underscore.tr('/', '.')
72
+ end
73
+
74
+ def with(**options)
75
+ OptionsProxy.new(self, options)
76
+ end
77
+
78
+ def execute(params, options, &block)
79
+ raise ArgumentError, 'call! does not take a handlers block' if block && options[:insecure_mode]
80
+
81
+ response = run_instance(params, options)
82
+
83
+ block ? Handlers.run(response, &block) : response
84
+ end
85
+
86
+ private
87
+
88
+ def returns_builder(kind)
89
+ returns_builders.fetch(kind) do
90
+ raise ArgumentError, "unknown returns kind #{kind.inspect}; append the adapter that defines it"
91
+ end
92
+ end
93
+
94
+ def run_instance(params, options)
95
+ operation_instance = new(params, options)
96
+
97
+ handle_adapters(params, operation_instance, options) { operation_instance._run }
98
+ rescue StandardError => e
99
+ raise Exceptions::Trail.record(e, operation_instance) if options[:insecure_mode]
100
+
101
+ operation_instance._report_error(e)
102
+ operation_instance._failure(e)
103
+ end
104
+ end
105
+
106
+ def _run
107
+ self.class.flow.run(self) || _success
108
+ end
109
+
110
+ def _report_error(error)
111
+ handler = ThunderOperation.config.error_handler
112
+
113
+ return unless handler
114
+
115
+ level = error.is_a?(Exceptions::OperationError) ? :warning : :error
116
+ handler.call(error, level:, operation: self.class, step: _current_step&.name, params: _reportable_params)
117
+ end
118
+
119
+ def _reportable_params
120
+ nil
121
+ end
122
+
123
+ def raise_failure(message_key, extras: nil, status: nil)
124
+ raise Exceptions::OperationError.new(message_key, self.class, extras:, status:)
125
+ end
126
+
127
+ def _success(payload = nil, status: nil)
128
+ Response.new(was_success: true, payload:, status:, operation_name: self.class.name)
129
+ end
130
+
131
+ def _failure(exception, status = :unprocessable_entity, payload: nil)
132
+ Exceptions::Trail.record(exception, self)
133
+
134
+ Response.new(
135
+ was_success: false,
136
+ payload:,
137
+ operation_name: self.class.name,
138
+ error: exception,
139
+ status:,
140
+ failed_step: _current_step&.name
141
+ )
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThunderOperation
4
+ module Clauses
5
+ class Base
6
+ def call(_instance, *_args, **_kwargs, &_run)
7
+ raise NotImplementedError, "#{self.class.name}#call"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThunderOperation
4
+ module Clauses
5
+ class Transaction < Base
6
+ def call(_instance, &run)
7
+ ThunderOperation.config.transaction.call(&run)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThunderOperation
4
+ module Clauses
5
+ extend Lookup
6
+
7
+ def self.lookup(name)
8
+ resolve(name, base: Base, except: %w[Base])&.new&.freeze
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThunderOperation
4
+ module Concerns
5
+ module HasAdapter
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ end
9
+
10
+ module ClassMethods
11
+ def append_adapter(adapter_class)
12
+ adapters << adapter_class
13
+ @adapter_rings = nil
14
+
15
+ return unless adapter_class.adapted_block
16
+
17
+ class_exec(&adapter_class.adapted_block)
18
+ end
19
+
20
+ def adapters
21
+ @adapters ||= superclass.respond_to?(:adapters) ? superclass.adapters.dup : []
22
+ end
23
+
24
+ def handle_adapters(params, operation_instance, options, &block)
25
+ deep_ring, outer_ring = adapter_rings
26
+
27
+ deep_block = deep_ring.reduce(block) do |previous_block, adapter|
28
+ proc { adapter.deep_execute(params, operation_instance, options, &previous_block) }
29
+ end
30
+
31
+ outer_ring.reduce(deep_block) do |previous_block, adapter|
32
+ proc { adapter.execute(params, operation_instance, options, &previous_block) }
33
+ end.call
34
+ end
35
+
36
+ private
37
+
38
+ def adapter_rings
39
+ @adapter_rings ||= begin
40
+ instances = adapters.map { |adapter_class| adapter_class.new(self).freeze }
41
+ [instances.select { |adapter| adapter.respond_to?(:deep_execute) }.freeze, instances.freeze].freeze
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThunderOperation
4
+ class Config
5
+ attr_accessor :transaction
6
+
7
+ attr_accessor :error_handler
8
+
9
+ # WARN: AI MADE
10
+ # Whether a payload is checked against `returns` (Validations); off by default, so a mistake in a declaration
11
+ # does not fail an operation in production.
12
+ attr_accessor :validate_returns
13
+
14
+ def initialize
15
+ @transaction = ->(&block) { block.call }
16
+ @error_handler = nil
17
+ @validate_returns = false
18
+ end
19
+ end
20
+
21
+ class << self
22
+ def config
23
+ @config ||= Config.new
24
+ end
25
+
26
+ def reset_config!
27
+ @config = Config.new
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThunderOperation
4
+ module Exceptions
5
+ module Trail
6
+ Entry = Data.define(:operation_class, :step) do
7
+ def to_s
8
+ step ? "#{operation_class.name}##{step}" : operation_class.name.to_s
9
+ end
10
+ end
11
+
12
+ def self.record(error, operation)
13
+ return error if error.nil? || error.frozen?
14
+
15
+ error.extend(self) unless error.is_a?(self)
16
+ entry = Entry.new(operation.class, operation._current_step&.name)
17
+ error.operation_trail << entry unless error.operation_trail.last == entry
18
+
19
+ error
20
+ end
21
+
22
+ def operation_trail
23
+ @operation_trail ||= []
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThunderOperation
4
+ module Exceptions
5
+ class Interruption < StandardError; end
6
+
7
+ class ParameterError < StandardError
8
+ attr_reader :field
9
+
10
+ def initialize(message, field: nil)
11
+ @field = field
12
+ super(message)
13
+ end
14
+ end
15
+
16
+ class OperationError < StandardError
17
+ attr_reader :message_key, :extras, :status
18
+
19
+ def initialize(message_key, operation_class, extras: nil, status: nil)
20
+ @message_key = message_key
21
+ @extras = extras
22
+ @status = status
23
+
24
+ super("operation_exception.#{operation_class.failure_key_prefix}.#{message_key}")
25
+ end
26
+ end
27
+
28
+ class DeclarationError < ScriptError; end
29
+ class UnknownStep < DeclarationError; end
30
+ class ReturnTypeError < DeclarationError; end
31
+ end
32
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThunderOperation
4
+ class Flow
5
+ class Builder < BasicObject
6
+ def self.record(&block)
7
+ builder = new
8
+ builder.instance_eval(&block)
9
+ builder.__steps__
10
+ end
11
+
12
+ def initialize
13
+ @steps = []
14
+ end
15
+
16
+ def __steps__
17
+ @steps
18
+ end
19
+
20
+ def method_missing(name, *args, **kwargs, &block)
21
+ @steps << ::ThunderOperation::Steps.build(name, *args, **kwargs, &block)
22
+
23
+ nil
24
+ end
25
+
26
+ def respond_to_missing?(*)
27
+ true
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThunderOperation
4
+ class Flow
5
+ include Enumerable
6
+
7
+ attr_reader :steps
8
+
9
+ def self.build(&block)
10
+ new(Builder.record(&block))
11
+ end
12
+
13
+ def initialize(steps = [])
14
+ @steps = steps.dup.freeze
15
+ freeze
16
+ end
17
+
18
+ EMPTY = new
19
+
20
+ def each(&)
21
+ steps.each(&)
22
+ end
23
+
24
+ def names
25
+ steps.map(&:name)
26
+ end
27
+
28
+ def method_names
29
+ steps.flat_map do |step|
30
+ case step
31
+ when Steps::Method then [step.name]
32
+ when Steps::Clause then step.flow.method_names
33
+ else []
34
+ end
35
+ end.uniq
36
+ end
37
+
38
+ def empty?
39
+ steps.empty?
40
+ end
41
+
42
+ def run(instance)
43
+ steps.each do |step|
44
+ instance._current_step = step
45
+ result = step.run(instance)
46
+
47
+ return result if result.is_a?(Response)
48
+ end
49
+
50
+ nil
51
+ end
52
+
53
+ def to_s
54
+ steps.map(&:to_s).join(', ')
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThunderOperation
4
+ class Handlers < BasicObject
5
+ def self.run(response, &block)
6
+ handlers = new
7
+ handlers.instance_exec(&block)
8
+ handlers.__dispatch__(response, block.binding.receiver)
9
+ response
10
+ end
11
+
12
+ def initialize
13
+ @success = nil
14
+ @failure = nil
15
+ end
16
+
17
+ def success(&block)
18
+ @success = block
19
+ end
20
+
21
+ def failure(&block)
22
+ @failure = block
23
+ end
24
+
25
+ def __dispatch__(response, receiver)
26
+ if response.success?
27
+ receiver.instance_exec(response, &@success) if @success
28
+ elsif @failure
29
+ receiver.instance_exec(response, &@failure)
30
+ else
31
+ ::Kernel.raise response.error
32
+ end
33
+ end
34
+
35
+ def method_missing(name, *)
36
+ ::Kernel.raise ::ArgumentError, "only `success` and `failure` are allowed in a call block, got `#{name}`"
37
+ end
38
+
39
+ def respond_to_missing?(*)
40
+ false
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThunderOperation
4
+ module Lookup
5
+ def resolve(name, base:, except: [])
6
+ const_name = name.to_s.camelize
7
+ return if except.include?(const_name) || !const_defined?(const_name, false)
8
+
9
+ klass = const_get(const_name, false)
10
+ klass if klass.is_a?(Class) && klass < base
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThunderOperation
4
+ module Parameters
5
+ class ArrayParameter < Base
6
+ def initialize(_type = nil, options = {}, &block)
7
+ super(Array, options)
8
+
9
+ @items = nil
10
+
11
+ instance_exec(&block) if block
12
+ end
13
+
14
+ def items(*args, &block)
15
+ return @items if args.empty? && block.nil?
16
+
17
+ @items = self.class.from_args(*args, &block)
18
+ end
19
+
20
+ def filter(values)
21
+ return FILTERED if sensitive?
22
+ return values if @items.nil? || values.nil?
23
+
24
+ values.map { |value| @items.filter(value) }
25
+ end
26
+
27
+ # WARN: AI MADE
28
+ def resolve_types!(&)
29
+ @items&.resolve_types!(&)
30
+ end
31
+
32
+ def check!(values, path: nil)
33
+ validate!(values, path:)
34
+ return if @items.nil? || values.nil?
35
+
36
+ values.each_with_index { |value, index| @items.check!(value, path: "#{path}[#{index}]") }
37
+ end
38
+
39
+ def sanitize(values, path: nil)
40
+ validate!(values, path:)
41
+
42
+ return values if @items.nil? || values.nil?
43
+
44
+ values.each_with_index.map { |value, index| @items.sanitize(value, path: "#{path}[#{index}]") }
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThunderOperation
4
+ module Parameters
5
+ class Base
6
+ FILTERED = '[FILTERED]'
7
+
8
+ attr_reader :type, :options
9
+
10
+ def self.from_args(*args, &block)
11
+ type, options = parse_args(args)
12
+
13
+ raise Exceptions::DeclarationError, "parameter type is required, got nil" if type.nil? && !block
14
+
15
+ klass = (Parameters.for_type(type) if type.is_a?(Class)) || (block ? HashParameter : Base)
16
+
17
+ klass.new(type, options, &block)
18
+ end
19
+
20
+ def self.parse_args(args)
21
+ # A String names a class the engine does not know yet ('Models::Supplier'); whoever loads the operation
22
+ # replaces it through resolve_types!.
23
+ case args
24
+ in [Class | String => type, Hash => options] then [type, options]
25
+ in [Hash => options] then [nil, options]
26
+ in [Class | String => type] then [type, {}]
27
+ else [nil, {}]
28
+ end
29
+ end
30
+ private_class_method :parse_args
31
+
32
+ def initialize(type = nil, options = {})
33
+ @type = type
34
+ @options = options
35
+ end
36
+
37
+ def optional?
38
+ @options[:optional] == true
39
+ end
40
+
41
+ def required?
42
+ !optional?
43
+ end
44
+
45
+ def sensitive?
46
+ @options[:sensitive] == true
47
+ end
48
+
49
+ def filter(value)
50
+ sensitive? ? FILTERED : value
51
+ end
52
+
53
+ def sanitize(value, path: nil)
54
+ validate!(value, path:)
55
+
56
+ value
57
+ end
58
+
59
+ def valid?(value)
60
+ if unresolved_type?
61
+ raise Exceptions::DeclarationError, "type '#{@type}' was never resolved to a class"
62
+ end
63
+
64
+ (@type.nil? || value.is_a?(@type)) ||
65
+ (options[:null] != false && value.nil?)
66
+ end
67
+
68
+ # WARN: AI MADE
69
+ # Replaces a type given by name with the class the block returns for it; nested parameters do the same.
70
+ def resolve_types!
71
+ @type = yield(@type) if @type.is_a?(String)
72
+ end
73
+
74
+ def unresolved_type? = @type.is_a?(String)
75
+
76
+ # The value has exactly the declared shape (returns and event payloads): the type, and for a Hash every
77
+ # declared key and no other. Raises ParameterError with the path of the first difference.
78
+ def check!(value, path: nil)
79
+ validate!(value, path:)
80
+ end
81
+
82
+ def validate!(value, path: nil)
83
+ return if valid?(value)
84
+
85
+ raise Exceptions::ParameterError.new(
86
+ "Invalid value for '#{path}': expected #{@type}, got #{value.class}",
87
+ field: path
88
+ )
89
+ end
90
+ end
91
+ end
92
+ end