tapioca 0.4.27 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +14 -14
  3. data/README.md +2 -2
  4. data/Rakefile +5 -7
  5. data/exe/tapioca +2 -2
  6. data/lib/tapioca/cli.rb +256 -2
  7. data/lib/tapioca/compilers/dsl/aasm.rb +122 -0
  8. data/lib/tapioca/compilers/dsl/action_controller_helpers.rb +52 -12
  9. data/lib/tapioca/compilers/dsl/action_mailer.rb +6 -9
  10. data/lib/tapioca/compilers/dsl/active_job.rb +8 -12
  11. data/lib/tapioca/compilers/dsl/active_model_attributes.rb +131 -0
  12. data/lib/tapioca/compilers/dsl/active_record_associations.rb +33 -54
  13. data/lib/tapioca/compilers/dsl/active_record_columns.rb +10 -105
  14. data/lib/tapioca/compilers/dsl/active_record_enum.rb +8 -10
  15. data/lib/tapioca/compilers/dsl/active_record_scope.rb +7 -10
  16. data/lib/tapioca/compilers/dsl/active_record_typed_store.rb +5 -8
  17. data/lib/tapioca/compilers/dsl/active_resource.rb +9 -37
  18. data/lib/tapioca/compilers/dsl/active_storage.rb +98 -0
  19. data/lib/tapioca/compilers/dsl/active_support_concern.rb +108 -0
  20. data/lib/tapioca/compilers/dsl/active_support_current_attributes.rb +13 -8
  21. data/lib/tapioca/compilers/dsl/base.rb +96 -82
  22. data/lib/tapioca/compilers/dsl/config.rb +111 -0
  23. data/lib/tapioca/compilers/dsl/frozen_record.rb +5 -7
  24. data/lib/tapioca/compilers/dsl/identity_cache.rb +66 -29
  25. data/lib/tapioca/compilers/dsl/protobuf.rb +19 -69
  26. data/lib/tapioca/compilers/dsl/sidekiq_worker.rb +25 -12
  27. data/lib/tapioca/compilers/dsl/smart_properties.rb +19 -31
  28. data/lib/tapioca/compilers/dsl/state_machines.rb +56 -78
  29. data/lib/tapioca/compilers/dsl/url_helpers.rb +7 -10
  30. data/lib/tapioca/compilers/dsl_compiler.rb +22 -38
  31. data/lib/tapioca/compilers/requires_compiler.rb +2 -2
  32. data/lib/tapioca/compilers/sorbet.rb +26 -5
  33. data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +138 -153
  34. data/lib/tapioca/compilers/symbol_table/symbol_loader.rb +4 -4
  35. data/lib/tapioca/compilers/todos_compiler.rb +1 -1
  36. data/lib/tapioca/config.rb +2 -0
  37. data/lib/tapioca/config_builder.rb +4 -2
  38. data/lib/tapioca/constant_locator.rb +6 -8
  39. data/lib/tapioca/gemfile.rb +2 -4
  40. data/lib/tapioca/generator.rb +124 -40
  41. data/lib/tapioca/generic_type_registry.rb +25 -98
  42. data/lib/tapioca/helpers/active_record_column_type_helper.rb +98 -0
  43. data/lib/tapioca/internal.rb +2 -9
  44. data/lib/tapioca/loader.rb +13 -33
  45. data/lib/tapioca/rbi_ext/model.rb +122 -0
  46. data/lib/tapioca/reflection.rb +131 -0
  47. data/lib/tapioca/sorbet_ext/fixed_hash_patch.rb +1 -1
  48. data/lib/tapioca/sorbet_ext/generic_name_patch.rb +72 -4
  49. data/lib/tapioca/sorbet_ext/name_patch.rb +1 -1
  50. data/lib/tapioca/version.rb +1 -1
  51. data/lib/tapioca.rb +2 -1
  52. metadata +34 -22
  53. data/lib/tapioca/cli/main.rb +0 -146
  54. data/lib/tapioca/core_ext/class.rb +0 -28
  55. data/lib/tapioca/core_ext/string.rb +0 -18
  56. data/lib/tapioca/rbi/model.rb +0 -405
  57. data/lib/tapioca/rbi/printer.rb +0 -410
  58. data/lib/tapioca/rbi/rewriters/group_nodes.rb +0 -106
  59. data/lib/tapioca/rbi/rewriters/nest_non_public_methods.rb +0 -65
  60. data/lib/tapioca/rbi/rewriters/nest_singleton_methods.rb +0 -42
  61. data/lib/tapioca/rbi/rewriters/sort_nodes.rb +0 -86
  62. data/lib/tapioca/rbi/visitor.rb +0 -21
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 91440af4ff5a8cd586284d4d5a2cc35600ec803b825bdcd57492a3eb94b65bf3
4
- data.tar.gz: d6db15fe94c5cd0e8100f8bf1fd8fb9a760123891cee3b079470011719dc55e0
3
+ metadata.gz: f062008a7e93aa3119ee191492917febf4a97294753865bb58ae0bac91e582d8
4
+ data.tar.gz: d4b89c7368da54276c54977ae742ce1765178ac6700193a9aefb5c1e65bc9dcd
5
5
  SHA512:
