grumlin 0.22.4 → 1.0.0.rc1

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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +0 -1
  3. data/.rubocop.yml +9 -9
  4. data/Gemfile.lock +10 -8
  5. data/README.md +102 -141
  6. data/Rakefile +1 -1
  7. data/bin/console +18 -3
  8. data/doc/middlewares.md +97 -0
  9. data/grumlin.gemspec +1 -0
  10. data/lib/async/channel.rb +54 -56
  11. data/lib/grumlin/benchmark/repository.rb +10 -14
  12. data/lib/grumlin/client.rb +92 -112
  13. data/lib/grumlin/config.rb +30 -15
  14. data/lib/grumlin/dummy_transaction.rb +13 -15
  15. data/lib/grumlin/edge.rb +18 -20
  16. data/lib/grumlin/expressions/cardinality.rb +5 -9
  17. data/lib/grumlin/expressions/column.rb +5 -9
  18. data/lib/grumlin/expressions/expression.rb +7 -11
  19. data/lib/grumlin/expressions/operator.rb +5 -9
  20. data/lib/grumlin/expressions/order.rb +5 -9
  21. data/lib/grumlin/expressions/p.rb +27 -31
  22. data/lib/grumlin/expressions/pop.rb +5 -9
  23. data/lib/grumlin/expressions/scope.rb +5 -9
  24. data/lib/grumlin/expressions/t.rb +5 -9
  25. data/lib/grumlin/expressions/text_p.rb +5 -9
  26. data/lib/grumlin/expressions/with_options.rb +17 -21
  27. data/lib/grumlin/features/feature_list.rb +8 -12
  28. data/lib/grumlin/features/neptune_features.rb +5 -9
  29. data/lib/grumlin/features/tinkergraph_features.rb +5 -9
  30. data/lib/grumlin/features.rb +8 -10
  31. data/lib/grumlin/middlewares/apply_shortcuts.rb +8 -0
  32. data/lib/grumlin/middlewares/build_query.rb +20 -0
  33. data/lib/grumlin/middlewares/builder.rb +15 -0
  34. data/lib/grumlin/middlewares/cast_results.rb +7 -0
  35. data/lib/grumlin/middlewares/find_blocklisted_steps.rb +14 -0
  36. data/lib/grumlin/middlewares/find_mutating_steps.rb +9 -0
  37. data/lib/grumlin/middlewares/middleware.rb +11 -0
  38. data/lib/grumlin/middlewares/run_query.rb +7 -0
  39. data/lib/grumlin/middlewares/serialize_to_bytecode.rb +9 -0
  40. data/lib/grumlin/middlewares/serialize_to_steps.rb +8 -0
  41. data/lib/grumlin/path.rb +11 -13
  42. data/lib/grumlin/property.rb +14 -16
  43. data/lib/grumlin/query_validators/blocklisted_steps_validator.rb +22 -0
  44. data/lib/grumlin/query_validators/validator.rb +36 -0
  45. data/lib/grumlin/repository/error_handling_strategy.rb +36 -40
  46. data/lib/grumlin/repository/instance_methods.rb +115 -118
  47. data/lib/grumlin/repository.rb +82 -58
  48. data/lib/grumlin/request_dispatcher.rb +55 -57
  49. data/lib/grumlin/request_error_factory.rb +53 -55
  50. data/lib/grumlin/shortcut.rb +19 -21
  51. data/lib/grumlin/shortcuts/properties.rb +12 -16
  52. data/lib/grumlin/shortcuts/storage.rb +67 -74
  53. data/lib/grumlin/shortcuts/upserts.rb +18 -22
  54. data/lib/grumlin/shortcuts.rb +23 -25
  55. data/lib/grumlin/shortcuts_applyer.rb +27 -29
  56. data/lib/grumlin/step.rb +92 -0
  57. data/lib/grumlin/step_data.rb +12 -14
  58. data/lib/grumlin/steppable.rb +24 -22
  59. data/lib/grumlin/steps.rb +51 -54
  60. data/lib/grumlin/steps_serializers/bytecode.rb +53 -56
  61. data/lib/grumlin/steps_serializers/human_readable_bytecode.rb +17 -21
  62. data/lib/grumlin/steps_serializers/serializer.rb +7 -11
  63. data/lib/grumlin/steps_serializers/string.rb +26 -30
  64. data/lib/grumlin/test/rspec/db_cleaner_context.rb +8 -12
  65. data/lib/grumlin/test/rspec/gremlin_context.rb +18 -16
  66. data/lib/grumlin/test/rspec.rb +1 -5
  67. data/lib/grumlin/transaction.rb +26 -27
  68. data/lib/grumlin/transport.rb +71 -73
  69. data/lib/grumlin/traversal_start.rb +31 -33
  70. data/lib/grumlin/traversal_strategies/options_strategy.rb +3 -7
  71. data/lib/grumlin/traverser.rb +5 -7
  72. data/lib/grumlin/typed_value.rb +11 -13
  73. data/lib/grumlin/typing.rb +70 -72
  74. data/lib/grumlin/version.rb +1 -1
  75. data/lib/grumlin/vertex.rb +14 -16
  76. data/lib/grumlin/vertex_property.rb +14 -16
  77. data/lib/grumlin/with_extension.rb +17 -19
  78. data/lib/grumlin.rb +23 -19
  79. metadata +32 -6
  80. data/lib/grumlin/action.rb +0 -92
  81. data/lib/grumlin/sugar.rb +0 -15
