yogurt 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.DS_Store +0 -0
- data/.github/workflows/tests.yml +117 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.rubocop.yml +113 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +0 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +82 -0
- data/LICENSE +201 -0
- data/README.md +38 -0
- data/Rakefile +9 -0
- data/bin/bundle +105 -0
- data/bin/byebug +29 -0
- data/bin/coderay +29 -0
- data/bin/console +16 -0
- data/bin/htmldiff +29 -0
- data/bin/ldiff +29 -0
- data/bin/pry +29 -0
- data/bin/rake +29 -0
- data/bin/rspec +29 -0
- data/bin/rubocop +29 -0
- data/bin/ruby-parse +29 -0
- data/bin/ruby-rewrite +29 -0
- data/bin/setup +8 -0
- data/bin/srb +29 -0
- data/bin/srb-rbi +29 -0
- data/lib/yogurt.rb +95 -0
- data/lib/yogurt/code_generator.rb +706 -0
- data/lib/yogurt/code_generator/defined_class.rb +22 -0
- data/lib/yogurt/code_generator/defined_class_sorter.rb +44 -0
- data/lib/yogurt/code_generator/defined_method.rb +24 -0
- data/lib/yogurt/code_generator/enum_class.rb +56 -0
- data/lib/yogurt/code_generator/field_access_method.rb +273 -0
- data/lib/yogurt/code_generator/field_access_path.rb +56 -0
- data/lib/yogurt/code_generator/generated_file.rb +53 -0
- data/lib/yogurt/code_generator/input_class.rb +52 -0
- data/lib/yogurt/code_generator/leaf_class.rb +68 -0
- data/lib/yogurt/code_generator/operation_declaration.rb +12 -0
- data/lib/yogurt/code_generator/root_class.rb +130 -0
- data/lib/yogurt/code_generator/type_wrapper.rb +13 -0
- data/lib/yogurt/code_generator/typed_input.rb +12 -0
- data/lib/yogurt/code_generator/typed_output.rb +16 -0
- data/lib/yogurt/code_generator/utils.rb +140 -0
- data/lib/yogurt/code_generator/variable_definition.rb +33 -0
- data/lib/yogurt/converters.rb +50 -0
- data/lib/yogurt/error_result.rb +30 -0
- data/lib/yogurt/http.rb +80 -0
- data/lib/yogurt/inspectable.rb +22 -0
- data/lib/yogurt/memoize.rb +33 -0
- data/lib/yogurt/query.rb +23 -0
- data/lib/yogurt/query_container.rb +89 -0
- data/lib/yogurt/query_container/interfaces_and_unions_have_typename.rb +107 -0
- data/lib/yogurt/query_declaration.rb +11 -0
- data/lib/yogurt/query_executor.rb +23 -0
- data/lib/yogurt/query_result.rb +25 -0
- data/lib/yogurt/scalar_converter.rb +19 -0
- data/lib/yogurt/unexpected_object_type.rb +30 -0
- data/lib/yogurt/validation_error.rb +6 -0
- data/lib/yogurt/version.rb +6 -0
- data/sorbet/config +10 -0
- data/sorbet/rbi/fake_schema.rbi +14 -0
- data/sorbet/rbi/graphql.rbi +11 -0
- data/sorbet/rbi/hidden-definitions/errors.txt +20513 -0
- data/sorbet/rbi/hidden-definitions/hidden.rbi +42882 -0
- data/sorbet/rbi/sorbet-typed/lib/graphql/all/graphql.rbi +48 -0
- data/sorbet/rbi/sorbet-typed/lib/rainbow/all/rainbow.rbi +276 -0
- data/sorbet/rbi/sorbet-typed/lib/rubocop/~>0.85/rubocop.rbi +2072 -0
- data/sorbet/rbi/todo.rbi +6 -0
- data/yogurt.gemspec +54 -0
- metadata +286 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Yogurt
|
5
|
+
class CodeGenerator
|
6
|
+
module DefinedClass
|
7
|
+
include Kernel
|
8
|
+
extend T::Sig
|
9
|
+
extend T::Helpers
|
10
|
+
abstract!
|
11
|
+
|
12
|
+
sig {abstract.returns(String)}
|
13
|
+
def name; end
|
14
|
+
|
15
|
+
sig {abstract.returns(String)}
|
16
|
+
def to_ruby; end
|
17
|
+
|
18
|
+
sig {abstract.returns(T::Array[String])}
|
19
|
+
def dependencies; end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'tsort'
|
5
|
+
module Yogurt
|
6
|
+
class CodeGenerator
|
7
|
+
class DefinedClassSorter
|
8
|
+
extend T::Sig
|
9
|
+
include TSort
|
10
|
+
|
11
|
+
sig {params(classes: T::Array[DefinedClass]).void}
|
12
|
+
def initialize(classes)
|
13
|
+
@classes = T.let(
|
14
|
+
classes.map {|k| [k.name, k]}.to_h,
|
15
|
+
T::Hash[String, DefinedClass],
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
sig {returns(T::Array[DefinedClass])}
|
20
|
+
def sorted_classes
|
21
|
+
tsort
|
22
|
+
end
|
23
|
+
|
24
|
+
sig {params(block: T.proc.params(arg0: DefinedClass).void).void}
|
25
|
+
private def tsort_each_node(&block)
|
26
|
+
@classes.keys.sort.each do |key|
|
27
|
+
yield(@classes.fetch(key))
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
sig do
|
32
|
+
params(
|
33
|
+
input_class: DefinedClass,
|
34
|
+
block: T.proc.params(arg0: DefinedClass).void,
|
35
|
+
).void
|
36
|
+
end
|
37
|
+
private def tsort_each_child(input_class, &block)
|
38
|
+
input_class.dependencies.sort.each do |name|
|
39
|
+
yield(@classes.fetch(name))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Yogurt
|
5
|
+
class CodeGenerator
|
6
|
+
module DefinedMethod
|
7
|
+
extend T::Sig
|
8
|
+
extend T::Helpers
|
9
|
+
include Kernel
|
10
|
+
abstract!
|
11
|
+
|
12
|
+
sig {abstract.returns(Symbol)}
|
13
|
+
def name; end
|
14
|
+
|
15
|
+
# Attempts to merge this method with the other defined method. Returns true if
|
16
|
+
# successful, false if they are incompatible.
|
17
|
+
sig {abstract.params(other: DefinedMethod).returns(T::Boolean)}
|
18
|
+
def merge?(other); end
|
19
|
+
|
20
|
+
sig {abstract.returns(String)}
|
21
|
+
def to_ruby; end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Yogurt
|
5
|
+
class CodeGenerator
|
6
|
+
# For GraphQL enum classes
|
7
|
+
class EnumClass < T::Struct
|
8
|
+
extend T::Sig
|
9
|
+
include Utils
|
10
|
+
include DefinedClass
|
11
|
+
|
12
|
+
const :name, String
|
13
|
+
const :serialized_values, T::Array[String]
|
14
|
+
|
15
|
+
sig {override.returns(T::Array[String])}
|
16
|
+
def dependencies
|
17
|
+
[]
|
18
|
+
end
|
19
|
+
|
20
|
+
sig {override.returns(String)}
|
21
|
+
def to_ruby
|
22
|
+
existing_constants = []
|
23
|
+
|
24
|
+
definitions = serialized_values.sort.map do |name|
|
25
|
+
const_name = safe_constant_name(name, existing_constants)
|
26
|
+
existing_constants << const_name
|
27
|
+
"#{const_name} = new(#{name.inspect})"
|
28
|
+
end
|
29
|
+
|
30
|
+
<<~STRING
|
31
|
+
class #{name} < T::Enum
|
32
|
+
enums do
|
33
|
+
#{indent(definitions.join("\n"), 2).strip}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
STRING
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns a valid Ruby constant name that doesn't conflict with the
|
40
|
+
# existing constants.
|
41
|
+
sig {params(desired_name: String, existing_constants: T::Array[String]).returns(String)}
|
42
|
+
private def safe_constant_name(desired_name, existing_constants)
|
43
|
+
desired_name = underscore(desired_name).upcase
|
44
|
+
base_desired_name = desired_name
|
45
|
+
escaping_level = 1
|
46
|
+
|
47
|
+
while existing_constants.include?(desired_name)
|
48
|
+
escaping_level += 1
|
49
|
+
desired_name = "#{base_desired_name}#{escaping_level}"
|
50
|
+
end
|
51
|
+
|
52
|
+
desired_name
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,273 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Yogurt
|
5
|
+
class CodeGenerator
|
6
|
+
# Method that is used to access a field on an object
|
7
|
+
class FieldAccessMethod < T::Struct
|
8
|
+
extend T::Sig
|
9
|
+
include Memoize
|
10
|
+
include DefinedMethod
|
11
|
+
include Utils
|
12
|
+
|
13
|
+
# Indicates the possible object types that could occur at runtime, and which expressions
|
14
|
+
# should be used if that object type appears.
|
15
|
+
class FragmentBranch < T::Struct
|
16
|
+
extend T::Sig
|
17
|
+
include Comparable
|
18
|
+
include Memoize
|
19
|
+
include Utils
|
20
|
+
|
21
|
+
const :typenames, T::Set[String]
|
22
|
+
const :expression, String
|
23
|
+
|
24
|
+
sig {returns(T::Array[String])}
|
25
|
+
def sorted_typenames
|
26
|
+
memoize_as(:sorted_typenames) {typenames.to_a.sort}
|
27
|
+
end
|
28
|
+
|
29
|
+
sig {returns(String)}
|
30
|
+
def to_ruby
|
31
|
+
<<~STRING.strip
|
32
|
+
when #{sorted_typenames.map(&:inspect).join(', ')}
|
33
|
+
#{indent(expression, 1)}
|
34
|
+
STRING
|
35
|
+
end
|
36
|
+
|
37
|
+
sig {override.params(other: T.untyped).returns(T.nilable(Integer))}
|
38
|
+
def <=>(other)
|
39
|
+
return unless other.is_a?(FragmentBranch)
|
40
|
+
|
41
|
+
comparison = sorted_typenames <=> other.sorted_typenames
|
42
|
+
return comparison if comparison != 0
|
43
|
+
|
44
|
+
expression <=> other.expression
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
IMPOSSIBLE_FIELD_SIGNATURE = T.let("NilClass", String)
|
49
|
+
IMPOSSIBLE_FIELD_BODY = T.let(<<~STRING, String)
|
50
|
+
# The combination of fragments used to retrieve this field make it impossible
|
51
|
+
# for the field to have any value other than `nil`.
|
52
|
+
nil
|
53
|
+
STRING
|
54
|
+
|
55
|
+
# Name of the method
|
56
|
+
const :name, Symbol
|
57
|
+
|
58
|
+
# Paths with the fragments that indicate how this method is accessed
|
59
|
+
const :field_access_paths, T::Array[FieldAccessPath]
|
60
|
+
|
61
|
+
# GraphQL schema for the query that is executing
|
62
|
+
const :schema, GRAPHQL_SCHEMA
|
63
|
+
|
64
|
+
# Attempts to merge this method with the other defined method. Returns true if
|
65
|
+
# successful, false if they are incompatible.
|
66
|
+
sig {override.params(other: DefinedMethod).returns(T::Boolean)}
|
67
|
+
def merge?(other)
|
68
|
+
return false unless other.is_a?(FieldAccessMethod)
|
69
|
+
|
70
|
+
field_access_paths.concat(other.field_access_paths)
|
71
|
+
true
|
72
|
+
end
|
73
|
+
|
74
|
+
sig {override.returns(String)}
|
75
|
+
def to_ruby
|
76
|
+
<<~STRING
|
77
|
+
sig {returns(#{signature})}
|
78
|
+
def #{name}
|
79
|
+
#{indent(body, 1).strip}
|
80
|
+
end
|
81
|
+
STRING
|
82
|
+
end
|
83
|
+
|
84
|
+
# Returns the different branches of execution that could happen based on the actual
|
85
|
+
# object type that is returned by the GraphQL query.
|
86
|
+
sig {returns(T::Array[FragmentBranch])}
|
87
|
+
def branches
|
88
|
+
reduce!
|
89
|
+
memoize_as(:branches) do
|
90
|
+
# Construct the branches for each of the possible fragments. When grouped by
|
91
|
+
# expression, the typenames possible for each branch should be disjoint. If they're
|
92
|
+
# not, the query would have been rejected as invalid by the `FieldsWillMerge`
|
93
|
+
# static validation rule.
|
94
|
+
result = field_access_paths.group_by(&:expression).map do |expression, group|
|
95
|
+
typenames = T.let(Set.new, T::Set[String])
|
96
|
+
group.each do |path|
|
97
|
+
typenames.merge(path.compatible_object_types)
|
98
|
+
end
|
99
|
+
|
100
|
+
FragmentBranch.new(expression: expression, typenames: typenames)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Invariant: Make sure that the behavior of the world matches our expectations.
|
104
|
+
invalid_branches = result.combination(2).select do |b1, b2|
|
105
|
+
next if b1.nil?
|
106
|
+
next if b2.nil?
|
107
|
+
|
108
|
+
b1.typenames.intersect?(b2.typenames)
|
109
|
+
end
|
110
|
+
|
111
|
+
if invalid_branches.any?
|
112
|
+
raise <<~STRING
|
113
|
+
Some field access branches have overlapping types, but different field resolution expressions.
|
114
|
+
#{invalid_branches.map {|b1, b2| { branch1: T.must(b1).serialize, branch2: T.must(b2).serialize }}.inspect}
|
115
|
+
STRING
|
116
|
+
end
|
117
|
+
|
118
|
+
result.sort!
|
119
|
+
result.freeze
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
sig {returns(String)}
|
124
|
+
def body
|
125
|
+
reduce!
|
126
|
+
memoize_as(:body) do
|
127
|
+
break IMPOSSIBLE_FIELD_BODY if field_access_is_impossible?
|
128
|
+
|
129
|
+
if field_access_is_guaranteed? && branches.size == 1
|
130
|
+
branches.fetch(0).expression
|
131
|
+
elsif field_access_is_guaranteed?
|
132
|
+
<<~STRING
|
133
|
+
case (type = __typename)
|
134
|
+
#{branches.map(&:to_ruby).join("\n")}
|
135
|
+
else
|
136
|
+
__unexpected_type(field: #{name.inspect}, observed_type: type, expected_types: POSSIBLE_TYPES)
|
137
|
+
end
|
138
|
+
STRING
|
139
|
+
elsif branches.size == 1
|
140
|
+
branch = branches.fetch(0)
|
141
|
+
condition = branch.sorted_typenames.map {|type| "type == #{type.inspect}"}.join(' || ')
|
142
|
+
<<~STRING
|
143
|
+
type = __typename
|
144
|
+
return unless #{condition}
|
145
|
+
#{branch.expression}
|
146
|
+
STRING
|
147
|
+
else
|
148
|
+
<<~STRING
|
149
|
+
case (type = __typename)
|
150
|
+
#{branches.map(&:to_ruby).join("\n")}
|
151
|
+
end
|
152
|
+
STRING
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
sig {returns(String)}
|
158
|
+
def signature
|
159
|
+
reduce!
|
160
|
+
memoize_as(:signature) do
|
161
|
+
break IMPOSSIBLE_FIELD_SIGNATURE if field_access_is_impossible?
|
162
|
+
|
163
|
+
signatures = field_access_paths.map do |path|
|
164
|
+
signature = path.signature
|
165
|
+
next signature if !signature.start_with?("T.nilable")
|
166
|
+
|
167
|
+
# Preserve the original signature if we're guaranteed to always return this field
|
168
|
+
next signature if field_access_is_guaranteed?
|
169
|
+
|
170
|
+
# If fragments might cause the field to be omitted, strip off the nilability
|
171
|
+
# because we'll wrap the composite signature in a `T.nilable`
|
172
|
+
signature.delete_prefix("T.nilable(").delete_suffix(")")
|
173
|
+
end
|
174
|
+
|
175
|
+
signatures.uniq!
|
176
|
+
signatures.sort!
|
177
|
+
|
178
|
+
composite_signature = if signatures.size == 1
|
179
|
+
signatures.fetch(0)
|
180
|
+
else
|
181
|
+
"T.any(#{signatures.join(', ')})"
|
182
|
+
end
|
183
|
+
|
184
|
+
if field_access_is_guaranteed?
|
185
|
+
composite_signature
|
186
|
+
else
|
187
|
+
"T.nilable(#{composite_signature})"
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
sig {returns(T::Boolean)}
|
193
|
+
def field_access_is_impossible?
|
194
|
+
reduce!
|
195
|
+
field_access_paths.none?
|
196
|
+
end
|
197
|
+
|
198
|
+
# Returns true if this field will always be evaluated when the query is run.
|
199
|
+
# Returns false if it's possible for the field to be excluded from the query because
|
200
|
+
# the types of the fragments might not match the type of the object.
|
201
|
+
sig {returns(T::Boolean)}
|
202
|
+
def field_access_is_guaranteed?
|
203
|
+
reduce!
|
204
|
+
memoize_as(:field_access_is_guaranteed?) do
|
205
|
+
# Field access is not guaranteed if, after eliminating all of the unnecessary field
|
206
|
+
# access paths, there are any that only return a value for a subset of the possible
|
207
|
+
# field types at the root of the fragment.
|
208
|
+
field_access_paths.all? do |path|
|
209
|
+
root_possible_types.subset?(path.compatible_object_types)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
# Returns the types of objects that are possible at the root of all of the
|
215
|
+
# field access paths. This will be the same for all of the field access paths,
|
216
|
+
# since they should all be starting from the same place.
|
217
|
+
sig {returns(T::Set[String])}
|
218
|
+
private def root_possible_types
|
219
|
+
reduce!
|
220
|
+
memoize_as(:root_possible_types) do
|
221
|
+
root_type = field_access_paths[0]&.fragment_types&.fetch(0)
|
222
|
+
break Set.new if root_type.nil?
|
223
|
+
|
224
|
+
raise "Invariant violated: Expected all FieldAccessPath's to have the same root fragment type." if !field_access_paths.all? {|path| path.fragment_types.fetch(0) == root_type}
|
225
|
+
|
226
|
+
schema.possible_types(schema.types[root_type]).map(&:graphql_name).to_set
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
# Eliminates field access paths that are impossible or reduntant
|
231
|
+
sig {void}
|
232
|
+
private def reduce!
|
233
|
+
memoize_as(:reduce!) do
|
234
|
+
# Eliminate paths where there are no objects that could possibly satisfy the fragment conditions
|
235
|
+
#
|
236
|
+
# For example:
|
237
|
+
#
|
238
|
+
# query {
|
239
|
+
# node(id: "foobar") {
|
240
|
+
# ... on Commit {
|
241
|
+
# ... on Node {
|
242
|
+
# ... on User {
|
243
|
+
# # This field can never be accessed, because User's will never be Commit's
|
244
|
+
# id
|
245
|
+
# }
|
246
|
+
# }
|
247
|
+
# }
|
248
|
+
# }
|
249
|
+
# }
|
250
|
+
|
251
|
+
field_access_paths.reject! {|path| path.compatible_object_types.empty?}
|
252
|
+
|
253
|
+
# Eliminate paths where the compatible object types are a subset of some other path's
|
254
|
+
# compatible object types. (These are redundant.)
|
255
|
+
supersets = T.let([], T::Array[FieldAccessPath])
|
256
|
+
|
257
|
+
# Put all of the supersets at the beginning of the array
|
258
|
+
field_access_paths.sort_by! {|path| -path.compatible_object_types.size}
|
259
|
+
field_access_paths.each do |path|
|
260
|
+
next if supersets.any? {|super_path| super_path.compatible_object_types.superset?(path.compatible_object_types)}
|
261
|
+
|
262
|
+
supersets.push(path)
|
263
|
+
end
|
264
|
+
|
265
|
+
field_access_paths.select! {|path| supersets.include?(path)}
|
266
|
+
field_access_paths.each(&:freeze)
|
267
|
+
field_access_paths.freeze
|
268
|
+
true
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Yogurt
|
5
|
+
class CodeGenerator
|
6
|
+
class FieldAccessPath < T::Struct
|
7
|
+
extend T::Sig
|
8
|
+
extend Utils
|
9
|
+
include Utils
|
10
|
+
include Memoize
|
11
|
+
|
12
|
+
# Name of the method
|
13
|
+
const :name, Symbol
|
14
|
+
|
15
|
+
# Sorbet signature for the value of the field
|
16
|
+
const :signature, String
|
17
|
+
|
18
|
+
# Expression for converting the value of the field
|
19
|
+
const :expression, String
|
20
|
+
|
21
|
+
# GraphQL schema for the query that this FieldAccessPath was derived from
|
22
|
+
const :schema, GRAPHQL_SCHEMA
|
23
|
+
|
24
|
+
# The types of all of the fragments leading to this field
|
25
|
+
const :fragment_types, T::Array[String]
|
26
|
+
|
27
|
+
sig {returns(T.self_type)}
|
28
|
+
def freeze
|
29
|
+
compatible_object_types
|
30
|
+
fragment_types.each(&:freeze)
|
31
|
+
fragment_types.freeze
|
32
|
+
expression.freeze
|
33
|
+
signature.freeze
|
34
|
+
super
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
# This field access path will only be evaluated if the object in the query
|
39
|
+
# is one of the objects in this set.
|
40
|
+
sig {returns(T::Set[String])}
|
41
|
+
def compatible_object_types
|
42
|
+
memoize_as(:compatible_object_types) do
|
43
|
+
result = schema.possible_types(schema.types[fragment_types.fetch(0)]).to_set
|
44
|
+
|
45
|
+
if fragment_types.size > 1
|
46
|
+
T.must(fragment_types[1..-1]).each do |next_type|
|
47
|
+
result = result.intersection(schema.possible_types(schema.types[next_type]))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
result.map(&:graphql_name).to_set.freeze
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|