yogurt 0.1.1
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.
- 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
|