@@ -1,39 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Grumlin
4
- module Expressions
5
- class P
6
- class Predicate
7
- attr_reader :namespace, :name, :value, :type
3
+ class Grumlin::Expressions::P
4
+ class Predicate
5
+ attr_reader :namespace, :name, :value, :type
8
6
 
9
- def initialize(namespace, name, value:, type: nil)
10
- @namespace = namespace
11
- @name = name
12
- @value = value
13
- @type = type
14
- end
15
- end
7
+ def initialize(namespace, name, value:, type: nil)
8
+ @namespace = namespace
9
+ @name = name
10
+ @value = value
11
+ @type = type
12
+ end
13
+ end
16
14
 
17
- class << self
18
- # TODO: support more predicates
19
- %i[eq gt lt neq].each do |predicate|
20
- define_method predicate do |*args|
21
- Predicate.new("P", predicate, value: args[0])
22
- end
23
- end
15
+ class << self
16
+ # TODO: support more predicates
17
+ [:eq, :gt, :lt, :neq].each do |predicate|
18
+ define_method predicate do |*args|
19
+ Predicate.new("P", predicate, value: args[0])
20
+ end
21
+ end
24
22
 
25
- %i[within without].each do |predicate|
26
- define_method predicate do |*args|
27
- args = if args.count == 1 && args[0].is_a?(Array)
28
- args[0]
29
- elsif args.count == 1 && args[0].is_a?(Set)
30
- args[0].to_a
31
- else
32
- args.to_a
33
- end
34
- Predicate.new("P", predicate, value: args, type: "List")
35
- end
36
- end
23
+ [:within, :without].each do |predicate|
24
+ define_method predicate do |*args|
25
+ args = if args.count == 1 && args[0].is_a?(Array)
26
+ args[0]
27
+ elsif args.count == 1 && args[0].is_a?(Set)
28
+ args[0].to_a
29
+ else
30
+ args.to_a
31
+ end
32
+ Predicate.new("P", predicate, value: args, type: "List")
37
33
  end
38
34
  end
39
35
  end
@@ -1,15 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Grumlin
4
- module Expressions
5
- module Pop
6
- SUPPORTED_STEPS = Grumlin.definitions.dig(:expressions, :pop).map(&:to_sym).freeze
3
+ module Grumlin::Expressions::Pop
4
+ SUPPORTED_STEPS = Grumlin.definitions.dig(:expressions, :pop).map(&:to_sym).freeze
7
5
 
8
- class << self
9
- extend Expression
6
+ class << self
7
+ extend Grumlin::Expressions::Expression
10
8
 
11
- define_steps(SUPPORTED_STEPS, "Pop")
12
- end
13
- end
9
+ define_steps(SUPPORTED_STEPS, "Pop")
14
10
  end
15
11
  end
@@ -1,15 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Grumlin
4
- module Expressions
5
- module Scope
6
- SUPPORTED_STEPS = Grumlin.definitions.dig(:expressions, :scope).map(&:to_sym).freeze
3
+ module Grumlin::Expressions::Scope
4
+ SUPPORTED_STEPS = Grumlin.definitions.dig(:expressions, :scope).map(&:to_sym).freeze
7
5
 
8
- class << self
9
- extend Expression
6
+ class << self
7
+ extend Grumlin::Expressions::Expression
10
8
 
11
- define_steps(SUPPORTED_STEPS, "Scope")
12
- end
13
- end
9
+ define_steps(SUPPORTED_STEPS, "Scope")
14
10
  end
15
11
  end