6
- metadata.gz: 536bcacea8d4b5848dc8075fc94f50d34d80c23f00b9ea331862f911be6eb58c3a62b30d529663ddfd60441f632aab62e6250d8dc1c7a1a8f32dbfc0d510ae18
7
- data.tar.gz: 57942419606b3d054fb392b6c3c026bff5aa7f63cb3cfc58defb8dbc81e49e72902eb26a8ebb9713561f21d4822a644fd6b9a36c7f2c0a135461b3b4e1d9c267
6
+ metadata.gz: 4466416f851f4e6a7e8342a8dae5229e38ede32da3823c0a7cdd97dc3bff6a3e3711d983d5552fc083725789bb3b0a6899b4cf2edcc5c855bc33224054598987
7
+ data.tar.gz: d66b2a55050aae7089d7349e9c4e43cc1a2e2c1f181805f5c9d6b1a1e2c26c3b3c7b9d939efc0580f4d83cde4c615dd13e4d40f30682c2cfc441fde58e22f23a
data/Gemfile CHANGED
@@ -18,21 +18,21 @@ group(:deployment, :development) do
18
18
  end
19
19
 
20
20
  group(:development, :test) do
21
- gem("smart_properties", ">= 1.15.0", require: false)
22
- gem("frozen_record", ">= 0.17", require: false)
23
- gem("sprockets", "~> 3.7", require: false)
24
- gem("rails", "~> 5.2", require: false)
25
- gem("state_machines", "~> 0.5.0", require: false)
26
- gem("activerecord-typedstore", "~> 1.3", require: false)
21
+ gem("smart_properties", require: false)
22
+ gem("frozen_record", require: false)
23
+ gem("sprockets", require: false)
24
+ gem("rails", require: false)
25
+ gem("state_machines", require: false)
26
+ gem("activerecord-typedstore", require: false)
27
27
  gem("sqlite3")
28
- gem("identity_cache", "~> 1.0", require: false)
28
+ gem("identity_cache", require: false)
29
29
  gem("cityhash", git: "https://github.com/csfrancis/cityhash.git",
30
30
  ref: "3cfc7d01f333c01811d5e834f1495eaa29f87c36", require: false)
31
- gem("activemodel-serializers-xml", "~> 1.0", require: false)
32
- gem("activeresource", "~> 5.1", require: false)
33
- gem("google-protobuf", "~> 3.12.0", require: false)
34
- # Fix version to 0.14.1 since it is the last version to support Ruby 2.4
35
- gem("shopify-money", "= 0.14.1", require: false)
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
31
+ gem("activeresource", require: false)
32
+ gem("google-protobuf", require: false)
33
+ gem("shopify-money", require: false)
34
+ gem("sidekiq", require: false)
35
+ gem("nokogiri", require: false)
36
+ gem("config", require: false)
37
+ gem("aasm", require: false)
38
38
  end
