tapioca 0.4.17 → 0.4.22
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 +4 -4
- data/Gemfile +13 -14
- data/README.md +4 -2
- data/exe/tapioca +17 -2
- data/lib/tapioca.rb +1 -27
- data/lib/tapioca/cli.rb +1 -108
- data/lib/tapioca/cli/main.rb +146 -0
- data/lib/tapioca/compilers/dsl/active_job.rb +71 -0
- data/lib/tapioca/compilers/dsl/active_record_columns.rb +4 -4
- data/lib/tapioca/compilers/dsl/active_record_scope.rb +1 -1
- data/lib/tapioca/compilers/dsl/base.rb +1 -1
- data/lib/tapioca/compilers/dsl/protobuf.rb +132 -16
- data/lib/tapioca/compilers/dsl/url_helpers.rb +3 -3
- data/lib/tapioca/compilers/requires_compiler.rb +1 -1
- data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +264 -301
- data/lib/tapioca/config.rb +1 -1
- data/lib/tapioca/config_builder.rb +7 -12
- data/lib/tapioca/core_ext/string.rb +18 -0
- data/lib/tapioca/gemfile.rb +1 -1
- data/lib/tapioca/generator.rb +144 -37
- data/lib/tapioca/generic_type_registry.rb +222 -0
- data/lib/tapioca/internal.rb +28 -0
- data/lib/tapioca/rbi/model.rb +405 -0
- data/lib/tapioca/rbi/printer.rb +410 -0
- data/lib/tapioca/rbi/rewriters/group_nodes.rb +106 -0
- data/lib/tapioca/rbi/rewriters/nest_non_public_methods.rb +65 -0
- data/lib/tapioca/rbi/rewriters/nest_singleton_methods.rb +42 -0
- data/lib/tapioca/rbi/rewriters/sort_nodes.rb +82 -0
- data/lib/tapioca/rbi/visitor.rb +21 -0
- data/lib/tapioca/sorbet_ext/generic_name_patch.rb +66 -0
- data/lib/tapioca/sorbet_ext/name_patch.rb +16 -0
- data/lib/tapioca/version.rb +1 -1
- metadata +17 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8e3ea6e1fb437f52cfb699c43b4c3d46115592ac09a69aec5a8d0701906f42cd
|
4
|
+
data.tar.gz: 9fba45117a3f1f72ab8d3278838fb7d4a860eaa7cadc03535d99be618e8994c9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6519978a03ff03499f223d810a9f0d2bfd74e406017efb300a72fff0d95e1e08b64d2165ae1b54bad48f8767c5f831fe5f4398185282a5643224499355d7badc
|
7
|
+
data.tar.gz: 28ace672d799beb84aa802fe0773e574d3a827c53e15564d643dbe04e6330d6abb11d640a06b064641781258600ce73f3b5695915caaf9ce0813679dede867b2
|
data/Gemfile
CHANGED
@@ -4,18 +4,18 @@ source("https://rubygems.org")
|
|
4
4
|
|
5
5
|
gemspec
|
6
6
|
|
7
|
-
gem 'rubocop-shopify', require: false
|
8
|
-
|
9
|
-
group(:deployment, :development) do
|
10
|
-
gem("rake")
|
11
|
-
end
|
12
|
-
|
13
|
-
gem("yard", "~> 0.9.25")
|
14
|
-
gem("pry-byebug")
|
15
7
|
gem("minitest")
|
16
8
|
gem("minitest-hooks")
|
17
9
|
gem("minitest-reporters")
|
10
|
+
gem("pry-byebug")
|
11
|
+
gem("rubocop-shopify", require: false)
|
12
|
+
gem("rubocop-sorbet", ">= 0.4.1")
|
18
13
|
gem("sorbet")
|
14
|
+
gem("yard", "~> 0.9.25")
|
15
|
+
|
16
|
+
group(:deployment, :development) do
|
17
|
+
gem("rake")
|
18
|
+
end
|
19
19
|
|
20
20
|
group(:development, :test) do
|
21
21
|
gem("smart_properties", ">= 1.15.0", require: false)
|
@@ -26,14 +26,13 @@ group(:development, :test) do
|
|
26
26
|
gem("activerecord-typedstore", "~> 1.3", require: false)
|
27
27
|
gem("sqlite3")
|
28
28
|
gem("identity_cache", "~> 1.0", require: false)
|
29
|
-
gem(
|
30
|
-
ref:
|
29
|
+
gem("cityhash", git: "https://github.com/csfrancis/cityhash.git",
|
30
|
+
ref: "3cfc7d01f333c01811d5e834f1495eaa29f87c36", require: false)
|
31
31
|
gem("activemodel-serializers-xml", "~> 1.0", require: false)
|
32
32
|
gem("activeresource", "~> 5.1", require: false)
|
33
|
-
gem("google-protobuf", "~>3.12.0", require: false)
|
33
|
+
gem("google-protobuf", "~> 3.12.0", require: false)
|
34
34
|
# Fix version to 0.14.1 since it is the last version to support Ruby 2.4
|
35
35
|
gem("shopify-money", "= 0.14.1", require: false)
|
36
|
-
gem("sidekiq", "~>5.0", require: false) # Version 6 dropped support for Ruby 2.4
|
36
|
+
gem("sidekiq", "~> 5.0", require: false) # Version 6 dropped support for Ruby 2.4
|
37
|
+
gem("nokogiri", "1.10.10", require: false) # Lock to last supported for Ruby 2.4
|
37
38
|
end
|
38
|
-
|
39
|
-
gem "rubocop-sorbet", ">= 0.4.1"
|
data/README.md
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
> :warning: **Note**: This software is currently under active development. The API and interface should be considered unstable until a v1.0.0 release.
|
2
|
+
|
1
3
|
# Tapioca
|
2
4
|
|
3
5
|

|
@@ -14,7 +16,7 @@ For gems that have a normal default `require` and load all of their constants th
|
|
14
16
|
|
15
17
|
For example, suppose you are using the class `BetterHtml::Parser` exported from the `better_html` gem. Just doing a `require "better_html"` (which is the default require) does not load that type:
|
16
18
|
|
17
|
-
```
|
19
|
+
```shell
|
18
20
|
$ bundle exec pry
|
19
21
|
[1] pry(main)> require 'better_html'
|
20
22
|
=> true
|
@@ -110,7 +112,7 @@ This will generate DSL RBIs for specified constants (or for all handled constant
|
|
110
112
|
- `--prerequire [file]`: A file to be required before `Bundler.require` is called.
|
111
113
|
- `--postrequire [file]`: A file to be required after `Bundler.require` is called.
|
112
114
|
- `--out [directory]`: The output directory for generated RBI files, default to `sorbet/rbi/gems`.
|
113
|
-
- `--generate-command [command]`: The command to run to regenerate RBI files (used in header comment of the RBI files), defaults to the current command.
|
115
|
+
- `--generate-command [command]`: **[DEPRECATED]** The command to run to regenerate RBI files (used in header comment of the RBI files), defaults to the current command.
|
114
116
|
- `--typed-overrides [gem:level]`: Overrides typed sigils for generated gem RBIs for gem `gem` to level `level` (`level` can be one of `ignore`, `false`, `true`, `strict`, or `strong`, see [the Sorbet docs](https://sorbet.org/docs/static#file-level-granularity-strictness-levels) for more details).
|
115
117
|
|
116
118
|
## Contributing
|
data/exe/tapioca
CHANGED
@@ -1,6 +1,21 @@
|
|
1
1
|
#! /usr/bin/env ruby
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
|
4
|
+
require 'sorbet-runtime'
|
5
5
|
|
6
|
-
|
6
|
+
begin
|
7
|
+
T::Configuration.default_checked_level = :never
|
8
|
+
# Suppresses errors caused by T.cast, T.let, T.must, etc.
|
9
|
+
T::Configuration.inline_type_error_handler = ->(*) {}
|
10
|
+
# Suppresses errors caused by incorrect parameter ordering
|
11
|
+
T::Configuration.sig_validation_error_handler = ->(*) {}
|
12
|
+
rescue
|
13
|
+
# Need this rescue so that if another gem has
|
14
|
+
# already set the checked level by the time we
|
15
|
+
# get to it, we don't fail outright.
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
|
19
|
+
require_relative "../lib/tapioca/internal"
|
20
|
+
|
21
|
+
Tapioca::Cli::Main.start(ARGV)
|
data/lib/tapioca.rb
CHANGED
@@ -17,31 +17,5 @@ module Tapioca
|
|
17
17
|
class Error < StandardError; end
|
18
18
|
end
|
19
19
|
|
20
|
-
|
21
|
-
T::Configuration.default_checked_level = :never
|
22
|
-
# Suppresses errors caused by T.cast, T.let, T.must, etc.
|
23
|
-
T::Configuration.inline_type_error_handler = ->(*) {}
|
24
|
-
# Suppresses errors caused by incorrect parameter ordering
|
25
|
-
T::Configuration.sig_validation_error_handler = ->(*) {}
|
26
|
-
rescue
|
27
|
-
# Need this rescue so that if another gem has
|
28
|
-
# already set the checked level by the time we
|
29
|
-
# get to it, we don't fail outright.
|
30
|
-
nil
|
31
|
-
end
|
32
|
-
|
33
|
-
require "tapioca/loader"
|
34
|
-
require "tapioca/constant_locator"
|
35
|
-
require "tapioca/config"
|
36
|
-
require "tapioca/config_builder"
|
37
|
-
require "tapioca/generator"
|
38
|
-
require "tapioca/cli"
|
39
|
-
require "tapioca/gemfile"
|
40
|
-
require "tapioca/compilers/sorbet"
|
41
|
-
require "tapioca/compilers/requires_compiler"
|
42
|
-
require "tapioca/compilers/symbol_table_compiler"
|
43
|
-
require "tapioca/compilers/symbol_table/symbol_generator"
|
44
|
-
require "tapioca/compilers/symbol_table/symbol_loader"
|
45
|
-
require "tapioca/compilers/todos_compiler"
|
46
|
-
require "tapioca/compilers/dsl_compiler"
|
20
|
+
require "tapioca/compilers/dsl/base"
|
47
21
|
require "tapioca/version"
|
data/lib/tapioca/cli.rb
CHANGED
@@ -4,112 +4,5 @@
|
|
4
4
|
require 'thor'
|
5
5
|
|
6
6
|
module Tapioca
|
7
|
-
|
8
|
-
include(Thor::Actions)
|
9
|
-
|
10
|
-
class_option :prerequire,
|
11
|
-
aliases: ["--pre", "-b"],
|
12
|
-
banner: "file",
|
13
|
-
desc: "A file to be required before Bundler.require is called"
|
14
|
-
class_option :postrequire,
|
15
|
-
aliases: ["--post", "-a"],
|
16
|
-
banner: "file",
|
17
|
-
desc: "A file to be required after Bundler.require is called"
|
18
|
-
class_option :outdir,
|
19
|
-
aliases: ["--out", "-o"],
|
20
|
-
banner: "directory",
|
21
|
-
desc: "The output directory for generated RBI files"
|
22
|
-
class_option :generate_command,
|
23
|
-
aliases: ["--cmd", "-c"],
|
24
|
-
banner: "command",
|
25
|
-
desc: "The command to run to regenerate RBI files"
|
26
|
-
class_option :exclude,
|
27
|
-
aliases: ["-x"],
|
28
|
-
type: :array,
|
29
|
-
banner: "gem [gem ...]",
|
30
|
-
desc: "Excludes the given gem(s) from RBI generation"
|
31
|
-
class_option :typed_overrides,
|
32
|
-
aliases: ["--typed", "-t"],
|
33
|
-
type: :hash,
|
34
|
-
banner: "gem:level [gem:level ...]",
|
35
|
-
desc: "Overrides for typed sigils for generated gem RBIs"
|
36
|
-
|
37
|
-
map T.unsafe(%w[--version -v] => :__print_version)
|
38
|
-
|
39
|
-
desc "init", "initializes folder structure"
|
40
|
-
def init
|
41
|
-
create_file(Config::SORBET_CONFIG, skip: true) do
|
42
|
-
<<~CONTENT
|
43
|
-
--dir
|
44
|
-
.
|
45
|
-
CONTENT
|
46
|
-
end
|
47
|
-
create_file(Config::DEFAULT_POSTREQUIRE, skip: true) do
|
48
|
-
<<~CONTENT
|
49
|
-
# typed: false
|
50
|
-
# frozen_string_literal: true
|
51
|
-
|
52
|
-
# Add your extra requires here
|
53
|
-
CONTENT
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
desc "require", "generate the list of files to be required by tapioca"
|
58
|
-
def require
|
59
|
-
Tapioca.silence_warnings do
|
60
|
-
generator.build_requires
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
desc "todo", "generate the list of unresolved constants"
|
65
|
-
def todo
|
66
|
-
Tapioca.silence_warnings do
|
67
|
-
generator.build_todos
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
desc "dsl [constant...]", "generate RBIs for dynamic methods"
|
72
|
-
option :generators,
|
73
|
-
type: :array,
|
74
|
-
aliases: ["--gen", "-g"],
|
75
|
-
banner: "generator [generator ...]",
|
76
|
-
desc: "Only run supplied DSL generators"
|
77
|
-
def dsl(*constants)
|
78
|
-
Tapioca.silence_warnings do
|
79
|
-
generator.build_dsl(constants)
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
desc "generate [gem...]", "generate RBIs from gems"
|
84
|
-
def generate(*gems)
|
85
|
-
Tapioca.silence_warnings do
|
86
|
-
generator.build_gem_rbis(gems)
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
|
-
desc "sync", "sync RBIs to Gemfile"
|
91
|
-
def sync
|
92
|
-
Tapioca.silence_warnings do
|
93
|
-
generator.sync_rbis_with_gemfile
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
desc "--version, -v", "show version"
|
98
|
-
def __print_version
|
99
|
-
puts "Tapioca v#{Tapioca::VERSION}"
|
100
|
-
end
|
101
|
-
|
102
|
-
no_commands do
|
103
|
-
def self.exit_on_failure?
|
104
|
-
true
|
105
|
-
end
|
106
|
-
|
107
|
-
def generator
|
108
|
-
current_command = T.must(current_command_chain.first)
|
109
|
-
@generator ||= Generator.new(
|
110
|
-
ConfigBuilder.from_options(current_command, options)
|
111
|
-
)
|
112
|
-
end
|
113
|
-
end
|
114
|
-
end
|
7
|
+
module Cli; end
|
115
8
|
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Tapioca
|
5
|
+
module Cli
|
6
|
+
class Main < Thor
|
7
|
+
include(Thor::Actions)
|
8
|
+
|
9
|
+
class_option :prerequire,
|
10
|
+
aliases: ["--pre", "-b"],
|
11
|
+
banner: "file",
|
12
|
+
desc: "A file to be required before Bundler.require is called"
|
13
|
+
class_option :postrequire,
|
14
|
+
aliases: ["--post", "-a"],
|
15
|
+
banner: "file",
|
16
|
+
desc: "A file to be required after Bundler.require is called"
|
17
|
+
class_option :outdir,
|
18
|
+
aliases: ["--out", "-o"],
|
19
|
+
banner: "directory",
|
20
|
+
desc: "The output directory for generated RBI files"
|
21
|
+
class_option :generate_command,
|
22
|
+
aliases: ["--cmd", "-c"],
|
23
|
+
banner: "command",
|
24
|
+
desc: "The command to run to regenerate RBI files"
|
25
|
+
class_option :exclude,
|
26
|
+
aliases: ["-x"],
|
27
|
+
type: :array,
|
28
|
+
banner: "gem [gem ...]",
|
29
|
+
desc: "Excludes the given gem(s) from RBI generation"
|
30
|
+
class_option :typed_overrides,
|
31
|
+
aliases: ["--typed", "-t"],
|
32
|
+
type: :hash,
|
33
|
+
banner: "gem:level [gem:level ...]",
|
34
|
+
desc: "Overrides for typed sigils for generated gem RBIs"
|
35
|
+
|
36
|
+
map T.unsafe(%w[--version -v] => :__print_version)
|
37
|
+
|
38
|
+
desc "init", "initializes folder structure"
|
39
|
+
def init
|
40
|
+
create_config
|
41
|
+
create_post_require
|
42
|
+
generate_binstub
|
43
|
+
end
|
44
|
+
|
45
|
+
desc "require", "generate the list of files to be required by tapioca"
|
46
|
+
def require
|
47
|
+
Tapioca.silence_warnings do
|
48
|
+
generator.build_requires
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
desc "todo", "generate the list of unresolved constants"
|
53
|
+
def todo
|
54
|
+
Tapioca.silence_warnings do
|
55
|
+
generator.build_todos
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
desc "dsl [constant...]", "generate RBIs for dynamic methods"
|
60
|
+
option :generators,
|
61
|
+
type: :array,
|
62
|
+
aliases: ["--gen", "-g"],
|
63
|
+
banner: "generator [generator ...]",
|
64
|
+
desc: "Only run supplied DSL generators"
|
65
|
+
option :verify,
|
66
|
+
type: :boolean,
|
67
|
+
default: false,
|
68
|
+
desc: "Verifies RBIs are up-to-date"
|
69
|
+
option :quiet,
|
70
|
+
aliases: ["-q"],
|
71
|
+
type: :boolean,
|
72
|
+
desc: "Supresses file creation output"
|
73
|
+
def dsl(*constants)
|
74
|
+
Tapioca.silence_warnings do
|
75
|
+
generator.build_dsl(constants, should_verify: options[:verify], quiet: options[:quiet])
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
desc "generate [gem...]", "generate RBIs from gems"
|
80
|
+
def generate(*gems)
|
81
|
+
Tapioca.silence_warnings do
|
82
|
+
generator.build_gem_rbis(gems)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
desc "sync", "sync RBIs to Gemfile"
|
87
|
+
def sync
|
88
|
+
Tapioca.silence_warnings do
|
89
|
+
generator.sync_rbis_with_gemfile
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
desc "--version, -v", "show version"
|
94
|
+
def __print_version
|
95
|
+
puts "Tapioca v#{Tapioca::VERSION}"
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def create_config
|
101
|
+
create_file(Config::SORBET_CONFIG, skip: true) do
|
102
|
+
<<~CONTENT
|
103
|
+
--dir
|
104
|
+
.
|
105
|
+
CONTENT
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def create_post_require
|
110
|
+
create_file(Config::DEFAULT_POSTREQUIRE, skip: true) do
|
111
|
+
<<~CONTENT
|
112
|
+
# typed: false
|
113
|
+
# frozen_string_literal: true
|
114
|
+
|
115
|
+
# Add your extra requires here
|
116
|
+
CONTENT
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def generate_binstub
|
121
|
+
bin_stub_exists = File.exist?("bin/tapioca")
|
122
|
+
installer = Bundler::Installer.new(Bundler.root, Bundler.definition)
|
123
|
+
spec = Bundler.definition.specs.find { |s| s.name == "tapioca" }
|
124
|
+
installer.generate_bundler_executable_stubs(spec, { force: true })
|
125
|
+
if bin_stub_exists
|
126
|
+
shell.say_status(:force, "bin/tapioca", :yellow)
|
127
|
+
else
|
128
|
+
shell.say_status(:create, "bin/tapioca", :green)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
no_commands do
|
133
|
+
def self.exit_on_failure?
|
134
|
+
true
|
135
|
+
end
|
136
|
+
|
137
|
+
def generator
|
138
|
+
current_command = T.must(current_command_chain.first)
|
139
|
+
@generator ||= Generator.new(
|
140
|
+
ConfigBuilder.from_options(current_command, options)
|
141
|
+
)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "parlour"
|
5
|
+
|
6
|
+
begin
|
7
|
+
require "active_job"
|
8
|
+
rescue LoadError
|
9
|
+
return
|
10
|
+
end
|
11
|
+
|
12
|
+
module Tapioca
|
13
|
+
module Compilers
|
14
|
+
module Dsl
|
15
|
+
# `Tapioca::Compilers::Dsl::ActiveJob` generates RBI files for subclasses of
|
16
|
+
# [`ActiveJob::Base`](https://api.rubyonrails.org/classes/ActiveJob/Base.html).
|
17
|
+
#
|
18
|
+
# For example, with the following `ActiveJob` subclass:
|
19
|
+
#
|
20
|
+
# ~~~rb
|
21
|
+
# class NotifyUserJob < ActiveJob::Base
|
22
|
+
# def perform(user)
|
23
|
+
# # ...
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
# ~~~
|
27
|
+
#
|
28
|
+
# this generator will produce the RBI file `notify_user_job.rbi` with the following content:
|
29
|
+
#
|
30
|
+
# ~~~rbi
|
31
|
+
# # notify_user_job.rbi
|
32
|
+
# # typed: true
|
33
|
+
# class NotifyUserJob
|
34
|
+
# sig { params(user: T.untyped).returns(NotifyUserJob) }
|
35
|
+
# def self.perform_later(user); end
|
36
|
+
#
|
37
|
+
# sig { params(user: T.untyped).returns(NotifyUserJob) }
|
38
|
+
# def self.perform_now(user); end
|
39
|
+
# end
|
40
|
+
# ~~~
|
41
|
+
class ActiveJob < Base
|
42
|
+
extend T::Sig
|
43
|
+
|
44
|
+
sig { override.params(root: Parlour::RbiGenerator::Namespace, constant: T.class_of(::ActiveJob::Base)).void }
|
45
|
+
def decorate(root, constant)
|
46
|
+
root.path(constant) do |job|
|
47
|
+
next unless constant.instance_methods(false).include?(:perform)
|
48
|
+
|
49
|
+
method = constant.instance_method(:perform)
|
50
|
+
parameters = compile_method_parameters_to_parlour(method)
|
51
|
+
|
52
|
+
%w[perform_later perform_now].each do |name|
|
53
|
+
create_method(
|
54
|
+
job,
|
55
|
+
name,
|
56
|
+
parameters: parameters,
|
57
|
+
return_type: constant.name,
|
58
|
+
class_method: true
|
59
|
+
)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
sig { override.returns(T::Enumerable[Module]) }
|
65
|
+
def gather_constants
|
66
|
+
::ActiveJob::Base.descendants
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|