@@ -1,15 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Grumlin
4
- module Expressions
5
- module T
6
- SUPPORTED_STEPS = Grumlin.definitions.dig(:expressions, :t).map(&:to_sym).freeze
3
+ module Grumlin::Expressions::T
4
+ SUPPORTED_STEPS = Grumlin.definitions.dig(:expressions, :t).map(&:to_sym).freeze
7
5
 
8
- class << self
9
- extend Expression
6
+ class << self
7
+ extend Grumlin::Expressions::Expression
10
8
 
11
- define_steps(SUPPORTED_STEPS, "T")
12
- end
13
- end
9
+ define_steps(SUPPORTED_STEPS, "T")
14
10
  end
15
11
  end
@@ -1,14 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Grumlin
4
- module Expressions
5
- class TextP < P
6
- class << self
7
- %i[containing endingWith notContaining notEndingWith notStartingWith startingWith].each do |predicate|
8
- define_method predicate do |*args|
9
- P::Predicate.new("TextP", predicate, value: args[0])
10
- end
11
- end
3
+ class Grumlin::Expressions::TextP < Grumlin::Expressions::P
4
+ class << self
5
+ [:containing, :endingWith, :notContaining, :notEndingWith, :notStartingWith, :startingWith].each do |predicate|
6
+ define_method predicate do |*args|
7
+ P::Predicate.new("TextP", predicate, value: args[0])
12
8
  end
13
9
  end
14
10
  end
@@ -1,31 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Grumlin
4
- module Expressions
5
- class WithOptions
6
- WITH_OPTIONS = Grumlin.definitions.dig(:expressions, :with_options).freeze
3
+ class Grumlin::Expressions::WithOptions
4
+ WITH_OPTIONS = Grumlin.definitions.dig(:expressions, :with_options).freeze
7
5
 
8
- class << self
9
- WITH_OPTIONS.each do |k, v|
10
- define_method k do
11
- name = "@#{k}"
12
- return instance_variable_get(name) if instance_variable_defined?(name)
6
+ class << self
7
+ WITH_OPTIONS.each do |k, v|
8
+ define_method k do
9
+ name = "@#{k}"
10
+ return instance_variable_get(name) if instance_variable_defined?(name)
13
11
 
14
- instance_variable_set(name, WithOptions.new(k, v))
15
- end
16
- end
12
+ instance_variable_set(name, WithOptions.new(k, v))
17
13
  end
14
+ end
15
+ end
18
16
 
19
- attr_reader :name, :value
17
+ attr_reader :name, :value
20
18
 
21
- def initialize(name, value)
22
- @name = name
23
- @value = value
24
- end
19
+ def initialize(name, value)
20
+ @name = name
21
+ @value = value
22
+ end
25
23
 
26
- def to_s
27
- "WithOptions.#{@name}"
28
- end
29
- end
24
+ def to_s
25
+ "WithOptions.#{@name}"
30
26
  end
31
27
  end
@@ -1,19 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Grumlin
4
- module Features
5
- class FeatureList
6
- def user_supplied_ids?
7
- raise(NotImplementedError) if @user_supplied_ids.nil?
3
+ class Grumlin::Features::FeatureList
4
+ def user_supplied_ids?
5
+ raise(NotImplementedError) if @user_supplied_ids.nil?
8
6
 
9
- @user_supplied_ids
10
- end
7
+ @user_supplied_ids
8
+ end
11
9
 
12
- def supports_transactions?
13
- raise(NotImplementedError) if @supports_transactions.nil?
10
+ def supports_transactions?
11
+ raise(NotImplementedError) if @supports_transactions.nil?
14
12
 
15
- @supports_transactions
16
- end
17
- end
13
+ @supports_transactions
18
14
  end
19
15
  end
@@ -1,13 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Grumlin
4
- module Features
5
- class NeptuneFeatures < FeatureList
6
- def initialize
7
- super
8
- @user_supplied_ids = true
9
- @supports_transactions = true
10
- end
11
- end
3
+ class Grumlin::Features::NeptuneFeatures < Grumlin::Features::FeatureList
4
+ def initialize
5
+ super
6
+ @user_supplied_ids = true
7
+ @supports_transactions = true
12
8
  end
13
9
  end
@@ -1,13 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Grumlin
4
- module Features
5
- class TinkergraphFeatures < FeatureList
6
- def initialize
7
- super
8
- @user_supplied_ids = true
9
- @supports_transactions = false
10
- end
11
- end
3
+ class Grumlin::Features::TinkergraphFeatures < Grumlin::Features::FeatureList
4
+ def initialize
5
+ super
6
+ @user_supplied_ids = true
7
+ @supports_transactions = false
12
8
  end
13
9
  end