data/README.md CHANGED
@@ -117,8 +117,8 @@ This will generate DSL RBIs for specified constants (or for all handled constant
117
117
 
118
118
  ## Contributing
119
119
 
120
- Bug reports and pull requests are welcome on GitHub at https://github.com/Shopify/tapioca. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](https://github.com/Shopify/tapioca/blob/master/CODE_OF_CONDUCT.md) code of conduct.
120
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Shopify/tapioca. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](https://github.com/Shopify/tapioca/blob/main/CODE_OF_CONDUCT.md) code of conduct.
121
121
 
122
122
  ## License
123
123
 
124
- The gem is available as open source under the terms of the [MIT License](https://github.com/Shopify/tapioca/blob/master/LICENSE.txt).
124
+ The gem is available as open source under the terms of the [MIT License](https://github.com/Shopify/tapioca/blob/main/LICENSE.txt).
data/Rakefile CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "bundler/gem_tasks"
4
4
  require "rake/testtask"
5
- Dir['tasks/**/*.rake'].each { |t| load t }
5
+ Dir["tasks/**/*.rake"].each { |t| load t }
6
6
 
7
7
  Rake.application.options.trace = false
8
8
 
@@ -10,15 +10,13 @@ Rake::TestTask.new do |t|
10
10
  t.libs << "lib"
11
11
  t.libs << "spec"
12
12
  t.warning = false
13
- t.test_files = FileList['spec/**/*_spec.rb']
13
+ t.test_files = FileList["spec/**/*_spec.rb"]
14
14
  end
15
15
 
16
16
  task(:spec) do
17
- begin
18
- Rake::Task[:test].execute
19
- rescue RuntimeError
20
- exit(1)
21
- end
17
+ Rake::Task[:test].execute
18
+ rescue RuntimeError
19
+ exit(1)
22
20
  end
23
21
 
24
22
  task(default: :spec)
data/exe/tapioca CHANGED
@@ -1,7 +1,7 @@
1
1
  #! /usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'sorbet-runtime'
4
+ require "sorbet-runtime"
5
5
 
6
6
  begin
7
7
  T::Configuration.default_checked_level = :never
@@ -20,4 +20,4 @@ end
20
20
 
21
21
  require_relative "../lib/tapioca/internal"
22
22
 
23
- Tapioca::Cli::Main.start(ARGV)
23
+ Tapioca::Cli.start(ARGV)
data/lib/tapioca/cli.rb CHANGED
@@ -1,8 +1,262 @@
1
1
  # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'thor'
4
+ require "thor"
5
5
 
6
6
  module Tapioca
7
- module Cli; end
7
+ class Cli < Thor
8
+ include(Thor::Actions)
9
+
10
+ class_option :outdir,
11
+ aliases: ["--out", "-o"],
12
+ banner: "directory",
13
+ desc: "The output directory for generated RBI files"
14
+ class_option :generate_command,
15
+ aliases: ["--cmd", "-c"],
16
+ banner: "command",
17
+ desc: "The command to run to regenerate RBI files"
18
+ class_option :file_header,
19
+ type: :boolean,
20
+ default: true,
21
+ desc: "Add a \"This file is generated\" header on top of each generated RBI file"
22
+ class_option :verbose,
23
+ aliases: ["-V"],
24
+ type: :boolean,
25
+ default: false,
26
+ desc: "Verbose output for debugging purposes"
27
+
28
+ map T.unsafe(["--version", "-v"] => :__print_version)
29
+
30
+ desc "init", "initializes folder structure"
31
+ def init
32
+ create_config
33
+ create_post_require
34
+ generate_binstub
35
+ end
36
+
37
+ desc "require", "generate the list of files to be required by tapioca"
38
+ def require
39
+ Tapioca.silence_warnings do
40
+ generator.build_requires
41
+ end
42
+ end
43
+
44
+ desc "todo", "generate the list of unresolved constants"
45
+ def todo
46
+ Tapioca.silence_warnings do
47
+ generator.build_todos
48
+ end
49
+ end
50
+
51
+ desc "dsl [constant...]", "generate RBIs for dynamic methods"
52
+ option :generators,
53
+ type: :array,
54
+ aliases: ["--gen", "-g"],
55
+ banner: "generator [generator ...]",
56
+ desc: "Only run supplied DSL generators"
57
+ option :exclude_generators,
58
+ type: :array,
59
+ banner: "generator [generator ...]",
60
+ desc: "Exclude supplied DSL generators"
61
+ option :verify,
62
+ type: :boolean,
63
+ default: false,
64
+ desc: "Verifies RBIs are up-to-date"
65
+ option :quiet,
66
+ aliases: ["-q"],
67
+ type: :boolean,
68
+ desc: "Supresses file creation output"
69
+ def dsl(*constants)
70
+ Tapioca.silence_warnings do
71
+ generator.build_dsl(
72
+ constants,
73
+ should_verify: options[:verify],
74
+ quiet: options[:quiet],
75
+ verbose: options[:verbose]
76
+ )
77
+ end
78
+ end
79
+
80
+ desc "gem [gem...]", "generate RBIs from gems"
81
+ option :all,
82
+ type: :boolean,
83
+ default: false,
84
+ desc: "Regenerate RBI files for all gems"
85
+ option :prerequire,
86
+ aliases: ["--pre", "-b"],
87
+ banner: "file",
88
+ desc: "A file to be required before Bundler.require is called"
89
+ option :postrequire,
90
+ aliases: ["--post", "-a"],
91
+ banner: "file",
92
+ desc: "A file to be required after Bundler.require is called"
93
+ option :exclude,
94
+ aliases: ["-x"],
95
+ type: :array,
96
+ banner: "gem [gem ...]",
97
+ desc: "Excludes the given gem(s) from RBI generation"
98
+ option :typed_overrides,
99
+ aliases: ["--typed", "-t"],
100
+ type: :hash,
101
+ banner: "gem:level [gem:level ...]",
102
+ desc: "Overrides for typed sigils for generated gem RBIs"
103
+ option :verify,
104
+ type: :boolean,
105
+ default: false,
106
+ desc: "Verifies RBIs are up-to-date"
107
+ def gem(*gems)
108
+ Tapioca.silence_warnings do
109
+ all = options[:all]
110
+ verify = options[:verify]
111
+
112
+ raise MalformattedArgumentError, "Options '--all' and '--verify' are mutually exclusive" if all && verify
113
+
114
+ unless gems.empty?
115
+ raise MalformattedArgumentError, "Option '--all' must be provided without any other arguments" if all
116
+ raise MalformattedArgumentError, "Option '--verify' must be provided without any other arguments" if verify
117
+ end
118
+
119
+ if gems.empty? && !all
120
+ generator.sync_rbis_with_gemfile(should_verify: verify)
121
+ else
122
+ generator.build_gem_rbis(all ? [] : gems)
123
+ end
124
+ end
125
+ end
126
+
127
+ desc "generate [gem...]", "DEPRECATED: generate RBIs from gems"
128
+ option :prerequire,
129
+ aliases: ["--pre", "-b"],
130
+ banner: "file",
131
+ desc: "A file to be required before Bundler.require is called"
132
+ option :postrequire,
133
+ aliases: ["--post", "-a"],
134
+ banner: "file",
135
+ desc: "A file to be required after Bundler.require is called"
136
+ option :exclude,
137
+ aliases: ["-x"],
138
+ type: :array,
139
+ banner: "gem [gem ...]",
140
+ desc: "Excludes the given gem(s) from RBI generation"
141
+ option :typed_overrides,
142
+ aliases: ["--typed", "-t"],
143
+ type: :hash,
144
+ banner: "gem:level [gem:level ...]",
145
+ desc: "Overrides for typed sigils for generated gem RBIs"
146
+ def generate(*gems)
147
+ gem_names = if gems.empty?
148
+ "--all"
149
+ else
150
+ gems.join(" ")
151
+ end
152
+ deprecation_message = <<~MSG
153
+ DEPRECATION: The `generate` command will be removed in a future release.
154
+
155
+ Start using `bin/tapioca gem #{gem_names}` instead.
156
+ MSG
157
+
158
+ say(deprecation_message, :red)
159
+ say("")
160
+
161
+ Tapioca.silence_warnings do
162
+ generator.build_gem_rbis(gems)
163
+ end
164
+
165
+ say("")
166
+ say(deprecation_message, :red)
167
+ end
168
+
169
+ desc "sync", "DEPRECATED: sync RBIs to Gemfile"
170
+ option :prerequire,
171
+ aliases: ["--pre", "-b"],
172
+ banner: "file",
173
+ desc: "A file to be required before Bundler.require is called"
174
+ option :postrequire,
175
+ aliases: ["--post", "-a"],
176
+ banner: "file",
177
+ desc: "A file to be required after Bundler.require is called"
178
+ option :exclude,
179
+ aliases: ["-x"],
180
+ type: :array,
181
+ banner: "gem [gem ...]",
182
+ desc: "Excludes the given gem(s) from RBI generation"
183
+ option :typed_overrides,
184
+ aliases: ["--typed", "-t"],
185
+ type: :hash,
186
+ banner: "gem:level [gem:level ...]",
187
+ desc: "Overrides for typed sigils for generated gem RBIs"
188
+ option :verify,
189
+ type: :boolean,
190
+ default: false,
191
+ desc: "Verifies RBIs are up-to-date"
192
+ def sync
193
+ deprecation_message = <<~MSG
194
+ DEPRECATION: The `sync` command will be removed in a future release.
195
+
196
+ Start using `bin/tapioca gem` instead.
197
+ MSG
198
+
199
+ say(deprecation_message, :red)
200
+ say("")
201
+
202
+ Tapioca.silence_warnings do
203
+ generator.sync_rbis_with_gemfile(should_verify: options[:verify])
204
+ end
205
+
206
+ say("")
207
+ say(deprecation_message, :red)
208
+ end
209
+
210
+ desc "--version, -v", "show version"
211
+ def __print_version
212
+ puts "Tapioca v#{Tapioca::VERSION}"
213
+ end
214
+
215
+ private
216
+
217
+ def create_config
218
+ create_file(Config::SORBET_CONFIG, skip: true) do
219
+ <<~CONTENT
220
+ --dir
221
+ .
222
+ CONTENT
223
+ end
224
+ end
225
+
226
+ def create_post_require
227
+ create_file(Config::DEFAULT_POSTREQUIRE, skip: true) do
228
+ <<~CONTENT
229
+ # typed: true
230
+ # frozen_string_literal: true
231
+
232
+ # Add your extra requires here (`tapioca require` can be used to boostrap this list)
233
+ CONTENT
234
+ end
235
+ end
236
+
237
+ def generate_binstub
238
+ bin_stub_exists = File.exist?("bin/tapioca")
239
+ installer = Bundler::Installer.new(Bundler.root, Bundler.definition)
240
+ spec = Bundler.definition.specs.find { |s| s.name == "tapioca" }
241
+ installer.generate_bundler_executable_stubs(spec, { force: true })
242
+ if bin_stub_exists
243
+ shell.say_status(:force, "bin/tapioca", :yellow)
244
+ else
245
+ shell.say_status(:create, "bin/tapioca", :green)
246
+ end
247
+ end
248
+
249
+ no_commands do
250
+ def self.exit_on_failure?
251
+ true
252
+ end
253
+
254
+ def generator
255
+ current_command = T.must(current_command_chain.first)
256
+ @generator ||= Generator.new(
257
+ ConfigBuilder.from_options(current_command, options)
258
+ )
259
+ end
260
+ end
261
+ end
8
262
  end
@@ -0,0 +1,122 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ begin
5
+ require "active_record"
6
+ require "aasm"
7
+ rescue LoadError
8
+ return
9
+ end
10
+
11
+ module Tapioca
12
+ module Compilers
13
+ module Dsl
14
+ # `Tapioca::Compilers::Dsl::AASM` generate types for AASM state machines.
15
+ # This gem dynamically defines constants and methods at runtime. For
16
+ # example, given a class:
17
+ #
18
+ # class MyClass
19
+ # include AASM
20
+ #
21
+ # aasm do
22
+ # state :sleeping, initial: true
23
+ # state :running, :cleaning
24
+ #
25
+ # event :run do
26
+ # transitions from: :sleeping, to: :running
27
+ # end
28
+ # end
29
+ # end
30
+ #
31
+ # This will result in the following constants being defined:
32
+ #
33
+ # STATE_SLEEPING, STATE_RUNNING, STATE_CLEANING
34
+ #
35
+ # and the following methods being defined:
36
+ #
37
+ # sleeping?, running?, cleaning?
38
+ # run, run!, run_without_validation!, may_run?
39
+ #
40
+ class AASM < Tapioca::Compilers::Dsl::Base
41
+ extend T::Sig
42
+
43
+ # Taken directly from the AASM::Core::Event class, here:
44
+ # https://github.com/aasm/aasm/blob/0e03746/lib/aasm/core/event.rb#L21-L29
45
+ EVENT_CALLBACKS =
46
+ T.let(
47
+ ["after", "after_commit", "after_transaction", "before", "before_transaction", "ensure", "error",
48
+ "before_success", "success"].freeze,
49
+ T::Array[String]
50
+ )
51
+
52
+ sig { override.params(root: RBI::Tree, constant: T.all(::AASM::ClassMethods, Class)).void }
53
+ def decorate(root, constant)
54
+ aasm = constant.aasm
55
+ return if !aasm || aasm.states.empty?
56
+
57
+ root.create_path(constant) do |model|
58
+ # Create all of the constants and methods for each state
59
+ aasm.states.each do |state|
60
+ model.create_constant("STATE_#{state.name.upcase}", value: "T.let(T.unsafe(nil), Symbol)")
61
+ model.create_method("#{state.name}?", return_type: "T::Boolean")
62
+ end
63
+
64
+ # Create all of the methods for each event
65
+ parameters = [create_rest_param("opts", type: "T.untyped")]
66
+ aasm.events.each do |event|
67
+ model.create_method(event.name.to_s, parameters: parameters)
68
+ model.create_method("#{event.name}!", parameters: parameters)
69
+ model.create_method("#{event.name}_without_validation!", parameters: parameters)
70
+ model.create_method("may_#{event.name}?", return_type: "T::Boolean")
71
+ end
72
+
73
+ # Create the overall state machine method, which will return an
74
+ # instance of the PrivateAASMMachine class.
75
+ model.create_method(
76
+ "aasm",
77
+ parameters: [
78
+ create_rest_param("args", type: "T.untyped"),
79
+ create_block_param("block", type: "T.nilable(T.proc.bind(PrivateAASMMachine).void)"),
80
+ ],
81
+ return_type: "PrivateAASMMachine",
82
+ class_method: true
83
+ )
84
+
85
+ # Create a private machine class that we can pass around for the
86
+ # purpose of binding various procs passed to methods without having
87
+ # to explicitly bind self in each one.
88
+ model.create_class("PrivateAASMMachine", superclass_name: "AASM::Base") do |machine|
89
+ machine.create_method(
90
+ "event",
91
+ parameters: [
92
+ create_param("name", type: "T.untyped"),
93
+ create_opt_param("options", default: "nil", type: "T.untyped"),
94
+ create_block_param("block", type: "T.proc.bind(PrivateAASMEvent).void"),
95
+ ]
96
+ )
97
+
98
+ # Create a private event class that we can pass around for the
99
+ # purpose of binding all of the callbacks without having to
100
+ # explicitly bind self in each one.
101
+ machine.create_class("PrivateAASMEvent", superclass_name: "AASM::Core::Event") do |event|
102
+ EVENT_CALLBACKS.each do |method|
103
+ event.create_method(
104
+ method,
105
+ parameters: [
106
+ create_block_param("block", type: "T.proc.bind(#{constant.name}).void"),
107
+ ]
108
+ )
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ sig { override.returns(T::Enumerable[Module]) }
116
+ def gather_constants
117
+ T.cast(ObjectSpace.each_object(::AASM::ClassMethods), T::Enumerable[Module])
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end