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,33 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Yogurt
|
5
|
+
class CodeGenerator
|
6
|
+
class VariableDefinition < T::Struct
|
7
|
+
extend T::Sig
|
8
|
+
include Comparable
|
9
|
+
|
10
|
+
const :name, Symbol
|
11
|
+
const :graphql_name, String
|
12
|
+
const :signature, String
|
13
|
+
const :serializer, String
|
14
|
+
const :dependency, T.nilable(String)
|
15
|
+
|
16
|
+
sig {returns(T::Boolean)}
|
17
|
+
def optional?
|
18
|
+
signature.start_with?("T.nilable")
|
19
|
+
end
|
20
|
+
|
21
|
+
sig {params(other: VariableDefinition).returns(T.nilable(Integer))}
|
22
|
+
def <=>(other)
|
23
|
+
if optional? && !other.optional?
|
24
|
+
1
|
25
|
+
elsif other.optional? && !optional?
|
26
|
+
-1
|
27
|
+
else
|
28
|
+
name <=> other.name
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Yogurt
|
5
|
+
module Converters
|
6
|
+
class Date
|
7
|
+
extend T::Sig
|
8
|
+
extend ScalarConverter
|
9
|
+
|
10
|
+
sig {override.returns(T::Types::Base)}
|
11
|
+
def self.type_alias
|
12
|
+
T.type_alias {::Date}
|
13
|
+
end
|
14
|
+
|
15
|
+
sig {override.params(value: ::Date).returns(String)}
|
16
|
+
def self.serialize(value)
|
17
|
+
value.iso8601
|
18
|
+
end
|
19
|
+
|
20
|
+
sig {override.params(raw_value: SCALAR_TYPE).returns(::Date)}
|
21
|
+
def self.deserialize(raw_value)
|
22
|
+
raise "Unexpected value returned for Date: #{raw_value.inspect}" if !raw_value.is_a?(String)
|
23
|
+
|
24
|
+
::Date.iso8601(raw_value)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Time
|
29
|
+
extend T::Sig
|
30
|
+
extend ScalarConverter
|
31
|
+
|
32
|
+
sig {override.returns(T::Types::Base)}
|
33
|
+
def self.type_alias
|
34
|
+
T.type_alias {::Time}
|
35
|
+
end
|
36
|
+
|
37
|
+
sig {override.params(value: ::Time).returns(String)}
|
38
|
+
def self.serialize(value)
|
39
|
+
value.iso8601
|
40
|
+
end
|
41
|
+
|
42
|
+
sig {override.params(raw_value: SCALAR_TYPE).returns(::Time)}
|
43
|
+
def self.deserialize(raw_value)
|
44
|
+
raise "Unexpected value returned for Time: #{raw_value.inspect}" if !raw_value.is_a?(String)
|
45
|
+
|
46
|
+
::Time.iso8601(raw_value)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Yogurt
|
5
|
+
module ErrorResult
|
6
|
+
include Kernel
|
7
|
+
extend T::Sig
|
8
|
+
extend T::Helpers
|
9
|
+
|
10
|
+
interface!
|
11
|
+
|
12
|
+
sig {abstract.returns(T.nilable(T::Array[OBJECT_TYPE]))}
|
13
|
+
def errors; end
|
14
|
+
|
15
|
+
class OnlyErrors
|
16
|
+
extend T::Sig
|
17
|
+
include ErrorResult
|
18
|
+
|
19
|
+
sig {params(errors: T::Array[OBJECT_TYPE]).void}
|
20
|
+
def initialize(errors)
|
21
|
+
@errors = T.let(errors, T::Array[OBJECT_TYPE])
|
22
|
+
end
|
23
|
+
|
24
|
+
sig {override.returns(T.nilable(T::Array[OBJECT_TYPE]))}
|
25
|
+
def errors
|
26
|
+
@errors
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/yogurt/http.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'uri'
|
5
|
+
require 'net/http'
|
6
|
+
require 'json'
|
7
|
+
|
8
|
+
module Yogurt
|
9
|
+
class Http
|
10
|
+
extend T::Sig
|
11
|
+
include QueryExecutor
|
12
|
+
|
13
|
+
sig {params(uri: String, headers: T::Hash[String, String]).void}
|
14
|
+
def initialize(uri, headers: {})
|
15
|
+
parsed = URI.parse(uri)
|
16
|
+
if parsed.is_a?(URI::HTTP) || parsed.is_a?(URI::HTTPS)
|
17
|
+
@uri = T.let(parsed, T.any(URI::HTTP, URI::HTTPS))
|
18
|
+
else
|
19
|
+
raise ArgumentError, "Invalid URI: #{uri} (must be HTTP or HTTPS)"
|
20
|
+
end
|
21
|
+
|
22
|
+
@headers = T.let(headers, T::Hash[String, String])
|
23
|
+
end
|
24
|
+
|
25
|
+
# You can override this method in a subclass to set options on the HTTP request
|
26
|
+
sig {overridable.params(options: T.untyped).returns(T::Hash[String, String])}
|
27
|
+
def headers(options)
|
28
|
+
@headers
|
29
|
+
end
|
30
|
+
|
31
|
+
# You can override this in a subclass to get strongly typed options on auto-generated code
|
32
|
+
sig {override.returns(T::Types::Base)}
|
33
|
+
def options_type_alias
|
34
|
+
T.type_alias {T.untyped}
|
35
|
+
end
|
36
|
+
|
37
|
+
sig do
|
38
|
+
override.params(
|
39
|
+
query: String,
|
40
|
+
operation_name: String,
|
41
|
+
variables: T.nilable(T::Hash[String, T.untyped]),
|
42
|
+
options: T.untyped,
|
43
|
+
).returns(T::Hash[String, T.untyped])
|
44
|
+
end
|
45
|
+
def execute(query, operation_name:, variables: nil, options: nil)
|
46
|
+
request = Net::HTTP::Post.new(@uri.request_uri)
|
47
|
+
request.basic_auth(@uri.user, @uri.password) if @uri.user || @uri.password
|
48
|
+
|
49
|
+
request["Accept"] = "application/json"
|
50
|
+
request["Content-Type"] = "application/json"
|
51
|
+
|
52
|
+
headers(options).each do |name, value|
|
53
|
+
request[name] = value
|
54
|
+
end
|
55
|
+
|
56
|
+
body = T.let({}, T::Hash[String, T.untyped])
|
57
|
+
body["query"] = query
|
58
|
+
body["variables"] = variables if variables&.any?
|
59
|
+
body["operationName"] = operation_name
|
60
|
+
request.body = JSON.generate(body)
|
61
|
+
|
62
|
+
response = connection.request(request)
|
63
|
+
case response
|
64
|
+
when Net::HTTPOK, Net::HTTPBadRequest
|
65
|
+
JSON.parse(response.body)
|
66
|
+
else
|
67
|
+
{ "errors" => [{ "message" => "#{response.code} #{response.message}" }] }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns an HTTP connection. You can override in subclasses to customize the conection.
|
72
|
+
sig {overridable.returns(Net::HTTP)}
|
73
|
+
def connection
|
74
|
+
client = Net::HTTP.new(@uri.host, @uri.port)
|
75
|
+
client.use_ssl = @uri.scheme == "https"
|
76
|
+
|
77
|
+
client
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'pp'
|
5
|
+
|
6
|
+
module Yogurt
|
7
|
+
module Inspectable
|
8
|
+
extend T::Sig
|
9
|
+
extend T::Helpers
|
10
|
+
include PP::ObjectMixin
|
11
|
+
|
12
|
+
abstract!
|
13
|
+
|
14
|
+
sig {abstract.params(p: PP::PPMethods).void}
|
15
|
+
def pretty_print(p); end
|
16
|
+
|
17
|
+
sig {returns(String)}
|
18
|
+
def inspect
|
19
|
+
pretty_print_inspect
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Yogurt
|
5
|
+
module Memoize
|
6
|
+
extend T::Sig
|
7
|
+
|
8
|
+
sig do
|
9
|
+
type_parameters(:ReturnType)
|
10
|
+
.params(
|
11
|
+
name: Symbol,
|
12
|
+
block: T.proc.returns(T.type_parameter(:ReturnType)),
|
13
|
+
)
|
14
|
+
.returns(T.type_parameter(:ReturnType))
|
15
|
+
end
|
16
|
+
def memoize_as(name, &block)
|
17
|
+
memoized_values = @memoized_values
|
18
|
+
memoized_values = @memoized_values = {} if memoized_values.nil?
|
19
|
+
|
20
|
+
return memoized_values[name] if memoized_values.key?(name)
|
21
|
+
|
22
|
+
memoized_values[name] = yield
|
23
|
+
end
|
24
|
+
|
25
|
+
sig {returns(T.self_type)}
|
26
|
+
def freeze
|
27
|
+
@memoized_values = T.let(@memoized_values, T.nilable(T::Hash[Symbol, T.untyped]))
|
28
|
+
@memoized_values&.freeze
|
29
|
+
super
|
30
|
+
self
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/yogurt/query.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Yogurt
|
5
|
+
class Query
|
6
|
+
extend T::Sig
|
7
|
+
extend T::Helpers
|
8
|
+
abstract!
|
9
|
+
|
10
|
+
sig {params(result: OBJECT_TYPE).returns(T.any(T.attached_class, Yogurt::ErrorResult::OnlyErrors))}
|
11
|
+
def self.from_result(result)
|
12
|
+
data = result['data']
|
13
|
+
if data
|
14
|
+
new(data, result['errors'])
|
15
|
+
else
|
16
|
+
Yogurt::ErrorResult::OnlyErrors.new(result['errors'])
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
sig {params(data: OBJECT_TYPE, errors: T.nilable(OBJECT_TYPE)).void}
|
21
|
+
def initialize(data, errors); end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Yogurt
|
5
|
+
module QueryContainer
|
6
|
+
extend T::Sig
|
7
|
+
|
8
|
+
CONTAINER = T.type_alias do
|
9
|
+
T.all(Module, QueryContainer)
|
10
|
+
end
|
11
|
+
|
12
|
+
VALIDATION_RULES = T.let(
|
13
|
+
[
|
14
|
+
*GraphQL::StaticValidation::ALL_RULES,
|
15
|
+
InterfacesAndUnionsHaveTypename
|
16
|
+
].freeze,
|
17
|
+
T::Array[T.untyped],
|
18
|
+
)
|
19
|
+
|
20
|
+
sig {params(other: Module).void}
|
21
|
+
def self.included(other)
|
22
|
+
Kernel.raise(ValidationError, "You need to `extend Yogurt::QueryContainer`, you cannot use `include`.")
|
23
|
+
end
|
24
|
+
|
25
|
+
sig {params(other: Module).void}
|
26
|
+
def self.extended(other)
|
27
|
+
super
|
28
|
+
QueryContainer.containers << T.cast(other, CONTAINER)
|
29
|
+
end
|
30
|
+
|
31
|
+
sig {returns(T::Array[CONTAINER])}
|
32
|
+
def self.containers
|
33
|
+
@containers = T.let(@containers, T.nilable(T::Array[CONTAINER]))
|
34
|
+
@containers ||= []
|
35
|
+
end
|
36
|
+
|
37
|
+
sig {returns(T::Array[QueryDeclaration])}
|
38
|
+
def declared_queries
|
39
|
+
@declared_queries = T.let(@declared_queries, T.nilable(T::Array[QueryDeclaration]))
|
40
|
+
@declared_queries ||= []
|
41
|
+
end
|
42
|
+
|
43
|
+
sig {params(operation_name: String).returns(QueryDeclaration)}
|
44
|
+
def fetch_query(operation_name)
|
45
|
+
result = declared_queries.detect {|d| d.operations.include?(operation_name)}
|
46
|
+
T.must(result)
|
47
|
+
end
|
48
|
+
|
49
|
+
sig do
|
50
|
+
params(
|
51
|
+
query_text: String,
|
52
|
+
schema: T.nilable(T.class_of(GraphQL::Schema)),
|
53
|
+
).void
|
54
|
+
end
|
55
|
+
def declare_query(query_text, schema: nil)
|
56
|
+
schema ||= Yogurt.default_schema
|
57
|
+
Kernel.raise(ValidationError, "You need to either provide a `schema:` to declare_query, or set Yogurt.default_schema") if schema.nil?
|
58
|
+
|
59
|
+
case (container = self)
|
60
|
+
when Module
|
61
|
+
# noop
|
62
|
+
else
|
63
|
+
Kernel.raise(ValidationError, "You need to `extend Yogurt::QueryContainer`, you cannot use `include`.")
|
64
|
+
end
|
65
|
+
|
66
|
+
Kernel.raise(ValidationError, "Query containers must be classes or modules that are assigned to constants.") if container.name.nil?
|
67
|
+
|
68
|
+
validator = GraphQL::StaticValidation::Validator.new(schema: schema, rules: VALIDATION_RULES)
|
69
|
+
query = GraphQL::Query.new(schema, query_text)
|
70
|
+
validation_result = validator.validate(query)
|
71
|
+
|
72
|
+
error = validation_result[:errors][0]
|
73
|
+
Kernel.raise(ValidationError, error.message) if error
|
74
|
+
|
75
|
+
if query.operations.key?(nil)
|
76
|
+
Kernel.raise(ValidationError, "You must provide a name for each of the operations in your GraphQL query.")
|
77
|
+
elsif query.operations.none?
|
78
|
+
Kernel.raise(ValidationError, "Your query did not define any operations.")
|
79
|
+
end
|
80
|
+
|
81
|
+
declared_queries << QueryDeclaration.new(
|
82
|
+
container: container,
|
83
|
+
operations: query.operations.keys.freeze,
|
84
|
+
query_text: query_text,
|
85
|
+
schema: schema,
|
86
|
+
)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Yogurt
|
5
|
+
module QueryContainer
|
6
|
+
module InterfacesAndUnionsHaveTypename
|
7
|
+
include Kernel
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
class Error < GraphQL::StaticValidation::Error
|
11
|
+
extend T::Sig
|
12
|
+
|
13
|
+
sig {returns(T.nilable(String))}
|
14
|
+
attr_reader :type_name
|
15
|
+
|
16
|
+
sig {returns(String)}
|
17
|
+
attr_reader :node_name
|
18
|
+
|
19
|
+
sig do
|
20
|
+
params(
|
21
|
+
message: String,
|
22
|
+
node_name: String,
|
23
|
+
path: T.nilable(String),
|
24
|
+
nodes: T.untyped,
|
25
|
+
type: T.nilable(String),
|
26
|
+
).void
|
27
|
+
end
|
28
|
+
def initialize(message, node_name:, path: nil, nodes: [], type: nil)
|
29
|
+
super(message, path: path, nodes: nodes)
|
30
|
+
@node_name = T.let(node_name, String)
|
31
|
+
@type_name = T.let(type, T.nilable(String))
|
32
|
+
end
|
33
|
+
|
34
|
+
# A hash representation of this Message
|
35
|
+
sig {returns(T::Hash[String, T.untyped])}
|
36
|
+
def to_h
|
37
|
+
extensions = {
|
38
|
+
"code" => code,
|
39
|
+
"nodeName" => @node_name
|
40
|
+
}
|
41
|
+
|
42
|
+
extensions['typeName'] = @type_name if @type_name
|
43
|
+
super.merge({ "extensions" => extensions })
|
44
|
+
end
|
45
|
+
|
46
|
+
sig {returns(String)}
|
47
|
+
def code
|
48
|
+
"interfaceOrUnionMissingTypename"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
sig {params(node: GraphQL::Language::Nodes::Field, parent: T.untyped).void}
|
53
|
+
def on_field(node, parent)
|
54
|
+
super if validate_interface_union_includes_typename(node, T.unsafe(self).field_definition.type.unwrap)
|
55
|
+
end
|
56
|
+
|
57
|
+
sig {params(node: GraphQL::Language::Nodes::OperationDefinition, parent: T.untyped).void}
|
58
|
+
def on_operation_definition(node, parent)
|
59
|
+
super if validate_interface_union_includes_typename(node, T.unsafe(self).type_definition)
|
60
|
+
end
|
61
|
+
|
62
|
+
sig do
|
63
|
+
params(
|
64
|
+
node: T.any(GraphQL::Language::Nodes::Field, GraphQL::Language::Nodes::OperationDefinition),
|
65
|
+
type_definition: T.untyped,
|
66
|
+
).returns(T::Boolean)
|
67
|
+
end
|
68
|
+
private def validate_interface_union_includes_typename(node, type_definition)
|
69
|
+
return true if node.selections.nil?
|
70
|
+
return true if node.selections.empty?
|
71
|
+
|
72
|
+
type_kind = type_definition.kind
|
73
|
+
return true unless type_kind.interface? || type_kind.union?
|
74
|
+
return true if node.selections.any? do |selection|
|
75
|
+
next false unless selection.is_a?(GraphQL::Language::Nodes::Field)
|
76
|
+
next false unless selection.name == '__typename'
|
77
|
+
|
78
|
+
selection.alias.nil?
|
79
|
+
end
|
80
|
+
|
81
|
+
msg = "Interfaces and unions must include the __typename field (%{node_name} returns #{type_definition.graphql_name} but doesn't select __typename)"
|
82
|
+
node_name = case node
|
83
|
+
when GraphQL::Language::Nodes::Field
|
84
|
+
"field '#{node.name}'"
|
85
|
+
when GraphQL::Language::Nodes::OperationDefinition
|
86
|
+
if node.name.nil?
|
87
|
+
"anonymous query"
|
88
|
+
else
|
89
|
+
"#{node.operation_type} '#{node.name}'"
|
90
|
+
end
|
91
|
+
else
|
92
|
+
T.absurd(node)
|
93
|
+
end
|
94
|
+
|
95
|
+
error = Error.new(
|
96
|
+
format(msg, node_name: node_name),
|
97
|
+
nodes: node,
|
98
|
+
node_name: node_name,
|
99
|
+
type: type_definition.graphql_name,
|
100
|
+
)
|
101
|
+
|
102
|
+
send(:add_error, error)
|
103
|
+
false
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|