rubocop-schema-gen 0.1.0 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +13 -0
  3. data/README.md +20 -25
  4. data/assets/templates/cop_schema.yml +1 -3
  5. data/assets/templates/schema.yml +4 -0
  6. data/exe/rubocop-schema-gen +4 -2
  7. data/lib/rubocop/schema.rb +0 -4
  8. data/lib/rubocop/schema/ascii_doc/base.rb +52 -0
  9. data/lib/rubocop/schema/ascii_doc/cop.rb +93 -0
  10. data/lib/rubocop/schema/ascii_doc/department.rb +21 -0
  11. data/lib/rubocop/schema/ascii_doc/index.rb +20 -0
  12. data/lib/rubocop/schema/ascii_doc/stringifier.rb +49 -0
  13. data/lib/rubocop/schema/{cache.rb → cached_http_client.rb} +9 -12
  14. data/lib/rubocop/schema/cli.rb +94 -26
  15. data/lib/rubocop/schema/cop_info_merger.rb +54 -0
  16. data/lib/rubocop/schema/cop_schema.rb +69 -26
  17. data/lib/rubocop/schema/defaults_ripper.rb +48 -0
  18. data/lib/rubocop/schema/diff.rb +67 -0
  19. data/lib/rubocop/schema/document_loader.rb +55 -0
  20. data/lib/rubocop/schema/extension_spec.rb +67 -0
  21. data/lib/rubocop/schema/generator.rb +91 -0
  22. data/lib/rubocop/schema/helpers.rb +62 -0
  23. data/lib/rubocop/schema/repo.rb +91 -0
  24. data/lib/rubocop/schema/value_objects.rb +29 -3
  25. data/lib/rubocop/schema/version.rb +1 -1
  26. metadata +20 -36
  27. data/.gitignore +0 -19
  28. data/.rspec +0 -3
  29. data/.rubocop.yml +0 -38
  30. data/.ruby-version +0 -1
  31. data/.travis.yml +0 -6
  32. data/CODE_OF_CONDUCT.md +0 -74
  33. data/Gemfile +0 -10
  34. data/LICENSE.txt +0 -21
  35. data/Rakefile +0 -6
  36. data/bin/console +0 -14
  37. data/bin/setup +0 -8
  38. data/lib/rubocop/schema/lockfile_inspector.rb +0 -51
  39. data/lib/rubocop/schema/scraper.rb +0 -183
  40. data/lib/rubocop/schema/templates.rb +0 -8
  41. data/rubocop-schema.gemspec +0 -31
  42. data/rubocop-schema.json +0 -21110
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9896f77d8a3c4c39bd31f21878bffdb2d8979dee897b5fe38b3ef090bd11043d
4
- data.tar.gz: bfa9fe9b715b977f1dc973f48e80eafe59a448545bbef559247055bd98cfc5bd
3
+ metadata.gz: 99db5861b36cfdf148861814554c532213934e810770b8ed61f570b81a51ea1c
4
+ data.tar.gz: f7a310fbab5d4889cf24d8e138ca3acacf1fe66ab823699561d60b6db4320b02
5
5
  SHA512:
6
- metadata.gz: 074f101c7a87e0ce7e1de730a82b6bc83682bf0068d60519c1b042ca6bbcc7d3ca574e5ed47f3fec0f2cb63ce3639b98ab2915b90644b1e349aa4c589c3d6cab
7
- data.tar.gz: aeafc3e4661bed171c7d182ffbf6c752ded81d002623984bd0e146072c70fca9609d79410a8f5c23fe031c49caf15755d8ad5a3fe120955e7f23b9ddbd813398
6
+ metadata.gz: 6b646939f86c4db226daf43887e9039fb487143d8ceea498124d9b84f0297a0815558494982531edab03a2754e2a26c8f6c6f06e04f6d3a406f63f3ac7d526e9
7
+ data.tar.gz: 42c0d2d6769be3de1ec6aec0dbb8efa8a609bfd59f82615587757a8bcbe5b79cad6e7ccbda6186cbe0f6ae59b4dc4fb84c106182b58ea05dcecabf8b059dc660
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2021 Neil E. Pearson
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
data/README.md CHANGED
@@ -1,44 +1,39 @@
1
- # Rubocop::Schema
1
+ # RuboCop Config Schema Generator
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/rubocop/schema`. To experiment with that code, run `bin/console` for an interactive prompt.
4
-
5
- TODO: Delete this and the text above, and describe your gem
3
+ This gem generates a JSON schema for your RuboCop configuration files, which you can use in your IDE (e.g. RubyMine) for autocompletion and validation.
6
4
 
7
5
  ## Installation
8
6
 
9
- Add this line to your application's Gemfile:
10
-
11
- ```ruby
12
- gem 'rubocop-schema'
13
- ```
14
-
15
- And then execute:
7
+ $ gem install rubocop-schema-gem
16
8
 
17
- $ bundle install
9
+ ## Usage
18
10
 
19
- Or install it yourself as:
11
+ Change to a directory containing a `Gemfile.lock`, which the generator will use to target your version of `rubocop`, and any extensions you may be using (e.g. `rubocop-rails`).
20
12
 
21
- $ gem install rubocop-schema
13
+ ```
14
+ $ cd ./my_project
15
+ $ rubocop-schema-gen
16
+ Generating rubocop-1.13.1-config-schema.json … complete in 5.2s
17
+ ```
22
18
 
23
- ## Usage
19
+ The name of the generated file is based on your gem version(s). You can override it with an argument.
24
20
 
25
- TODO: Write usage instructions here
21
+ ```
22
+ $ rubocop-schema-gen rubocop-schema.json
23
+ Generating rubocop-schema.json … complete in 0.7s
24
+ ```
26
25
 
27
- ## Development
26
+ Pass `-` to write to standard output.
28
27
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
28
+ The generator caches pages from https://raw.githubusercontent.com/rubocop in `~/.rubocop-schema-cache`.
30
29
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
30
+ Please refer to your IDE's documentation regarding applying the schema to your `.rubocop.yml` file.
32
31
 
33
32
  ## Contributing
34
33
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/rubocop-schema. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/rubocop-schema/blob/master/CODE_OF_CONDUCT.md).
34
+ Bug reports and pull requests are welcome on GitHub at https://github.com/hx/rubocop-schema. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/hx/rubocop-schema/blob/master/CODE_OF_CONDUCT.md).
36
35
 
37
36
 
38
37
  ## License
39
38
 
40
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
41
-
42
- ## Code of Conduct
43
-
44
- Everyone interacting in the Rubocop::Schema project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/rubocop-schema/blob/master/CODE_OF_CONDUCT.md).
39
+ The gem is available as open source under the terms of the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0).
@@ -1,9 +1,7 @@
1
1
  type: object
2
2
  additionalProperties: true
3
3
 
4
- propertyNames:
5
- type: string
6
- pattern: '^[A-Z][A-Za-z]*$'
4
+ propertyNames: { $ref: '#/definitions/attributeName' }
7
5
 
8
6
  properties:
9
7
  Enabled: { type: boolean }
@@ -41,6 +41,10 @@ definitions:
41
41
  - type: array
42
42
  items: { type: string }
43
43
 
44
+ attributeName:
45
+ type: string
46
+ pattern: '^[A-Z][A-Za-z]*$'
47
+
44
48
  properties:
45
49
  inherit_from: { type: string }
46
50
 
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'bundler/setup'
3
+ Dir.chdir File.expand_path('..', __dir__) do
4
+ require 'bundler/setup'
5
+ end
4
6
  require 'rubocop/schema'
5
7
  require 'rubocop/schema/cli'
6
8
  require 'json'
7
9
 
8
- RuboCop::Schema::CLI.new(Dir.pwd, ENV, ARGV).run
10
+ RuboCop::Schema::CLI.new.run
@@ -1,10 +1,6 @@
1
1
  require 'pathname'
2
- require 'rubocop'
3
2
 
4
3
  require 'rubocop/schema/version'
5
- require 'rubocop/schema/cache'
6
- require 'rubocop/schema/scraper'
7
- require 'rubocop/schema/lockfile_inspector'
8
4
 
9
5
  module RuboCop
10
6
  module Schema
@@ -0,0 +1,52 @@
1
+ require 'rubocop/schema/ascii_doc/stringifier'
2
+ require 'rubocop/schema/helpers'
3
+
4
+ module RuboCop
5
+ module Schema
6
+ module AsciiDoc
7
+ class Base
8
+ include Helpers
9
+
10
+ # @param [Asciidoctor::AbstractBlock] ascii_block
11
+ def initialize(ascii_block)
12
+ @root = ascii_block
13
+ scan
14
+ end
15
+
16
+ protected
17
+
18
+ # @return [Asciidoctor::Document]
19
+ attr_reader :root
20
+
21
+ def scan
22
+ raise NotImplementedError
23
+ end
24
+
25
+ def link_text(str)
26
+ # The Asciidoctor API doesn't provide access to the raw title, or parts of it.
27
+ str[%r{<a\s.+?>(.+?)</a>}, 1]&.then &method(:strip_html)
28
+ end
29
+
30
+ # @param [Asciidoctor::Table] table
31
+ # @return [Array<Hash>] A hash for each row, with table headings as keys
32
+ def table_to_hash(table)
33
+ headings = table.rows.head.first.map(&:text)
34
+ table.rows.body.map do |row|
35
+ headings.each_with_index.map do |heading, i|
36
+ [heading, strip_html(row[i].text)]
37
+ end.to_h
38
+ end
39
+ end
40
+
41
+ def stringify_section(section)
42
+ @stringifier ||= Stringifier.new
43
+ @stringifier.stringify section
44
+ end
45
+
46
+ def presence(str)
47
+ str unless str.strip == ''
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,93 @@
1
+ require 'rubocop/schema/ascii_doc/base'
2
+ require 'rubocop/schema/value_objects'
3
+
4
+ module RuboCop
5
+ module Schema
6
+ module AsciiDoc
7
+ class Cop < Base
8
+ # @return [String]
9
+ attr_reader :name
10
+
11
+ # @return [String]
12
+ attr_reader :description
13
+
14
+ # @return [TrueClass, FalseClass]
15
+ attr_reader :enabled_by_default
16
+
17
+ # @return [TrueClass, FalseClass]
18
+ attr_reader :supports_autocorrect
19
+
20
+ # @return [Array<Attribute>]
21
+ attr_reader :attributes
22
+
23
+ ATTRS = public_instance_methods(false).freeze
24
+
25
+ def to_h
26
+ ATTRS.map { |k| [k, __send__(k)] }.to_h
27
+ end
28
+
29
+ protected
30
+
31
+ def scan
32
+ @name = root.title
33
+ read_stats_table
34
+ read_description
35
+ read_attributes
36
+ end
37
+
38
+ private
39
+
40
+ def read_stats_table
41
+ return unless stats_table
42
+
43
+ @enabled_by_default = stats_table['Enabled by default'] == 'Enabled'
44
+ @supports_autocorrect = stats_table['Supports autocorrection'] == 'Yes'
45
+ end
46
+
47
+ def read_description
48
+ blocks = root.blocks[find_description_range]
49
+ @description = blocks.map(&method(:stringify_section)).join("\n\n") if blocks.any?
50
+ end
51
+
52
+ def read_attributes
53
+ return unless attr_table_block
54
+
55
+ @attributes = table_to_hash(attr_table_block).map do |row|
56
+ Attribute.new(
57
+ name: row['Name'],
58
+ default: presence(row['Default value']),
59
+ type: presence(row['Configurable values'])
60
+ )
61
+ end
62
+ end
63
+
64
+ def find_description_range
65
+ top = stats_table_block ? root.blocks.index(stats_table_block) + 1 : 0
66
+ bottom = root.blocks.index(root.sections.first) || 0
67
+ top..(bottom - 1)
68
+ end
69
+
70
+ # @return [Asciidoctor::Block, nil]
71
+ def stats_table_block
72
+ @stats_table_block ||= root
73
+ .query(context: :table) { |t| t.rows.head.first.first.text == 'Enabled by default' }
74
+ .first
75
+ end
76
+
77
+ # @return [Asciidoctor::Block, nil]
78
+ def attr_table_block
79
+ @attr_table_block ||= root
80
+ .query(context: :section) { |s| s.title == 'Configurable attributes' }
81
+ &.first
82
+ &.query(context: :table)
83
+ &.first
84
+ end
85
+
86
+ # @return [Array<Hash>, nil]
87
+ def stats_table
88
+ @stats_table ||= stats_table_block && table_to_hash(stats_table_block).first
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,21 @@
1
+ require 'rubocop/schema/ascii_doc/base'
2
+ require 'rubocop/schema/ascii_doc/cop'
3
+
4
+ module RuboCop
5
+ module Schema
6
+ module AsciiDoc
7
+ class Department < Base
8
+ # @return [Array<Cop>]
9
+ attr_reader :cops
10
+
11
+ protected
12
+
13
+ def scan
14
+ @cops = root
15
+ .query(context: :section) { |s| s.title.start_with? "#{root.title}/" }
16
+ .map &Cop.method(:new)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,20 @@
1
+ require 'rubocop/schema/ascii_doc/base'
2
+
3
+ module RuboCop
4
+ module Schema
5
+ module AsciiDoc
6
+ class Index < Base
7
+ # @return [Array<string>]
8
+ attr_reader :department_names
9
+
10
+ protected
11
+
12
+ def scan
13
+ @department_names = root
14
+ .query(context: :section) { |s| s.title.start_with? 'Department ' }
15
+ .map { |section| link_text section.title }
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,49 @@
1
+ require 'rubocop/schema/helpers'
2
+
3
+ module RuboCop
4
+ module Schema
5
+ module AsciiDoc
6
+ class Stringifier
7
+ include Helpers
8
+
9
+ # @param [Asciidoctor::Section] section
10
+ def stringify(section)
11
+ method = :"stringify_#{section.context}"
12
+ raise "Don't know what to do with #{section.context}" unless private_methods(false).include? method
13
+
14
+ __send__(method, section)
15
+ end
16
+
17
+ private
18
+
19
+ # @param [Asciidoctor::Section] section
20
+ def stringify_paragraph(section)
21
+ section.lines.join ' '
22
+ end
23
+
24
+ alias stringify_admonition stringify_paragraph
25
+ alias stringify_listing stringify_paragraph
26
+
27
+ # @param [Asciidoctor::Section] section
28
+ def stringify_literal(section)
29
+ section.lines.map { |l| " #{l}" }.join "\n"
30
+ end
31
+
32
+ # @param [Asciidoctor::Section] section
33
+ def stringify_ulist(section)
34
+ section.blocks.map { |b| " - #{strip_html b.text}" }.join "\n\n" # TODO: single newline
35
+ end
36
+
37
+ # @param [Asciidoctor::Section] section
38
+ def stringify_olist(section)
39
+ section.blocks.map.with_index { |b, i| " #{i + 1}. #{strip_html b.text}" }.join "\n\n" # TODO: single newline
40
+ end
41
+
42
+ # @param [Asciidoctor::Section] section
43
+ def stringify_dlist(section)
44
+ strip_html section.convert # Too hard, just go HTML for now
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -1,30 +1,29 @@
1
1
  require 'pathname'
2
2
  require 'uri'
3
- require 'net/http'
3
+
4
+ require 'rubocop/schema/helpers'
4
5
 
5
6
  module RuboCop
6
7
  module Schema
7
- class Cache
8
- # @return [URI]
9
- attr_reader :base_url
8
+ class CachedHTTPClient
9
+ include Helpers
10
10
 
11
- def initialize(cache_dir, base_url: nil, &event_handler)
11
+ def initialize(cache_dir, &event_handler)
12
12
  @cache_dir = Pathname(cache_dir)
13
- @base_url = validate_url(base_url)
14
13
  @event_handler = event_handler
15
14
  end
16
15
 
17
16
  def get(url)
18
17
  url = URI(url)
19
- url = @base_url + url if @base_url && url.relative?
20
18
  validate_url url
21
19
 
22
20
  path = path_for_url(url)
23
21
  return path.read if path.readable?
24
22
 
25
23
  path.parent.mkpath
26
- @event_handler&.call Event.new(type: :request)
27
- Net::HTTP.get(url).force_encoding(Encoding::UTF_8).tap(&path.method(:write))
24
+ Event.dispatch type: :request, &@event_handler
25
+
26
+ http_get(url).tap(&path.method(:write))
28
27
  end
29
28
 
30
29
  private
@@ -34,13 +33,11 @@ module RuboCop
34
33
 
35
34
  raise ArgumentError, 'Expected an absolute URL' unless url.absolute?
36
35
  raise ArgumentError, 'Expected an HTTP URL' unless url.is_a? URI::HTTP
37
-
38
- url
39
36
  end
40
37
 
41
38
  # @param [URI::HTTP] url
42
39
  def path_for_url(url)
43
- @cache_dir + url.scheme + url.hostname + url.path[1..]
40
+ @cache_dir + url.scheme + url.hostname + url.path[1..-1]
44
41
  end
45
42
  end
46
43
  end
@@ -1,67 +1,135 @@
1
1
  require 'pathname'
2
2
  require 'json'
3
3
 
4
+ require 'rubocop/schema/document_loader'
5
+ require 'rubocop/schema/cached_http_client'
6
+ require 'rubocop/schema/generator'
7
+ require 'rubocop/schema/extension_spec'
8
+ require 'rubocop/schema/repo'
9
+
4
10
  module RuboCop
5
11
  module Schema
6
12
  class CLI
7
13
  # @param [Pathname] working_dir
8
14
  # @param [Hash] env
9
15
  # @param [Array<String>] args
10
- def initialize(working_dir, env, args)
16
+ # @param [String] home
17
+ # @param [IO] out_file
18
+ # @param [IO] log_file
19
+ def initialize(working_dir: Dir.pwd, env: ENV, args: ARGV, home: Dir.home, out_file: nil, log_file: $stderr)
11
20
  @working_dir = Pathname(working_dir)
21
+ @home_dir = Pathname(home)
12
22
  @env = env
13
23
  @args = args
24
+ @out_file = out_file
25
+ @log_file = log_file
26
+
27
+ raise ArgumentError, 'Cannot accept an out_file and an argument' if @out_file && args.first
14
28
  end
15
29
 
16
30
  def run
17
- fail "Cannot read #{lockfile_path}" unless lockfile_path.readable?
18
- fail 'RuboCop is not part of this project' unless lockfile.specs.any?
31
+ read_flag while @args.first&.start_with?('--')
32
+ assign_outfile
19
33
 
20
- schema = report_duration { Scraper.new(lockfile, cache).schema }
21
- puts JSON.pretty_generate schema
34
+ print "Generating #{@out_path} " if @out_path
35
+
36
+ schema = report_duration(lowercase: @out_path) { Generator.new(spec.specs, document_loader).schema }
37
+ @out_file.puts JSON.pretty_generate schema
22
38
  end
23
39
 
24
40
  private
25
41
 
26
- def report_duration
27
- started = Time.now
42
+ def read_flag
43
+ case @args.shift
44
+ when '--version'
45
+ info VERSION
46
+ when '--spec'
47
+ info spec
48
+ when /\A--spec=(\S+)/
49
+ @spec = ExtensionSpec.from_string($1)
50
+ when /\A--build-repo=(.+)/
51
+ build_repo $1
52
+ end
53
+ end
54
+
55
+ def build_repo(dir)
56
+ Repo.new(dir, document_loader, &method(:handle_event)).build
57
+ exit
58
+ end
59
+
60
+ def spec
61
+ @spec ||=
62
+ begin
63
+ lockfile_path = @working_dir + 'Gemfile.lock'
64
+ fail "Cannot read #{lockfile_path}" unless lockfile_path.readable?
65
+
66
+ spec = ExtensionSpec.from_lockfile(lockfile_path)
67
+ fail 'RuboCop is not part of this project' if spec.empty?
68
+
69
+ spec
70
+ end
71
+ end
72
+
73
+ def assign_outfile
74
+ return if @out_file
75
+
76
+ @out_path = path_from_arg(@args.first)
77
+
78
+ @out_file ||= File.open(@out_path, 'w') # rubocop:disable Naming/MemoizedInstanceVariableName
79
+ end
80
+
81
+ def path_from_arg(arg)
82
+ case arg
83
+ when '-'
84
+ @out_file = $stdout
85
+ nil
86
+ when nil
87
+ "#{spec}-config-schema.json"
88
+ else
89
+ arg
90
+ end
91
+ end
92
+
93
+ def report_duration(lowercase: false)
94
+ started = Time.now
28
95
  yield
29
96
  ensure
30
97
  finished = Time.now
31
- handle_event Event.new(message: "Complete in #{(finished - started).round 1}s")
98
+ message = "Complete in #{(finished - started).round 1}s"
99
+ message.downcase! if lowercase
100
+ handle_event Event.new(message: message)
32
101
  end
33
102
 
34
103
  def handle_event(event)
35
104
  case event.type
36
105
  when :request
37
- $stderr << '.'
106
+ @log_file << '.'
38
107
  @line_dirty = true
39
108
  else
40
- $stderr.puts '' if @line_dirty
109
+ @log_file.puts '' if @line_dirty
41
110
  @line_dirty = false
42
- $stderr.puts event.message.to_s
111
+ @log_file.puts event.message.to_s
43
112
  end
44
113
  end
45
114
 
46
- def fail(msg)
47
- $stderr.puts msg.to_s
48
- exit 1
49
- end
50
-
51
- def lockfile
52
- @lockfile ||= LockfileInspector.new(lockfile_path)
115
+ def info(msg)
116
+ $stdout.puts msg
117
+ exit
53
118
  end
54
119
 
55
- def lockfile_path
56
- @lockfile_path ||= @working_dir + 'Gemfile.lock'
57
- end
58
-
59
- def cache
60
- @cache ||= Cache.new(cache_dir, &method(:handle_event))
120
+ def fail(msg)
121
+ @log_file.puts msg.to_s
122
+ exit 1
61
123
  end
62
124
 
63
- def cache_dir
64
- @cache_dir ||= Pathname(Dir.home) + '.rubocop-schema-cache'
125
+ def document_loader
126
+ @document_loader ||=
127
+ DocumentLoader.new(
128
+ CachedHTTPClient.new(
129
+ @home_dir + '.rubocop-schema-cache',
130
+ &method(:handle_event)
131
+ )
132
+ )
65
133
  end
66
134
  end
67
135
  end