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,53 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Yogurt
|
5
|
+
class CodeGenerator
|
6
|
+
class GeneratedFile < T::Struct
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
class FileType < T::Enum
|
10
|
+
enums do
|
11
|
+
# File contains the definition of a GraphQL Enum
|
12
|
+
ENUM = new('enum')
|
13
|
+
|
14
|
+
# File contains the definition of a GraphQL operation (ie, a root query result)
|
15
|
+
OPERATION = new('operation')
|
16
|
+
|
17
|
+
# File contains the definition of a GraphQL object result (ie, a non-root query result)
|
18
|
+
OBJECT_RESULT = new('object_result')
|
19
|
+
|
20
|
+
# File contains the definition of a GraphQL input object
|
21
|
+
INPUT_OBJECT = new('input_object')
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# The name of the constant that is stored in this file
|
26
|
+
const :constant_name, String
|
27
|
+
|
28
|
+
# Type of constant that is stored in this file
|
29
|
+
const :type, FileType
|
30
|
+
|
31
|
+
# Names of the constants that this file references. If you are not using an
|
32
|
+
# autoloading tool, the files containing these constants need to be `require`'d
|
33
|
+
# at the start of the file.
|
34
|
+
const :dependencies, T::Array[String]
|
35
|
+
|
36
|
+
# The code that defines the constant that is stored in this file
|
37
|
+
const :code, String
|
38
|
+
|
39
|
+
# Full contents of the file
|
40
|
+
sig {returns(String)}
|
41
|
+
def contents
|
42
|
+
<<~STRING
|
43
|
+
# typed: strict
|
44
|
+
# frozen_string_literal: true
|
45
|
+
|
46
|
+
require 'pp'
|
47
|
+
|
48
|
+
#{code}
|
49
|
+
STRING
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Yogurt
|
5
|
+
class CodeGenerator
|
6
|
+
# Input classes are used for input objects
|
7
|
+
class InputClass < T::Struct
|
8
|
+
extend T::Sig
|
9
|
+
include Utils
|
10
|
+
include DefinedClass
|
11
|
+
|
12
|
+
const :name, String
|
13
|
+
const :arguments, T::Array[VariableDefinition]
|
14
|
+
|
15
|
+
sig {override.returns(T::Array[String])}
|
16
|
+
def dependencies
|
17
|
+
arguments.map(&:dependency).compact
|
18
|
+
end
|
19
|
+
|
20
|
+
sig {override.returns(String)}
|
21
|
+
def to_ruby
|
22
|
+
extract = []
|
23
|
+
props = []
|
24
|
+
serializers = []
|
25
|
+
arguments.sort.each do |definition|
|
26
|
+
props << "prop #{definition.name.inspect}, #{definition.signature}"
|
27
|
+
extract << "#{definition.name} = self.#{definition.name}"
|
28
|
+
serializers.push(<<~STRING.strip)
|
29
|
+
#{definition.graphql_name.inspect} => #{definition.serializer.strip},
|
30
|
+
STRING
|
31
|
+
end
|
32
|
+
|
33
|
+
<<~STRING
|
34
|
+
class #{name} < T::Struct
|
35
|
+
extend T::Sig
|
36
|
+
|
37
|
+
#{indent(props.join("\n"), 1).strip}
|
38
|
+
|
39
|
+
sig {returns(T::Hash[String, T.untyped])}
|
40
|
+
def serialize
|
41
|
+
#{indent(extract.join("\n"), 2).strip}
|
42
|
+
|
43
|
+
{
|
44
|
+
#{indent(serializers.join("\n"), 3).strip}
|
45
|
+
}
|
46
|
+
end
|
47
|
+
end
|
48
|
+
STRING
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Yogurt
|
5
|
+
class CodeGenerator
|
6
|
+
# Leaf classes are generated for the inner types of query results.
|
7
|
+
class LeafClass < T::Struct
|
8
|
+
include DefinedClass
|
9
|
+
extend T::Sig
|
10
|
+
include Utils
|
11
|
+
|
12
|
+
const :name, String
|
13
|
+
const :schema, GRAPHQL_SCHEMA
|
14
|
+
const :graphql_type, T.untyped # rubocop:disable Sorbet/ForbidUntypedStructProps
|
15
|
+
prop :defined_methods, T::Array[DefinedMethod]
|
16
|
+
prop :dependencies, T::Array[String]
|
17
|
+
|
18
|
+
# Adds the defined methods to the ones that are already defined in the class
|
19
|
+
sig {params(extra_methods: T::Array[DefinedMethod]).void}
|
20
|
+
def merge_defined_methods(extra_methods)
|
21
|
+
own_methods = defined_methods.map {|dm| [dm.name, dm]}.to_h
|
22
|
+
extra_methods.each do |extra|
|
23
|
+
own = own_methods[extra.name]
|
24
|
+
if own.nil?
|
25
|
+
own_methods[extra.name] = extra
|
26
|
+
elsif !own.merge?(extra)
|
27
|
+
raise "Cannot merge method #{extra.inspect} into #{own.inspect}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
self.defined_methods = own_methods.values
|
32
|
+
end
|
33
|
+
|
34
|
+
sig {override.returns(String)}
|
35
|
+
def to_ruby
|
36
|
+
pretty_print = generate_pretty_print(defined_methods)
|
37
|
+
|
38
|
+
dynamic_methods = <<~STRING.strip
|
39
|
+
#{defined_methods.map(&:to_ruby).join("\n")}
|
40
|
+
#{pretty_print}
|
41
|
+
STRING
|
42
|
+
|
43
|
+
<<~STRING
|
44
|
+
class #{name}
|
45
|
+
extend T::Sig
|
46
|
+
include Yogurt::QueryResult
|
47
|
+
|
48
|
+
#{indent(possible_types_constant(schema, graphql_type), 1).strip}
|
49
|
+
|
50
|
+
sig {params(result: Yogurt::OBJECT_TYPE).void}
|
51
|
+
def initialize(result)
|
52
|
+
@result = T.let(result, Yogurt::OBJECT_TYPE)
|
53
|
+
end
|
54
|
+
|
55
|
+
sig {override.returns(Yogurt::OBJECT_TYPE)}
|
56
|
+
def raw_result
|
57
|
+
@result
|
58
|
+
end
|
59
|
+
|
60
|
+
#{indent(typename_method(schema, graphql_type), 1).strip}
|
61
|
+
|
62
|
+
#{indent(dynamic_methods, 1).strip}
|
63
|
+
end
|
64
|
+
STRING
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Yogurt
|
5
|
+
class CodeGenerator
|
6
|
+
class OperationDeclaration < T::Struct
|
7
|
+
const :declaration, QueryDeclaration
|
8
|
+
const :operation_name, String
|
9
|
+
const :variables, T::Array[GraphQL::Language::Nodes::VariableDefinition]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Yogurt
|
5
|
+
class CodeGenerator
|
6
|
+
# Root classes are generated for the root of a GraphQL query.
|
7
|
+
class RootClass < T::Struct
|
8
|
+
include DefinedClass
|
9
|
+
extend T::Sig
|
10
|
+
include Utils
|
11
|
+
|
12
|
+
const :name, String
|
13
|
+
const :schema, GRAPHQL_SCHEMA
|
14
|
+
const :operation_name, String
|
15
|
+
const :graphql_type, T.untyped # rubocop:disable Sorbet/ForbidUntypedStructProps
|
16
|
+
const :query_container, QueryContainer::CONTAINER
|
17
|
+
const :defined_methods, T::Array[DefinedMethod]
|
18
|
+
const :variables, T::Array[VariableDefinition]
|
19
|
+
const :dependencies, T::Array[String]
|
20
|
+
|
21
|
+
sig {override.returns(String)}
|
22
|
+
def to_ruby
|
23
|
+
pretty_print = generate_pretty_print(defined_methods)
|
24
|
+
declaration = query_container.fetch_query(operation_name)
|
25
|
+
original_query = declaration.query_text
|
26
|
+
parsed_query = GraphQL.parse(original_query)
|
27
|
+
reprinted_query = GraphQL::Language::Printer.new.print(parsed_query)
|
28
|
+
|
29
|
+
dynamic_methods = <<~STRING.strip
|
30
|
+
#{defined_methods.map(&:to_ruby).join("\n")}
|
31
|
+
#{pretty_print}
|
32
|
+
STRING
|
33
|
+
|
34
|
+
<<~STRING
|
35
|
+
class #{name} < Yogurt::Query
|
36
|
+
extend T::Sig
|
37
|
+
include Yogurt::QueryResult
|
38
|
+
include Yogurt::ErrorResult
|
39
|
+
|
40
|
+
SCHEMA = T.let(#{schema.name}, Yogurt::GRAPHQL_SCHEMA)
|
41
|
+
OPERATION_NAME = T.let(#{operation_name.inspect}, String)
|
42
|
+
QUERY_TEXT = T.let(<<~'GRAPHQL', String)
|
43
|
+
#{indent(reprinted_query, 2).strip}
|
44
|
+
GRAPHQL
|
45
|
+
#{indent(possible_types_constant(schema, graphql_type), 1).strip}
|
46
|
+
|
47
|
+
#{indent(execute_method, 1).strip}
|
48
|
+
|
49
|
+
sig {params(data: Yogurt::OBJECT_TYPE, errors: T.nilable(T::Array[Yogurt::OBJECT_TYPE])).void}
|
50
|
+
def initialize(data, errors)
|
51
|
+
@result = T.let(data, Yogurt::OBJECT_TYPE)
|
52
|
+
@errors = T.let(errors, T.nilable(T::Array[Yogurt::OBJECT_TYPE]))
|
53
|
+
end
|
54
|
+
|
55
|
+
sig {override.returns(Yogurt::OBJECT_TYPE)}
|
56
|
+
def raw_result
|
57
|
+
@result
|
58
|
+
end
|
59
|
+
|
60
|
+
#{indent(typename_method(schema, graphql_type), 1).strip}
|
61
|
+
|
62
|
+
sig {override.returns(T.nilable(T::Array[Yogurt::OBJECT_TYPE]))}
|
63
|
+
def errors
|
64
|
+
@errors
|
65
|
+
end
|
66
|
+
|
67
|
+
#{indent(dynamic_methods, 1).strip}
|
68
|
+
end
|
69
|
+
STRING
|
70
|
+
end
|
71
|
+
|
72
|
+
sig {returns(String)}
|
73
|
+
def execute_method
|
74
|
+
executor = Yogurt.registered_schemas.fetch(schema)
|
75
|
+
options_type = executor.options_type_alias.name
|
76
|
+
signature_params = ["options: #{options_type}"]
|
77
|
+
|
78
|
+
params = if options_type.start_with?("T.nilable", "T.untyped")
|
79
|
+
["options=nil"]
|
80
|
+
else
|
81
|
+
["options"]
|
82
|
+
end
|
83
|
+
|
84
|
+
variable_extraction = if variables.any?
|
85
|
+
serializers = []
|
86
|
+
|
87
|
+
variables.sort.each do |variable|
|
88
|
+
if variable.signature.start_with?("T.nilable")
|
89
|
+
params.push("#{variable.name}: nil")
|
90
|
+
else
|
91
|
+
params.push("#{variable.name}:")
|
92
|
+
end
|
93
|
+
|
94
|
+
signature_params.push("#{variable.name}: #{variable.signature}")
|
95
|
+
serializers.push(<<~STRING.strip)
|
96
|
+
#{variable.graphql_name.inspect} => #{variable.serializer.strip},
|
97
|
+
STRING
|
98
|
+
end
|
99
|
+
|
100
|
+
<<~STRING
|
101
|
+
{
|
102
|
+
#{indent(serializers.join("\n"), 1).strip}
|
103
|
+
}
|
104
|
+
STRING
|
105
|
+
else
|
106
|
+
"nil"
|
107
|
+
end
|
108
|
+
|
109
|
+
<<~STRING
|
110
|
+
sig do
|
111
|
+
params(
|
112
|
+
#{indent(signature_params.join(",\n"), 2).strip}
|
113
|
+
).returns(T.any(T.attached_class, Yogurt::ErrorResult::OnlyErrors))
|
114
|
+
end
|
115
|
+
def self.execute(#{params.join(', ')})
|
116
|
+
raw_result = Yogurt.execute(
|
117
|
+
query: QUERY_TEXT,
|
118
|
+
schema: SCHEMA,
|
119
|
+
operation_name: OPERATION_NAME,
|
120
|
+
options: options,
|
121
|
+
variables: #{indent(variable_extraction, 2).strip}
|
122
|
+
)
|
123
|
+
|
124
|
+
from_result(raw_result)
|
125
|
+
end
|
126
|
+
STRING
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Yogurt
|
5
|
+
class CodeGenerator
|
6
|
+
class TypedOutput < T::Struct
|
7
|
+
# The signature to put in the sorbet type
|
8
|
+
const :signature, String
|
9
|
+
|
10
|
+
# Converter function to use for the return result.
|
11
|
+
# This assumes that a local variable named `raw_value` has the
|
12
|
+
# value to be converted.
|
13
|
+
const :deserializer, String
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Yogurt
|
5
|
+
class CodeGenerator
|
6
|
+
module Utils
|
7
|
+
extend T::Sig
|
8
|
+
extend self
|
9
|
+
|
10
|
+
sig {params(schema: GRAPHQL_SCHEMA, graphql_type: T.untyped).returns(String)}
|
11
|
+
def typename_method(schema, graphql_type)
|
12
|
+
possible_types = schema.possible_types(graphql_type)
|
13
|
+
|
14
|
+
if possible_types.size == 1
|
15
|
+
<<~STRING
|
16
|
+
sig {override.returns(String)}
|
17
|
+
def __typename
|
18
|
+
#{possible_types.fetch(0).graphql_name.inspect}
|
19
|
+
end
|
20
|
+
STRING
|
21
|
+
else
|
22
|
+
<<~STRING
|
23
|
+
sig {override.returns(String)}
|
24
|
+
def __typename
|
25
|
+
raw_result["__typename"]
|
26
|
+
end
|
27
|
+
STRING
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
sig {params(schema: GRAPHQL_SCHEMA, graphql_type: T.untyped).returns(String)}
|
32
|
+
def possible_types_constant(schema, graphql_type)
|
33
|
+
possible_types = schema
|
34
|
+
.possible_types(graphql_type)
|
35
|
+
.map(&:graphql_name)
|
36
|
+
.sort
|
37
|
+
.map(&:inspect)
|
38
|
+
|
39
|
+
single_line = possible_types.join(', ')
|
40
|
+
if single_line.size <= 80
|
41
|
+
<<~STRING.strip
|
42
|
+
POSSIBLE_TYPES = T.let(
|
43
|
+
[#{single_line}],
|
44
|
+
T::Array[String]
|
45
|
+
)
|
46
|
+
STRING
|
47
|
+
else
|
48
|
+
multi_line = possible_types.join(",\n")
|
49
|
+
<<~STRING.strip
|
50
|
+
POSSIBLE_TYPES = T.let(
|
51
|
+
[
|
52
|
+
#{indent(multi_line, 2).strip}
|
53
|
+
].freeze,
|
54
|
+
T::Array[String]
|
55
|
+
)
|
56
|
+
STRING
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
sig {params(camel_cased_word: String).returns(String)}
|
61
|
+
def underscore(camel_cased_word)
|
62
|
+
return camel_cased_word unless /[A-Z-]|::/.match?(camel_cased_word)
|
63
|
+
|
64
|
+
word = camel_cased_word.to_s.gsub("::", "/")
|
65
|
+
word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
|
66
|
+
word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
67
|
+
word.tr!("-", "_")
|
68
|
+
word.downcase!
|
69
|
+
word
|
70
|
+
end
|
71
|
+
|
72
|
+
sig {params(term: String).returns(String)}
|
73
|
+
def camelize(term)
|
74
|
+
string = term.to_s
|
75
|
+
string = string.sub(/^[a-z\d]*/, &:capitalize)
|
76
|
+
string.gsub!(%r{(?:_|(/))([a-z\d]*)}i) {"#{Regexp.last_match(1)}#{T.must(Regexp.last_match(2)).capitalize}"}
|
77
|
+
string.gsub!("/", "::")
|
78
|
+
string
|
79
|
+
end
|
80
|
+
|
81
|
+
sig {params(string: String, amount: Integer).returns(String)}
|
82
|
+
def indent(string, amount)
|
83
|
+
return string if amount.zero?
|
84
|
+
|
85
|
+
padding = ' ' * amount
|
86
|
+
|
87
|
+
buffer = T.unsafe(String).new("", capacity: string.size)
|
88
|
+
string.each_line do |line|
|
89
|
+
buffer << padding if line.size > 1 || line != "\n"
|
90
|
+
buffer << line
|
91
|
+
end
|
92
|
+
|
93
|
+
buffer
|
94
|
+
end
|
95
|
+
|
96
|
+
sig {params(desired_name: String).returns(Symbol)}
|
97
|
+
def generate_method_name(desired_name)
|
98
|
+
base_desired_name = desired_name
|
99
|
+
escaping_level = 0
|
100
|
+
|
101
|
+
while PROTECTED_NAMES.include?(desired_name)
|
102
|
+
escaping_level += 1
|
103
|
+
desired_name = "#{base_desired_name}#{'_' * escaping_level}"
|
104
|
+
end
|
105
|
+
|
106
|
+
desired_name.to_sym
|
107
|
+
end
|
108
|
+
|
109
|
+
sig {params(methods: T::Array[DefinedMethod]).returns(String)}
|
110
|
+
def generate_pretty_print(methods)
|
111
|
+
inspect_lines = methods.map do |dm|
|
112
|
+
<<~STRING
|
113
|
+
p.comma_breakable
|
114
|
+
p.text(#{dm.name.to_s.inspect})
|
115
|
+
p.text(': ')
|
116
|
+
p.pp(#{dm.name})
|
117
|
+
STRING
|
118
|
+
end
|
119
|
+
|
120
|
+
object_group = <<~STRING.strip
|
121
|
+
p.breakable
|
122
|
+
p.text('__typename')
|
123
|
+
p.text(': ')
|
124
|
+
p.pp(__typename)
|
125
|
+
|
126
|
+
#{inspect_lines.join("\n\n")}
|
127
|
+
STRING
|
128
|
+
|
129
|
+
<<~STRING
|
130
|
+
sig {override.params(p: PP::PPMethods).void}
|
131
|
+
def pretty_print(p)
|
132
|
+
p.object_group(self) do
|
133
|
+
#{indent(object_group, 2).strip}
|
134
|
+
end
|
135
|
+
end
|
136
|
+
STRING
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|