graphql 1.8.0.pre1 → 1.8.0.pre2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/function_generator.rb +1 -1
  3. data/lib/generators/graphql/loader_generator.rb +1 -1
  4. data/lib/generators/graphql/mutation_generator.rb +6 -1
  5. data/lib/generators/graphql/templates/function.erb +2 -2
  6. data/lib/generators/graphql/templates/loader.erb +2 -2
  7. data/lib/graphql.rb +1 -0
  8. data/lib/graphql/execution.rb +1 -0
  9. data/lib/graphql/execution/instrumentation.rb +82 -0
  10. data/lib/graphql/execution/multiplex.rb +11 -28
  11. data/lib/graphql/field.rb +5 -0
  12. data/lib/graphql/internal_representation/node.rb +1 -1
  13. data/lib/graphql/language.rb +1 -0
  14. data/lib/graphql/language/document_from_schema_definition.rb +185 -0
  15. data/lib/graphql/language/lexer.rb +3 -3
  16. data/lib/graphql/language/lexer.rl +2 -2
  17. data/lib/graphql/language/token.rb +9 -2
  18. data/lib/graphql/query.rb +4 -0
  19. data/lib/graphql/railtie.rb +83 -0
  20. data/lib/graphql/relay/relation_connection.rb +13 -18
  21. data/lib/graphql/schema.rb +6 -0
  22. data/lib/graphql/schema/argument.rb +1 -1
  23. data/lib/graphql/schema/build_from_definition.rb +2 -0
  24. data/lib/graphql/schema/field.rb +5 -2
  25. data/lib/graphql/schema/input_object.rb +2 -2
  26. data/lib/graphql/schema/member.rb +10 -0
  27. data/lib/graphql/schema/member/build_type.rb +8 -0
  28. data/lib/graphql/schema/member/instrumentation.rb +3 -3
  29. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +6 -4
  30. data/lib/graphql/tracing.rb +1 -0
  31. data/lib/graphql/tracing/data_dog_tracing.rb +45 -0
  32. data/lib/graphql/tracing/platform_tracing.rb +20 -7
  33. data/lib/graphql/upgrader/member.rb +111 -0
  34. data/lib/graphql/upgrader/schema.rb +37 -0
  35. data/lib/graphql/version.rb +1 -1
  36. data/readme.md +1 -1
  37. data/spec/dummy/app/channels/graphql_channel.rb +22 -1
  38. data/spec/dummy/log/development.log +239 -0
  39. data/spec/dummy/log/test.log +204 -0
  40. data/spec/dummy/test/system/action_cable_subscription_test.rb +4 -0
  41. data/spec/dummy/tmp/screenshots/failures_test_it_handles_subscriptions.png +0 -0
  42. data/spec/generators/graphql/function_generator_spec.rb +26 -0
  43. data/spec/generators/graphql/loader_generator_spec.rb +24 -0
  44. data/spec/graphql/analysis/max_query_complexity_spec.rb +3 -3
  45. data/spec/graphql/analysis/max_query_depth_spec.rb +3 -3
  46. data/spec/graphql/base_type_spec.rb +12 -0
  47. data/spec/graphql/boolean_type_spec.rb +3 -3
  48. data/spec/graphql/execution/execute_spec.rb +1 -1
  49. data/spec/graphql/execution/instrumentation_spec.rb +165 -0
  50. data/spec/graphql/execution/multiplex_spec.rb +1 -1
  51. data/spec/graphql/float_type_spec.rb +2 -2
  52. data/spec/graphql/id_type_spec.rb +1 -1
  53. data/spec/graphql/input_object_type_spec.rb +2 -2
  54. data/spec/graphql/int_type_spec.rb +2 -2
  55. data/spec/graphql/internal_representation/rewrite_spec.rb +2 -2
  56. data/spec/graphql/introspection/schema_type_spec.rb +1 -0
  57. data/spec/graphql/language/document_from_schema_definition_spec.rb +337 -0
  58. data/spec/graphql/language/lexer_spec.rb +12 -1
  59. data/spec/graphql/language/parser_spec.rb +1 -1
  60. data/spec/graphql/query/arguments_spec.rb +3 -3
  61. data/spec/graphql/query/variables_spec.rb +1 -1
  62. data/spec/graphql/query_spec.rb +4 -4
  63. data/spec/graphql/relay/base_connection_spec.rb +1 -1
  64. data/spec/graphql/relay/connection_resolve_spec.rb +1 -1
  65. data/spec/graphql/relay/connection_type_spec.rb +1 -1
  66. data/spec/graphql/relay/mutation_spec.rb +3 -3
  67. data/spec/graphql/relay/relation_connection_spec.rb +58 -0
  68. data/spec/graphql/schema/build_from_definition_spec.rb +14 -0
  69. data/spec/graphql/schema/field_spec.rb +5 -1
  70. data/spec/graphql/schema/instrumentation_spec.rb +39 -0
  71. data/spec/graphql/schema/validation_spec.rb +1 -1
  72. data/spec/graphql/schema/warden_spec.rb +11 -11
  73. data/spec/graphql/schema_spec.rb +8 -1
  74. data/spec/graphql/string_type_spec.rb +3 -3
  75. data/spec/graphql/subscriptions_spec.rb +1 -1
  76. data/spec/graphql/tracing/platform_tracing_spec.rb +59 -0
  77. data/spec/graphql/upgrader/member_spec.rb +222 -0
  78. data/spec/graphql/upgrader/schema_spec.rb +82 -0
  79. data/spec/support/dummy/schema.rb +19 -0
  80. data/spec/support/jazz.rb +14 -14
  81. data/spec/support/star_wars/data.rb +1 -2
  82. metadata +18 -2