@@ -1,16 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Grumlin
4
- module Features
5
- class << self
6
- FEATURES = {
7
- neptune: NeptuneFeatures.new,
8
- tinkergraph: TinkergraphFeatures.new
9
- }.freeze
3
+ module Grumlin::Features
4
+ class << self
5
+ FEATURES = {
6
+ neptune: NeptuneFeatures.new,
7
+ tinkergraph: TinkergraphFeatures.new
8
+ }.freeze
10
9
 
11
- def for(provider)
12
- FEATURES[provider]
13
- end
10
+ def for(provider)
11
+ FEATURES[provider]
14
12
  end
15
13
  end
16
14
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Grumlin::Middlewares::ApplyShortcuts < Grumlin::Middlewares::Middleware
4
+ def call(env)
5
+ env[:steps_without_shortcuts] = Grumlin::ShortcutsApplyer.call(env[:steps])
6
+ @app.call(env)
7
+ end
8
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Grumlin::Middlewares::BuildQuery < Grumlin::Middlewares::Middleware
4
+ def call(env)
5
+ env[:query] = {
6
+ requestId: SecureRandom.uuid,
7
+ op: :bytecode,
8
+ processor: env[:session_id] ? :session : :traversal,
9
+ args: {
10
+ gremlin: {
11
+ :@type => "g:Bytecode",
12
+ :@value => env[:bytecode]
13
+ },
14
+ aliases: { g: :g },
15
+ session: env[:session_id]
16
+ }.compact
17
+ }
18
+ @app.call(env)
19
+ end
20
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Grumlin::Middlewares::Builder < ::Middleware::Builder
4
+ def similar?(other)
5
+ stack == other.stack
6
+ end
7
+
8
+ def include?(middleware)
9
+ stack.any? { |m| m.first == middleware }
10
+ end
11
+
12
+ def to_app
13
+ @to_app ||= super
14
+ end
15
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Grumlin::Middlewares::CastResults < Grumlin::Middlewares::Middleware
4
+ def call(env)
5
+ env[:parsed_results] = @app.call(env).flat_map { |item| Grumlin::Typing.cast(item) }
6
+ end
7
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Grumlin::Middlewares::FindBlocklistedSteps < Grumlin::Middlewares::Middleware
4
+ def initialize(app, *steps)
5
+ super(app)
6
+ @validator = Grumlin::QueryValidators::BlocklistedStepsValidator.new(*steps)
7
+ end
8
+
9
+ def call(env)
10
+ @validator.validate!(env[:steps_without_shortcuts])
11
+ @app.call(env)
12
+ env[:parsed_results] = @app.call(env).flat_map { |item| Grumlin::Typing.cast(item) }
13
+ end
14
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Grumlin::Middlewares::FindMutatingSteps < Grumlin::Middlewares::FindBlocklistedSteps
4
+ MUTATING_STEPS = [:addV, :addE, :property, :drop].freeze
5
+
6
+ def initialize(app)
7
+ super(app, *MUTATING_STEPS)
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Grumlin::Middlewares::Middleware
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ raise NotImplementedError
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Grumlin::Middlewares::RunQuery < Grumlin::Middlewares::Middleware
4
+ def call(env)
5
+ env[:results] = env[:pool].acquire { |c| c.write(env[:query]) }
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Grumlin::Middlewares::SerializeToBytecode < Grumlin::Middlewares::Middleware
4
+ def call(env)
5
+ env[:bytecode] = Grumlin::StepsSerializers::Bytecode.new(env[:steps_without_shortcuts],
6
+ no_return: !env[:need_results]).serialize
7
+ @app.call(env)
8
+ end
9
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Grumlin::Middlewares::SerializeToSteps < Grumlin::Middlewares::Middleware
4
+ def call(env)
5
+ env[:steps] = Grumlin::Steps.from(env[:traversal])
6
+ @app.call(env)
7
+ end
8
+ end
data/lib/grumlin/path.rb CHANGED
@@ -1,20 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Grumlin
4
- class Path
5
- attr_reader :objects
3
+ class Grumlin::Path
4
+ attr_reader :objects
6
5
 
7
- def initialize(path)
8
- @labels = Typing.cast(path[:labels])
9
- @objects = Typing.cast(path[:objects])
10
- end
6
+ def initialize(path)
7
+ @labels = Grumlin::Typing.cast(path[:labels])
8
+ @objects = Grumlin::Typing.cast(path[:objects])
9
+ end
11
10
 
12
- def inspect
13
- "p[#{@objects}]"
14
- end
11
+ def inspect
12
+ "p[#{@objects}]"
13
+ end
15
14
 
