graphlyte 0.3.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphlyte/data.rb +68 -0
  3. data/lib/graphlyte/document.rb +131 -0
  4. data/lib/graphlyte/dsl.rb +86 -0
  5. data/lib/graphlyte/editor.rb +288 -0
  6. data/lib/graphlyte/editors/annotate_types.rb +75 -0
  7. data/lib/graphlyte/editors/canonicalize.rb +26 -0
  8. data/lib/graphlyte/editors/collect_variable_references.rb +36 -0
  9. data/lib/graphlyte/editors/infer_signature.rb +36 -0
  10. data/lib/graphlyte/editors/inline_fragments.rb +37 -0
  11. data/lib/graphlyte/editors/remove_unneeded_spreads.rb +64 -0
  12. data/lib/graphlyte/editors/select_operation.rb +116 -0
  13. data/lib/graphlyte/editors/with_variables.rb +106 -0
  14. data/lib/graphlyte/errors.rb +33 -0
  15. data/lib/graphlyte/lexer.rb +392 -0
  16. data/lib/graphlyte/lexing/location.rb +43 -0
  17. data/lib/graphlyte/lexing/token.rb +31 -0
  18. data/lib/graphlyte/parser.rb +269 -0
  19. data/lib/graphlyte/parsing/backtracking_parser.rb +160 -0
  20. data/lib/graphlyte/refinements/string_refinement.rb +14 -8
  21. data/lib/graphlyte/refinements/syntax_refinements.rb +62 -0
  22. data/lib/graphlyte/schema.rb +165 -0
  23. data/lib/graphlyte/schema_query.rb +82 -65
  24. data/lib/graphlyte/selection_builder.rb +189 -0
  25. data/lib/graphlyte/selector.rb +75 -0
  26. data/lib/graphlyte/serializer.rb +223 -0
  27. data/lib/graphlyte/syntax.rb +369 -0
  28. data/lib/graphlyte.rb +24 -42
  29. metadata +88 -19
  30. data/lib/graphlyte/arguments/set.rb +0 -94
  31. data/lib/graphlyte/arguments/value.rb +0 -42
  32. data/lib/graphlyte/arguments/value_literal.rb +0 -17
  33. data/lib/graphlyte/builder.rb +0 -59
  34. data/lib/graphlyte/directive.rb +0 -25
  35. data/lib/graphlyte/field.rb +0 -65
  36. data/lib/graphlyte/fieldset.rb +0 -36
  37. data/lib/graphlyte/fragment.rb +0 -17
  38. data/lib/graphlyte/inline_fragment.rb +0 -29
  39. data/lib/graphlyte/query.rb +0 -150
  40. data/lib/graphlyte/schema/parser.rb +0 -687
  41. data/lib/graphlyte/schema/types/base.rb +0 -54
  42. data/lib/graphlyte/types.rb +0 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 93f97fef094a43b1c151dee58329d7b198210246b1fec046e18263d65437d3f4
4
- data.tar.gz: 3d057d189d9a93e601c7b91ce5ad1feb1fd00b0ab17e1c772ebdbff58cbe8349
3
+ metadata.gz: 50a30d874f185f08a1d8e9d7911056135927ec25a2f9e272b14dd50124aeea79
4
+ data.tar.gz: 97b938f8391811a7da0a29d855daea3812c7e97e953dd64fc5fa90fd98a10b8d
5
5
  SHA512:
6
- metadata.gz: 8f106a673e0d70b97d0a4d5634c5c5b03c540fa4e4b857465e2da4252053744f2950e1b78021b99fd0ff3a4fb7bdd896aedbe775a9afc5c2e50b9017692dc626
7
- data.tar.gz: 5ce5c740e6b25fca56736261db1596e25471f1e7bf5bd91785f81da8d2d8d6641b75487b325063ff115a2a3594c6f3fb891e47613c93e2ea5d53529240251b06
6
+ metadata.gz: ebe479ecb5e89d21e0e193207cd72fa6891df00621882f0de1d9fa344655d8058ee566857fc12e3d7ea25d34c83f1b6fb6545ea30739e4473695ef875ef6661a
7
+ data.tar.gz: 7d1bc0754b536f724f4e9ef4f91b3d5411335e568227a4e77e171a45c5a5d719b4edf3c4f289504321f5662d8d25571d7041970fc0bf873e54a489f9698e1d0e
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module Graphlyte
6
+ # Very simplistic data-class. Inheritance is not modelled.
7
+ class Data
8
+ def self.attr_accessor(*names)
9
+ super
10
+ attributes.merge(names)
11
+ end
12
+
13
+ def self.attr_reader(*names)
14
+ super
15
+ attributes.merge(names)
16
+ end
17
+
18
+ def self.attributes
19
+ @attributes ||= [].to_set
20
+ end
21
+
22
+ # Permissive constructor: ignores unknown attributes
23
+ def initialize(**kwargs)
24
+ self.class.attributes.each do |arg|
25
+ send(:"#{arg}=", kwargs[arg]) if kwargs.key?(arg)
26
+ end
27
+ end
28
+
29
+ def eql?(other)
30
+ other.is_a?(self.class) && state == other.send(:state)
31
+ end
32
+
33
+ def ==(other)
34
+ eql?(other)
35
+ end
36
+
37
+ def hash
38
+ state.hash
39
+ end
40
+
41
+ def dup
42
+ self.class.new(**self.class.attributes.to_h { [_1, dup_attribute(_1)] })
43
+ end
44
+
45
+ def inspect
46
+ "#<#{self.class} #{self.class.attributes.map { "@#{_1}=#{send(_1).inspect}" }.join(' ')}>"
47
+ end
48
+
49
+ private
50
+
51
+ def dup_attribute(attr)
52
+ value = send(attr)
53
+
54
+ case value
55
+ when Array
56
+ value.map(&:dup)
57
+ when Hash
58
+ value.transform_values(&:dup)
59
+ else
60
+ value.dup
61
+ end
62
+ end
63
+
64
+ def state
65
+ self.class.attributes.map { send _1 }
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ require_relative './syntax'
6
+ require_relative './data'
7
+ require_relative './serializer'
8
+ require_relative './refinements/string_refinement'
9
+ require_relative './editors/with_variables'
10
+
11
+ module Graphlyte
12
+ # The representation of a GraphQL document.
13
+ #
14
+ # Documents can have multiple definitions, which can
15
+ # be queries, mutations, subscriptions (operations) or fragments.
16
+ #
17
+ # During execution, only one operation can be executed.
18
+ class Document < Graphlyte::Data
19
+ using Graphlyte::Refinements::StringRefinement
20
+ extend Forwardable
21
+
22
+ attr_accessor :definitions, :variables, :schema
23
+
24
+ def_delegators :@definitions, :length, :empty?
25
+
26
+ def initialize(**kwargs)
27
+ super
28
+ @definitions ||= []
29
+ @variables ||= {}
30
+ @var_name_counter = @variables.size + 1
31
+ end
32
+
33
+ def +(other)
34
+ return dup unless other
35
+
36
+ other = other.dup
37
+ doc = dup
38
+
39
+ defs = doc.definitions + other.definitions
40
+ vars = doc.variables.merge(other.variables) # TODO: detect conflicts?
41
+
42
+ self.class.new(definitions: defs, vars: vars)
43
+ end
44
+
45
+ def eql?(other)
46
+ other.is_a?(self.class) && other.fragments == fragments && other.operations == operations
47
+ end
48
+
49
+ alias == eql?
50
+
51
+ def define(dfn)
52
+ @definitions << dfn
53
+ end
54
+
55
+ def add_fragments(frags)
56
+ current = fragments
57
+
58
+ frags.each do |frag|
59
+ @definitions << frag unless current[frag.name]
60
+ end
61
+ end
62
+
63
+ def declare(var)
64
+ if var.name.nil?
65
+ var.name = "var#{@var_name_counter}"
66
+ @var_name_counter += 1
67
+ end
68
+
69
+ parser = Graphlyte::Parser.new(tokens: Graphlyte::Lexer.lex(var.type))
70
+ parsed_type = parser.type_name! if var.type
71
+ current_def = @variables[var.name]
72
+
73
+ if current_def && current_def.type != parsed_type
74
+ msg = "Cannot re-declare #{var.name} at different types. #{current_def.type} != #{var.type}"
75
+ raise ArgumentError, msg
76
+ end
77
+
78
+ @variables[var.name] ||= Graphlyte::Syntax::VariableDefinition.new(
79
+ variable: var.name,
80
+ type: parsed_type
81
+ )
82
+
83
+ Syntax::VariableReference.new(var.name, parsed_type)
84
+ end
85
+
86
+ def fragments
87
+ definitions.select { _1.is_a?(Graphlyte::Syntax::Fragment) }.to_h { [_1.name, _1] }
88
+ end
89
+
90
+ def operations
91
+ @definitions.select { _1.is_a?(Graphlyte::Syntax::Operation) }.to_h { [_1.name, _1] }
92
+ end
93
+
94
+ def executable?
95
+ @definitions.all?(&:executable?)
96
+ end
97
+
98
+ def to_s
99
+ buff = []
100
+ write(buff)
101
+
102
+ buff.join
103
+ end
104
+
105
+ # More efficient for writing to files or streams - avoids building up the full string.
106
+ def write(io)
107
+ Graphlyte::Serializer.new(io).dump_definitions(definitions)
108
+ end
109
+
110
+ # Return this document as a JSON request body, suitable for posting to a server.
111
+ def request_body(operation = nil, **variables)
112
+ if operation.nil? && operations.size != 1
113
+ raise ArgumentError, 'Operation name is required when the document contains multiple operations'
114
+ end
115
+
116
+ variables.transform_keys! { _1.to_s.camelize }
117
+
118
+ doc = Editors::WithVariables.new(schema, operation, variables).edit(dup)
119
+
120
+ {
121
+ query: doc.to_s,
122
+ variables: variables,
123
+ operation: operation
124
+ }.compact.to_json
125
+ end
126
+
127
+ def variable_references
128
+ Editors::CollectVariableReferences.new.edit(self)[Syntax::Operation]
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './syntax'
4
+ require_relative './selection_builder'
5
+ require_relative './refinements/string_refinement'
6
+ require_relative './editors/infer_signature'
7
+
8
+ module Graphlyte
9
+ # The DSL methods for query construction are defined here.
10
+ #
11
+ # The main methods are:
12
+ #
13
+ # - `var`: creates a fresh unique variable
14
+ # - `enum`: allows referring to enum values
15
+ # - `fragment`: creates a fragment that can be re-used in operations
16
+ # - `query`: creates a `Query` operation
17
+ # - `mutation`: creates a `Mutation` operation
18
+ class DSL
19
+ using Graphlyte::Refinements::StringRefinement
20
+
21
+ attr_reader :schema
22
+
23
+ def initialize(schema = nil)
24
+ @schema = schema
25
+ end
26
+
27
+ def var(type = nil, name = nil)
28
+ SelectionBuilder::Variable.new(type: type, name: name&.to_s&.camelize)
29
+ end
30
+
31
+ def enum(value)
32
+ Syntax::Value.new(value.to_sym, :ENUM)
33
+ end
34
+
35
+ def query(name = nil, doc = Document.new, &block)
36
+ op = Syntax::Operation.new(type: :query)
37
+ doc.define(op)
38
+
39
+ op.name = name
40
+ op.selection = SelectionBuilder.build(doc, &block)
41
+
42
+ Editors::InferSignature.new(@schema).edit(doc)
43
+
44
+ doc
45
+ end
46
+
47
+ def mutation(name = nil, doc = Document.new, &block)
48
+ op = Syntax::Operation.new(type: :mutation)
49
+ doc.define(op)
50
+
51
+ op.name = name
52
+ op.selection = SelectionBuilder.build(doc, &block)
53
+
54
+ # TODO: infer operation signatures (requires schema!)
55
+ doc
56
+ end
57
+
58
+ def fragment(fragment_name = nil, on:, doc: Document.new, &block)
59
+ frag = Graphlyte::Syntax::Fragment.new
60
+
61
+ frag.type_name = on
62
+ frag.selection = SelectionBuilder.build(doc, &block)
63
+
64
+ if fragment_name
65
+ frag.name = fragment_name
66
+ else
67
+ base = "#{on}Fields"
68
+ n = 1
69
+ frag.name = base
70
+
71
+ while doc.fragments[frag.name]
72
+ frag.name = "#{base}_#{n}"
73
+ n += 1
74
+ end
75
+ end
76
+
77
+ doc.fragments.each_value do |required|
78
+ frag.refers_to required
79
+ end
80
+
81
+ doc.define(frag)
82
+
83
+ frag
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,288 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './syntax'
4
+
5
+ module Graphlyte
6
+ # Walk the document tree and edit or collect data.
7
+ #
8
+ # This is the general purpose recursive transformer for
9
+ # syntax trees, used to write various validation and
10
+ # transformation passes. See `lib/graphlyte/editors`
11
+ #
12
+ # Usage
13
+ #
14
+ # A fragment inliner:
15
+ #
16
+ # inliner = Editor.new.on_fragment_spread do |spread, action|
17
+ # action.replace action.document.fragments[spread.name].inline
18
+ # end
19
+ #
20
+ # inliner.edit(document)
21
+ #
22
+ # A variable renamer:
23
+ #
24
+ # renamer = Editor.new.on_variable do |var|
25
+ # var.variable = 'x' if var.variable == 'y'
26
+ # end
27
+ #
28
+ # renamer.edit(document)
29
+ #
30
+ # A string collector:
31
+ #
32
+ # strings = []
33
+ # collector = Editor.new.on_value do |value|
34
+ # strings << value.value if value.type == :STRING
35
+ # end
36
+ #
37
+ # collector.edit(document)
38
+ #
39
+ class Editor
40
+ Deleted = Class.new(StandardError)
41
+
42
+ attr_accessor :direction
43
+
44
+ def initialize
45
+ @hooks = {}
46
+ @direction = :bottom_up
47
+ end
48
+
49
+ # The value passed to the handler blocks, in addition to the syntax node.
50
+ # Users can call methods on this object to edit the document in-place, as
51
+ # well as read information about the context of this node.
52
+ class Action
53
+ attr_reader :new_nodes, :path, :definition, :parent, :document
54
+
55
+ def initialize(old_node, path, parent, document)
56
+ @new_nodes = [old_node]
57
+ @definition = path.first
58
+ @path = path.dup.freeze
59
+ @parent = parent
60
+ @document = document
61
+ end
62
+
63
+ def replace(replacement)
64
+ @new_nodes = [replacement]
65
+ end
66
+
67
+ def insert_before(node)
68
+ @new_nodes = [node] + @new_nodes
69
+ end
70
+
71
+ def insert_after(node)
72
+ @new_nodes.push(node)
73
+ end
74
+
75
+ def delete
76
+ @new_nodes = []
77
+ end
78
+
79
+ def expand(new_nodes)
80
+ @new_nodes = new_nodes
81
+ end
82
+
83
+ def closest(node_type)
84
+ @path.reverse.find { _1.is_a?(node_type) }
85
+ end
86
+ end
87
+
88
+ # The class responsible for orchestration of the hooks. This class
89
+ # defines the recursion through the document.
90
+ Context = Struct.new(:document, :direction, :hooks, :path) do
91
+ def edit(object, &block)
92
+ parent = path.last
93
+ path.push(object)
94
+
95
+ processor = hooks[object.class]
96
+ action = Action.new(object, path, parent, document)
97
+
98
+ case direction
99
+ when :bottom_up
100
+ edit_bottom_up(object, processor, action, &block)
101
+ when :top_down
102
+ edit_top_down(object, processor, action, &block)
103
+ else
104
+ raise ArgumentError, "Unknown direction: #{direction}"
105
+ end
106
+
107
+ action.new_nodes
108
+ ensure
109
+ path.pop
110
+ end
111
+
112
+ def edit_top_down(object, processor, action)
113
+ processor&.call(object, action)
114
+ action.new_nodes = action.new_nodes.filter_map do |node|
115
+ yield node if block_given?
116
+ node
117
+ rescue Deleted
118
+ nil
119
+ end
120
+ end
121
+
122
+ def edit_bottom_up(object, processor, action)
123
+ yield object if block_given?
124
+ processor&.call(object, action)
125
+ rescue Deleted
126
+ action.new_nodes = []
127
+ end
128
+
129
+ def edit_variables(object)
130
+ return unless object.respond_to?(:variables)
131
+
132
+ object.variables = object.variables&.flat_map do |var|
133
+ edit(var) do |v|
134
+ edit_directives(v)
135
+ v.default_value = edit_value(v.default_value).first
136
+ end
137
+ end
138
+ end
139
+
140
+ def edit_directives(object)
141
+ return unless object.respond_to?(:directives)
142
+
143
+ object.directives = object.directives&.flat_map do |dir|
144
+ edit(dir) { |d| edit_arguments(d) }
145
+ end
146
+ end
147
+
148
+ def edit_arguments(object)
149
+ return unless object.respond_to?(:arguments)
150
+
151
+ object.arguments = object.arguments&.flat_map do |arg|
152
+ edit(arg) do |_a|
153
+ arg.value = edit_value(arg.value).first
154
+ raise Deleted if arg.value.nil?
155
+ end
156
+ end
157
+ end
158
+
159
+ def edit_value(object)
160
+ case object
161
+ when Array
162
+ [object.flat_map { edit_value(_1) }]
163
+ when Hash
164
+ [
165
+ object.to_a.flat_map do |(k, old_value)|
166
+ edit_value(old_value).take(1).map do |new_value|
167
+ [k, new_value]
168
+ end
169
+ end.to_h
170
+ ]
171
+ else
172
+ edit(object)
173
+ end
174
+ end
175
+
176
+ def edit_selection(object)
177
+ return unless object.respond_to?(:selection)
178
+
179
+ object.selection = object.selection&.flat_map do |selected|
180
+ edit(selected) do |s|
181
+ edit_arguments(s)
182
+ edit_directives(s)
183
+ edit_selection(s)
184
+ end
185
+ end
186
+ end
187
+
188
+ def edit_definition(object)
189
+ edit(object) do |o|
190
+ edit_variables(o)
191
+ edit_directives(o)
192
+ edit_selection(o)
193
+ end
194
+ end
195
+ end
196
+
197
+ def self.top_down
198
+ e = new
199
+ e.direction = :top_down
200
+
201
+ e
202
+ end
203
+
204
+ def self.bottom_up
205
+ new
206
+ end
207
+
208
+ def on_value(&block)
209
+ @hooks[Syntax::Value] = block
210
+ self
211
+ end
212
+
213
+ def on_argument(&block)
214
+ @hooks[Syntax::Argument] = block
215
+ self
216
+ end
217
+
218
+ def on_directive(&block)
219
+ @hooks[Syntax::Directive] = block
220
+ self
221
+ end
222
+
223
+ def on_operation(&block)
224
+ @hooks[Syntax::Operation] = block
225
+ self
226
+ end
227
+
228
+ def on_variable(&block)
229
+ on_variable_definition(&block)
230
+ on_variable_reference(&block)
231
+ self
232
+ end
233
+
234
+ def on_variable_definition(&block)
235
+ @hooks[Syntax::VariableDefinition] = block
236
+ self
237
+ end
238
+
239
+ def on_variable_reference(&block)
240
+ @hooks[Syntax::VariableReference] = block
241
+ self
242
+ end
243
+
244
+ # Selected nodes:
245
+
246
+ def on_field(&block)
247
+ @hooks[Syntax::Field] = block
248
+ self
249
+ end
250
+
251
+ def on_fragment(&block)
252
+ on_inline_fragment(&block)
253
+ on_fragment_definition(&block)
254
+ self
255
+ end
256
+
257
+ def on_fragment_spread(&block)
258
+ @hooks[Syntax::FragmentSpread] = block
259
+ self
260
+ end
261
+
262
+ def on_inline_fragment(&block)
263
+ @hooks[Syntax::InlineFragment] = block
264
+ self
265
+ end
266
+
267
+ def on_fragment_definition(&block)
268
+ @hooks[Syntax::Fragment] = block
269
+ self
270
+ end
271
+
272
+ # To edit specific nodes in a document (or isolated from a document)
273
+ # you will need a Context.
274
+ def context(document = nil)
275
+ Context.new(document, direction, @hooks.dup.freeze, [])
276
+ end
277
+
278
+ def edit(document)
279
+ c = context(document)
280
+
281
+ document.definitions = document.definitions.flat_map do |object|
282
+ c.edit_definition(object)
283
+ end
284
+
285
+ document
286
+ end
287
+ end
288
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Graphlyte
4
+ module Editors
5
+ # Use a schema definition to annotate the type of each field and variable reference.
6
+ class AnnotateTypes
7
+ TypeCheckError = Class.new(StandardError)
8
+ TypeNotFound = Class.new(TypeCheckError)
9
+ FieldNotFound = Class.new(TypeCheckError)
10
+ CannotDetermineTypeName = Class.new(TypeCheckError)
11
+
12
+ def initialize(schema, recheck: false)
13
+ @schema = schema
14
+ @recheck = recheck
15
+ end
16
+
17
+ def edit(doc)
18
+ return if !recheck && doc.schema # Previously annotated
19
+
20
+ doc.schema = @schema
21
+ editor.edit(doc)
22
+ end
23
+
24
+ def editor
25
+ @editor ||=
26
+ Editor
27
+ .top_down
28
+ .on_field { |field, action| infer_field(field, action.parent, action.document) }
29
+ .on_variable_reference do |ref, action|
30
+ infer_ref(ref, action.closest(Syntax::Argument), action.closest(Syntax::Field))
31
+ end
32
+ end
33
+
34
+ # For now we are ignoring variables nested in input objects.
35
+ # TODO: encode input objects differently?
36
+ def infer_ref(ref, argument, field)
37
+ type = @schema.types[field.type.unpack]
38
+ arg = type.arguments[argument.name]
39
+ raise ArgumentNotFound, "#{type.name}.#{field.name}(#{argument.name})" unless arg
40
+
41
+ ref.inferred_type = Syntax::Type.from_type_ref(arg.type)
42
+ end
43
+
44
+ def infer_field(field, parent, document)
45
+ name = object_name(parent, document)
46
+ object_type = type(name)
47
+
48
+ raise FieldNotFound, "#{object_name}.#{field.name}" unless object_type.fields.key?(field.name)
49
+
50
+ field.type = Syntax::Type.from_type_ref(object_type.fields[field.name].type)
51
+ end
52
+
53
+ def type(name)
54
+ object_type = @schema.types[name]
55
+ raise TypeNotFound, object_name unless object_type
56
+
57
+ object_type
58
+ end
59
+
60
+ def object_name(parent, document)
61
+ case parent
62
+ when Syntax::FragmentSpread
63
+ fragment = document.fragments[parent.name]
64
+ raise CannotDetermineTypeName, parent unless fragment
65
+
66
+ fragment.type_name
67
+ when Syntax::InlineFragment, Syntax::Fragment
68
+ parent.type_name
69
+ when Syntax::Field
70
+ parent.type.unpack
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../editor'
4
+ require_relative '../syntax'
5
+ require_relative './inline_fragments'
6
+
7
+ module Graphlyte
8
+ module Editors
9
+ # Reduce the query to a canonical form.
10
+ class Canonicalize
11
+ def edit(doc)
12
+ doc = doc.dup
13
+ InlineFragments.new.edit(doc)
14
+ doc.definitions = doc.definitions.sort_by(&:name)
15
+ # TODO: we should also perform the selection Merge operation here.
16
+ order_argments.edit(doc)
17
+ end
18
+
19
+ def order_argments
20
+ Editor.new
21
+ .on_field { |field| field.arguments = field.arguments&.sort_by(&:name) }
22
+ .on_directive { |dir| dir.arguments = dir.arguments&.sort_by(&:name) }
23
+ end
24
+ end
25
+ end
26
+ end