@@ -387,7 +387,7 @@ def self.run_lexer(query_string)
387
387
  begin
388
388
  te = p+1;
389
389
  begin
390
- emit_string(ts + 1, te - 1, meta)
390
+ emit_string(ts + 1, te, meta)
391
391
  end
392
392
 
393
393
  end
@@ -791,7 +791,7 @@ def self.run_lexer(query_string)
791
791
  begin
792
792
  p = ((te))-1;
793
793
  begin
794
- emit_string(ts + 1, te - 1, meta)
794
+ emit_string(ts + 1, te, meta)
795
795
  end
796
796
 
797
797
  end
@@ -1304,7 +1304,7 @@ PACK_DIRECTIVE = "c*"
1304
1304
  UTF_8_ENCODING = "UTF-8"
1305
1305
 
1306
1306
  def self.emit_string(ts, te, meta)
1307
- value = meta[:data][ts...te].pack(PACK_DIRECTIVE).force_encoding(UTF_8_ENCODING)
1307
+ value = meta[:data][ts...te - 1].pack(PACK_DIRECTIVE).force_encoding(UTF_8_ENCODING)
1308
1308
  if value !~ VALID_STRING
1309
1309
  meta[:tokens] << token = GraphQL::Language::Token.new(
1310
1310
  name: :BAD_UNICODE_ESCAPE,
@@ -75,7 +75,7 @@
75
75
  RBRACKET => { emit(:RBRACKET, ts, te, meta) };
76
76
  LBRACKET => { emit(:LBRACKET, ts, te, meta) };
77
77
  COLON => { emit(:COLON, ts, te, meta) };
78
- QUOTED_STRING => { emit_string(ts + 1, te - 1, meta) };
78
+ QUOTED_STRING => { emit_string(ts + 1, te, meta) };
79
79
  VAR_SIGN => { emit(:VAR_SIGN, ts, te, meta) };
80
80
  DIR_SIGN => { emit(:DIR_SIGN, ts, te, meta) };
81
81
  ELLIPSIS => { emit(:ELLIPSIS, ts, te, meta) };
@@ -189,7 +189,7 @@ module GraphQL
189
189
  UTF_8_ENCODING = "UTF-8"
190
190
 
191
191
  def self.emit_string(ts, te, meta)
192
- value = meta[:data][ts...te].pack(PACK_DIRECTIVE).force_encoding(UTF_8_ENCODING)
192
+ value = meta[:data][ts...te - 1].pack(PACK_DIRECTIVE).force_encoding(UTF_8_ENCODING)
193
193
  if value !~ VALID_STRING
194
194
  meta[:tokens] << token = GraphQL::Language::Token.new(
195
195
  name: :BAD_UNICODE_ESCAPE,
@@ -5,7 +5,10 @@ module GraphQL
5
5
  # Contains type, value and position data.
6
6
  class Token
7
7
  # @return [Symbol] The kind of token this is
8
- attr_reader :name, :prev_token, :line
8
+ attr_reader :name
9
+ # @return [String] The text of this token
10
+ attr_reader :value
11
+ attr_reader :prev_token, :line, :col
9
12
 
10
13
  def initialize(value:, name:, line:, col:, prev_token:)
11
14
  @name = name
@@ -15,13 +18,17 @@ module GraphQL
15
18
  @prev_token = prev_token
16
19
  end
17
20
 
18
- def to_s; @value; end
21
+ alias to_s value
19
22
  def to_i; @value.to_i; end
20
23
  def to_f; @value.to_f; end
21
24
 
22
25
  def line_and_column
23
26
  [@line, @col]
24
27
  end
28
+
29
+ def inspect
30
+ "(#{@name} #{@value.inspect} [#{@line}:#{@col}])"
31
+ end
25
32
  end
26
33
  end
27
34
  end
@@ -47,6 +47,10 @@ module GraphQL
47
47
  with_prepared_ast { @document }
48
48
  end
49
49
 
50
+ def inspect
51
+ "query ..."
52
+ end
53
+
50
54
  # @return [String, nil] The name of the operation to run (may be inferred)
51
55
  def selected_operation_name
52
56
  return nil unless selected_operation
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './upgrader/member'
4
+ require_relative './upgrader/schema'
5
+
6
+ module GraphQL
7
+ class Railtie < Rails::Railtie
8
+ rake_tasks do
9
+ namespace :graphql do
10
+ task :upgrade, [:dir] do |t, args|
11
+ unless (dir = args[:dir])
12
+ fail 'You have to give me a directory where your GraphQL schema and types live. ' \
13
+ 'For example: `bin/rake graphql:upgrade[app/graphql/**/*]`'
14
+ end
15
+
16
+ Dir[dir].each do |file|
17
+ # Members (types, interfaces, etc.)
18
+ if file =~ /.*_(type|interface|enum|union|)\.rb$/
19
+ Rake::Task["graphql:upgrade:member"].execute(Struct.new(:member_file).new(file))
20
+ end
21
+ end
22
+ end
23
+
24
+ namespace :upgrade do
25
+ task :create_base_objects, [:base_dir] do |t, args|
26
+ base_dir = args.base_dir
27
+
28
+ destination_file = File.join(base_dir, "types", "base_enum.rb")
29
+ unless File.exists?(destination_file)
30
+ FileUtils.mkdir_p(File.dirname(destination_file))
31
+ File.open(destination_file, 'w') do |f|
32
+ f.write "class Types::BaseEnum < GraphQL::Schema::Enum; end"
33
+ end
34
+ end
35
+
36
+ destination_file = File.join(base_dir, "types", "base_union.rb")
37
+ unless File.exists?(destination_file)
38
+ FileUtils.mkdir_p(File.dirname(destination_file))
39
+ File.open(destination_file, 'w') do |f|
40
+ f.write "class Types::BaseUnion < GraphQL::Schema::Union; end"
41
+ end
42
+ end
43
+
44
+ destination_file = File.join(base_dir, "types", "base_interface.rb")
45
+ unless File.exists?(destination_file)
46
+ FileUtils.mkdir_p(File.dirname(destination_file))
47
+ File.open(destination_file, 'w') do |f|
48
+ f.write "class Types::BaseInterface < GraphQL::Schema::Interface; end"
49
+ end
50
+ end
51
+
52
+ destination_file = File.join(base_dir, "types", "base_object.rb")
53
+ unless File.exists?(destination_file)
54
+ File.open(destination_file, 'w') do |f|
55
+ f.write "class Types::BaseObject < GraphQL::Schema::Object; end"
56
+ end
57
+ end
58
+ end
59
+
60
+ task :schema, [:schema_file] do |t, args|
61
+ schema_file = args.schema_file
62
+
63
+ upgrader = GraphQL::Upgrader::Schema.new File.read(schema_file)
64
+
65
+ puts "- Transforming schema #{schema_file}"
66
+ File.open(schema_file, 'w') { |f| f.write upgrader.upgrade }
67
+ end
68
+
69
+ task :member, [:member_file] do |t, args|
70
+ member_file = args.member_file
71
+
72
+ upgrader = GraphQL::Upgrader::Member.new File.read(member_file)
73
+ next unless upgrader.upgradeable?
74
+
75
+ puts "- Transforming member #{member_file}"
76
+ File.open(member_file, 'w') { |f| f.write upgrader.upgrade }
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+
@@ -7,11 +7,11 @@ module GraphQL
7
7
  # - `Sequel::Dataset`
8
8
  class RelationConnection < BaseConnection
9
9
  def cursor_from_node(item)
10
- item_index = paged_nodes_array.index(item)
10
+ item_index = paged_nodes.index(item)
11
11
  if item_index.nil?
12
12
  raise("Can't generate cursor, item not found in connection: #{item}")
13
13
  else
14
- offset = item_index + 1 + ((relation_offset(paged_nodes) || 0) - (relation_offset(sliced_nodes) || 0))
14
+ offset = item_index + 1 + ((paged_nodes_offset || 0) - (relation_offset(sliced_nodes) || 0))
15
15
 
16
16
  if after
17
17
  offset += offset_from_cursor(after)
@@ -25,7 +25,7 @@ module GraphQL
25
25
 
26
26
  def has_next_page
27
27
  if first
28
- paged_nodes_length >= first && sliced_nodes_count > first
28
+ paged_nodes.length >= first && sliced_nodes_count > first
29
29
  elsif GraphQL::Relay::ConnectionType.bidirectional_pagination && last
30
30
  sliced_nodes_count > last
31
31
  else
@@ -35,7 +35,7 @@ module GraphQL
35
35
 
36
36
  def has_previous_page
37
37
  if last
38
- paged_nodes_length >= last && sliced_nodes_count > last
38
+ paged_nodes.length >= last && sliced_nodes_count > last
39
39
  elsif GraphQL::Relay::ConnectionType.bidirectional_pagination && after
40
40
  # We've already paginated through the collection a bit,
41
41
  # there are nodes behind us
@@ -64,6 +64,7 @@ module GraphQL
64
64
  private
65
65
 
66
66
  # apply first / last limit results
67
+ # @return [Array]
67
68
  def paged_nodes
68
69
  return @paged_nodes if defined? @paged_nodes
69
70
 
@@ -94,7 +95,14 @@ module GraphQL
94
95
  end
95
96
  end
96
97
 
97
- @paged_nodes = items
98
+ # Store this here so we can convert the relation to an Array
99
+ # (this avoids an extra DB call on Sequel)
100
+ @paged_nodes_offset = relation_offset(items)
101
+ @paged_nodes = items.to_a
102
+ end
103
+
104
+ def paged_nodes_offset
105
+ paged_nodes && @paged_nodes_offset
98
106
  end
99
107
 
100
108
  def relation_offset(relation)
@@ -166,19 +174,6 @@ module GraphQL
166
174
  def offset_from_cursor(cursor)
167
175
  decode(cursor).to_i
168
176
  end
169
-
170
- def paged_nodes_array
171
- return @paged_nodes_array if defined?(@paged_nodes_array)
172
- @paged_nodes_array = paged_nodes.to_a
173
- end
174
-
175
- def paged_nodes_length
176
- if paged_nodes.respond_to?(:length)
177
- paged_nodes.length
178
- else
179
- paged_nodes_array.length
180
- end
181
- end
182
177
  end
183
178
 
184
179
  if defined?(ActiveRecord::Relation)
@@ -570,6 +570,12 @@ module GraphQL
570
570
  GraphQL::Schema::Printer.print_schema(self, only: only, except: except, context: context)
571
571
  end
572
572
 
573
+ # Return the GraphQL::Language::Document IDL AST for the schema
574
+ # @return [GraphQL::Language::Document]
575
+ def to_document
576
+ GraphQL::Language::DocumentFromSchemaDefinition.new(self).document
577
+ end
578
+
573
579
  # Return the Hash response of {Introspection::INTROSPECTION_QUERY}.
574
580
  # @param context [Hash]
575
581
  # @param only [<#call(member, ctx)>]
@@ -18,7 +18,7 @@ module GraphQL
18
18
 
19
19
  def to_graphql
20
20
  argument = GraphQL::Argument.new
21
- argument.name = @name
21
+ argument.name = Member::BuildType.camelize(@name)
22
22
  argument.type = -> {
23
23
  Member::BuildType.parse_type(@type_expr, null: @null)
24
24
  }
@@ -191,6 +191,8 @@ module GraphQL
191
191
  default_value.name
192
192
  when GraphQL::Language::Nodes::NullValue
193
193
  nil
194
+ when GraphQL::Language::Nodes::InputObject
195
+ default_value.to_h
194
196
  else
195
197
  default_value
196
198
  end
@@ -16,7 +16,7 @@ module GraphQL
16
16
  def initialize(name, return_type_expr = nil, desc = nil, null: nil, field: nil, function: nil, deprecation_reason: nil, method: nil, connection: nil, max_page_size: nil, resolve: nil, &args_block)
17
17
  if !(field || function)
18
18
  if return_type_expr.nil?
19
- raise ArgumentError "missing possitional argument `type`"
19
+ raise ArgumentError, "missing positional argument `type`"
20
20
  end
21
21
  if null.nil?
22
22
  raise ArgumentError, "missing keyword argument null:"
@@ -47,7 +47,8 @@ module GraphQL
47
47
  else
48
48
  GraphQL::Field.new
49
49
  end
50
- field_defn.name = @name
50
+
51
+ field_defn.name = Member::BuildType.camelize(name)
51
52
 
52
53
  if @return_type_expr
53
54
  return_type_name = Member::BuildType.to_type_name(@return_type_expr)
@@ -96,6 +97,8 @@ module GraphQL
96
97
  field_defn
97
98
  end
98
99
 
100
+ private
101
+
99
102
  class << self
100
103
  def argument_class(new_arg_class = nil)
101
104
  if new_arg_class
@@ -20,7 +20,7 @@ module GraphQL
20
20
  def argument(*args)
21
21
  argument = GraphQL::Schema::Argument.new(*args)
22
22
  own_arguments << argument
23
- arg_name = argument.name
23
+ arg_name = argument.graphql_definition.name
24
24
  # Add a method access
25
25
  define_method(Member::BuildType.underscore(arg_name)) do
26
26
  @arguments.public_send(arg_name)
@@ -47,7 +47,7 @@ module GraphQL
47
47
  type_defn.name = graphql_name
48
48
  type_defn.description = description
49
49
  arguments.each do |arg|
50
- type_defn.arguments[arg.name] = arg.graphql_definition
50
+ type_defn.arguments[arg.graphql_definition.name] = arg.graphql_definition
51
51
  end
52
52
  # Make a reference to a classic-style Arguments class
53
53
  self.arguments_class = GraphQL::Query::Arguments.construct_arguments_class(type_defn)
@@ -62,6 +62,16 @@ module GraphQL
62
62
  end
63
63
  end
64
64
 
65
+ # Just a convenience method to point out that people should use graphql_name instead
66
+ def name(new_name = nil)
67
+ return super() if new_name.nil?
68
+
69
+ fail(
70
+ "The new name override method is `graphql_name`, not `name`. Usage: "\
71
+ "graphql_name \"#{new_name}\""
72
+ )
73
+ end
74
+
65
75
  # Call this method to provide a new description; OR
66
76
  # call it without an argument to get the description
67
77
  # @param new_description [String]
@@ -94,6 +94,14 @@ module GraphQL
94
94
  end
95
95
  end
96
96
 
97
+ def camelize(string)
98
+ return string unless string.include?('_')
99
+
100
+ string.split('_').map(&:capitalize).join.tap do |camelized|
101
+ camelized[0] = camelized[0].downcase
102
+ end
103
+ end
104
+
97
105
  def underscore(string)
98
106
  string
99
107
  .gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2') # URLDecoder -> URL_Decoder
@@ -85,10 +85,10 @@ module GraphQL
85
85
  private
86
86
 
87
87
  def proxy_to_depth(obj, depth, type, ctx)
88
- if depth > 0
89
- obj.map { |inner_obj| proxy_to_depth(inner_obj, depth - 1, type, ctx) }
90
- elsif obj.nil?
88
+ if obj.nil?
91
89
  obj
90
+ elsif depth > 0
91
+ obj.map { |inner_obj| proxy_to_depth(inner_obj, depth - 1, type, ctx) }
92
92
  else
93
93
  concrete_type = case type
94
94
  when GraphQL::UnionType, GraphQL::InterfaceType
@@ -62,19 +62,21 @@ module GraphQL
62
62
  class ActionCableSubscriptions < GraphQL::Subscriptions
63
63
  SUBSCRIPTION_PREFIX = "graphql-subscription:"
64
64
  EVENT_PREFIX = "graphql-event:"
65
- def initialize(**rest)
65
+
66
+ # @param serializer [<#dump(obj), #load(string)] Used for serializing messages before handing them to `.broadcast(msg)`
67
+ def initialize(serializer: Serialize, **rest)
66
68
  # A per-process map of subscriptions to deliver.
67
69
  # This is provided by Rails, so let's use it
68
70
  @subscriptions = Concurrent::Map.new
71
+ @serializer = serializer
69
72
  super
70
73
  end
71
74
 
72
75
  # An event was triggered; Push the data over ActionCable.
73
76
  # Subscribers will re-evaluate locally.
74
- # TODO: this method name is a smell
75
77
  def execute_all(event, object)
76
78
  stream = EVENT_PREFIX + event.topic
77
- message = Serialize.dump(object)
79
+ message = @serializer.dump(object)
78
80
  ActionCable.server.broadcast(stream, message)
79
81
  end
80
82
 
@@ -97,7 +99,7 @@ module GraphQL
97
99
  @subscriptions[subscription_id] = query
98
100
  events.each do |event|
99
101
  channel.stream_from(EVENT_PREFIX + event.topic, coder: ActiveSupport::JSON) do |message|
100
- execute(subscription_id, event, Serialize.load(message))
102
+ execute(subscription_id, event, @serializer.load(message))
101
103
  nil
102
104
  end
103
105
  end
@@ -2,6 +2,7 @@
2
2
  require "graphql/tracing/active_support_notifications_tracing"
3
3
  require "graphql/tracing/platform_tracing"
4
4
  require "graphql/tracing/appsignal_tracing"
5
+ require "graphql/tracing/data_dog_tracing"
5
6
  require "graphql/tracing/new_relic_tracing"
6
7
  require "graphql/tracing/scout_tracing"
7
8
  require "graphql/tracing/skylight_tracing"
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Tracing
5
+ class DataDogTracing < PlatformTracing
6
+ self.platform_keys = {
7
+ 'lex' => 'lex.graphql',
8
+ 'parse' => 'parse.graphql',
9
+ 'validate' => 'validate.graphql',
10
+ 'analyze_query' => 'analyze.graphql',
11
+ 'analyze_multiplex' => 'analyze.graphql',
12
+ 'execute_multiplex' => 'execute.graphql',
13
+ 'execute_query' => 'execute.graphql',
14
+ 'execute_query_lazy' => 'execute.graphql',
15
+ }
16
+
17
+ def platform_trace(platform_key, key, data)
18
+ service = options.fetch(:service, 'ruby-graphql')
19
+
20
+ pin = Datadog::Pin.get_from(self)
21
+ unless pin
22
+ pin = Datadog::Pin.new(service)
23
+ pin.onto(self)
24
+ end
25
+
26
+ pin.tracer.trace(platform_key, service: pin.service) do |span|
27
+ if key == 'execute_multiplex'
28
+ span.resource = data[:multiplex].queries.map(&:selected_operation_name).join(', ')
29
+ end
30
+
31
+ if key == 'execute_query'
32
+ span.set_tag(:selected_operation_name, data[:query].selected_operation_name)
33
+ span.set_tag(:selected_operation_type, data[:query].selected_operation.operation_type)
34
+ span.set_tag(:query_string, data[:query].query_string)
35
+ end
36
+ yield
37
+ end
38
+ end
39
+
40
+ def platform_field_key(type, field)
41
+ "#{type.name}.#{field.name}"
42
+ end
43
+ end
44
+ end
45
+ end