16
- def to_s
17
- inspect
18
- end
15
+ def to_s
16
+ inspect
19
17
  end
20
18
  end
@@ -1,24 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Grumlin
4
- class Property
5
- attr_reader :key, :value
3
+ class Grumlin::Property
4
+ attr_reader :key, :value
6
5
 
7
- def initialize(value)
8
- @key = value[:key]
9
- @value = Typing.cast(value[:value])
10
- end
6
+ def initialize(value)
7
+ @key = value[:key]
8
+ @value = Grumlin::Typing.cast(value[:value])
9
+ end
11
10
 
12
- def inspect
13
- "p[#{key}->#{value}]"
14
- end
11
+ def inspect
12
+ "p[#{key}->#{value}]"
13
+ end
15
14
 
16
- def to_s
17
- inspect
18
- end
15
+ def to_s
16
+ inspect
17
+ end
19
18
 
20
- def ==(other)
21
- self.class == other.class && @key == other.key && @value == other.value
22
- end
19
+ def ==(other)
20
+ self.class == other.class && @key == other.key && @value == other.value
23
21
  end
24
22
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Grumlin::QueryValidators::BlocklistedStepsValidator < Grumlin::QueryValidators::Validator
4
+ def initialize(*names)
5
+ super()
6
+ @names = names.to_set
7
+ end
8
+
9
+ protected
10
+
11
+ def validate(steps, errors)
12
+ (steps.configuration_steps + steps.steps).each do |step|
13
+ if @names.include?(step.name)
14
+ errors[:blocklisted_steps] ||= []
15
+ errors[:blocklisted_steps] << step.name
16
+ end
17
+ step.args.each do |arg|
18
+ validate(arg, errors) if arg.is_a?(Grumlin::Steps)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Grumlin::QueryValidators::Validator
4
+ class ValidationError < Grumlin::Error
5
+ attr_reader :errors, :steps
6
+
7
+ def initialize(steps, errors)
8
+ super("Query is invalid: #{errors}")
9
+ @steps = steps
10
+ @errors = errors
11
+ end
12
+ end
13
+
14
+ # steps is an instance of `Steps` after shortcuts applied
15
+ def validate!(steps)
16
+ return unless (err = errors(steps)).any?
17
+
18
+ raise ValidationError.new(steps, err)
19
+ end
20
+
21
+ def valid?(steps)
22
+ errors(steps).empty?
23
+ end
24
+
25
+ protected
26
+
27
+ def errors(steps)
28
+ {}.tap do |errors|
29
+ validate(steps, errors)
30
+ end
31
+ end
32
+
33
+ def validate(steps, errors)
34
+ raise NotImplementedError
35
+ end
36
+ end
@@ -1,44 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Grumlin
4
- module Repository
5
- class ErrorHandlingStrategy
6
- def initialize(mode: :retry, **params)
7
- @mode = mode
8
- @params = params
9
- @on_exceptions = params[:on]
10
- end
11
-
12
- def raise?
13
- @mode == :raise
14
- end
15
-
16
- def ignore?
17
- @mode == :ignore
18
- end
19
-
20
- def retry?
21
- @mode == :retry
22
- end
23
-
24
- def apply!(&block)
25
- return yield if raise?
26
- return ignore_errors!(&block) if ignore?
27
-
28
- retry_errors!(&block)
29
- end
30
-
31
- private
32
-
33
- def ignore_errors!
34
- yield
35
- rescue *@on_exceptions
36
- # ignore errors
37
- end
38
-
39
- def retry_errors!(&block)
40
- Retryable.retryable(**@params, &block)
41
- end
42
- end
3
+ class Grumlin::Repository::ErrorHandlingStrategy
4
+ def initialize(mode: :retry, **params)
5
+ @mode = mode
6
+ @params = params
7
+ @on_exceptions = params[:on]
8
+ end
9
+
10
+ def raise?
11
+ @mode == :raise
12
+ end
13
+
14
+ def ignore?
15
+ @mode == :ignore
16
+ end
17
+
18
+ def retry?
19
+ @mode == :retry
20
+ end
21
+
22
+ def apply!(&block)
23
+ return yield if raise?
24
+ return ignore_errors!(&block) if ignore?
25
+
26
+ retry_errors!(&block)
27
+ end
28
+
29
+ private
30
+
31
+ def ignore_errors!
32
+ yield
33
+ rescue *@on_exceptions
34
+ # ignore errors
35
+ end
36
+
37
+ def retry_errors!(&block)
38
+ Retryable.retryable(**@params, &block)
43
39
  end
44
40
  end