rujira 0.1.14 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 77641c9b95c2b8777a5e95202deb2bf38ab8d41e1d2e339f95ab5f2c1243bce8
4
- data.tar.gz: 759fb9ee8e937322f10c266fb88ea71abe7f8f59a66396e18092ef378bcc37af
3
+ metadata.gz: '099900e906a1d71b9c787fa6565fa02bdce269dba4ab74394f6c222b10f3a737'
4
+ data.tar.gz: f00ae30d02ca63aecca57b1ea8099fa0afe2482de3baa385f92e7d29968ae83d
5
5
  SHA512:
6
- metadata.gz: 901f27c13e274d717e52dda2218b9bf70c3432a7a2077f0e70f3203673c4218280e875efb96d50220e4d7e2e07ac81cf2fee2645f3210cc0ed54a911d935fe99
7
- data.tar.gz: 9cee06af219613b52e24337ad2441a2f10b880e351d2d60075a2aef5b14fd2460813541086047c94db6381588764b6cd7a6a10676c35d48000d85bd9443ea163
6
+ metadata.gz: 581f6a09da83eadd7ef5d474face9d61e34b91999c3009094c7be6a83872eb18a0c2a50fc1462b910b36b289b8e1277906744f738aae6c7f179a6cc0a3193e01
7
+ data.tar.gz: 82d46f9a3a7d56a605d69cb5263c5b3be64fda4973920766de48cbe5b12a7e8b22d96b740497faa3edccbc4848a3d910ee1f66b897bcfd95927993b68ec45d5c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,83 @@
1
- ## [Unreleased]
1
+ ## [0.3.1] - 2025-09-13
2
2
 
3
- ## [0.1.0] - 2024-04-10
3
+ ### 🚜 Refactor
4
4
 
5
- - Initial release
5
+ - Fix for headers setter
6
+ - Cleanup data method
7
+ - Abort if id is nil #2
8
+ - Abort if id is nil
9
+
10
+ ### 📚 Documentation
11
+
12
+ - Use ENV for URL getting
13
+ - Added some docs
14
+ ## [0.3.0] - 2025-09-13
15
+
16
+ ### 🚀 Features
17
+
18
+ - Added client supports
19
+
20
+ ### 🚜 Refactor
21
+
22
+ - Updates tasks
23
+ - Use params
24
+ - Added examples
25
+
26
+ ### Reafactor
27
+
28
+ - How to search sprints
29
+ ## [0.2.0] - 2025-09-11
30
+
31
+ ### 🚀 Features
32
+
33
+ - Added new method for API
34
+
35
+ ### 📚 Documentation
36
+
37
+ - Update README
38
+ ## [0.1.15] - 2025-09-11
39
+
40
+ ### 🚀 Features
41
+
42
+ - Added dotenv
43
+ - Update a gems
44
+ - Example of generation url
45
+
46
+ ### 🐛 Bug Fixes
47
+
48
+ - No test in CI
49
+
50
+ ### 🚜 Refactor
51
+
52
+ - Docs
53
+ - BUILDER
54
+ - RAKE TASKS
55
+
56
+ ### 📚 Documentation
57
+
58
+ - Fix a readme
59
+
60
+ ### Hotfix
61
+
62
+ - Remove debug line
63
+
64
+ ### ITMG
65
+
66
+ - Work with missing methods
67
+ - Rewrite tasks for gem
68
+ - Added project create entrypoint
69
+
70
+ ### Reafactor
71
+
72
+ - Full
73
+ ## [0.1.10] - 2024-07-05
74
+
75
+ ### ITMG
76
+
77
+ - Adding watchers and security level getter
78
+ ## [0.1.9] - 2024-06-23
79
+
80
+ ### ITMG
81
+
82
+ - Description fixes
83
+ - Release 0.1.8
data/README.md CHANGED
@@ -1,78 +1,201 @@
1
- # Rujira
2
-
3
- ## Running
4
-
5
- RUJIRA_DEBUG=true RUJIRA_URL=http://localhost:8080 RUJIRA_TOKEN=<JIRA_ACCESS_TOKEN> bundle exec ./bin/console
6
-
7
- ## Usage in the code
8
-
9
- project = random_name
10
- name = Rujira::Api::Myself.get.name
11
- Rujira::Api::Project.create do
12
- data key: project.to_s,
13
- name: project.to_s,
14
- projectTypeKey: 'software',
15
- lead: 'root'
16
- end
17
- Rujira::Api::Project.get project.to_s
18
- Rujira::Api::Issue.create do
19
- data fields: {
20
- project: { key: project.to_s },
21
- summary: 'BOT: added a new feature.',
22
- description: 'This task was generated by the bot when creating changes in the repository.',
23
- issuetype: { name: 'Task' }
24
- }
25
- params updateHistory: true
26
- end
27
- Rujira::Api::Issue.watchers "#{project}-1", name
28
- Rujira::Api::Issue.get "#{project}-1"
29
- result = Rujira::Api::Search.get do
30
- data jql: "project = #{project} and status IN (\"To Do\", \"In Progress\") ORDER BY issuekey",
31
- maxResults: 10,
32
- startAt: 0,
33
- fields: %w[id key]
34
- end
35
- Rujira::Api::Issue.comment "#{project}-1" do
36
- data body: 'Adding a new comment'
37
- end
38
- Rujira::Api::Issue.edit "#{project}-1" do
39
- data update: {
40
- labels: [{ add: 'bot' }, { remove: 'some' }]
41
- },
42
- fields: {
43
- assignee: { name: name },
44
- summary: 'This is a shorthand for a set operation on the summary field'
45
- }
46
- end
47
- result.iter.each do |issue|
48
- Rujira::Api::Issue.del issue.data['id'] do
49
- params deleteSubtasks: true
50
- end
51
- end
52
-
53
- ## Rake tasks
54
-
55
- require 'rujira/tasks/jira'
56
- Rujira::Tasks::Jira.new
57
-
58
- rake jira:issue:whoami
59
- rake jira:issue:create -- '--project=ITMG' \
60
- '--summary=The short summary information' \
61
- '--description=The base description of task' \
62
- '--issuetype=Task'
63
- rake jira:issue:search -- '-q project = ITMG'
64
- rake jira:issue:attach -- '--file=upload.png' '--issue=ITMG-1'
65
-
66
- ## Testing
67
-
68
- ### Run the instance of jira
69
-
70
- docker compose up -d
71
- open http://localhost:8080
72
-
73
- ### Example with Curl
74
-
75
- curl -H "Authorization: Bearer <JIRA_ACCESS_TOKEN>" 'http://localhost:8080/rest/api/2/search?expand=summary'
76
- curl -vv -X POST --data '"some"' -H "Authorization: Bearer <JIRA_ACCESS_TOKEN>" -H "Content-Type: application/json" 'http://localhost:8080/rest/api/2/issue/ITMG-70/watchers'
77
- curl -D- -F "file=@upload.png" -X POST -H "X-Atlassian-Token: nocheck" \
78
- -H "Authorization: Bearer <JIRA_ACCESS_TOKEN>" 'http://localhost:8080/rest/api/2/issue/ITMG-70/attachments'
1
+ # RUJIRA
2
+
3
+ **RUJIRA** is a Ruby gem for easy interaction with the Jira API. It provides a simple and flexible interface to work with Jira resources and includes Rake tasks for convenient command-line operations.
4
+
5
+ ---
6
+
7
+ ## Features
8
+
9
+ * Easy DSL for building and executing Jira API requests.
10
+ * Built-in Rake tasks for console operations (e.g., creating issues, updating data, exporting).
11
+
12
+ ---
13
+
14
+ ## Installation
15
+
16
+ Add to your `Gemfile`:
17
+
18
+ ```ruby
19
+ gem 'rujira', '~> 0.1.0'
20
+ ```
21
+
22
+ Or install directly:
23
+
24
+ ```bash
25
+ gem install rujira
26
+ ```
27
+
28
+ ---
29
+
30
+ ## Quick Start
31
+
32
+ ### Configuration
33
+
34
+ ```ruby
35
+ cat .env
36
+ RUJIRA_DEBUG=false
37
+ RUJIRA_TOKEN='<TOKEN>'
38
+ RUJIRA_URL='http://localhost:8080'
39
+ ```
40
+
41
+ ### Example of usage
42
+
43
+ ```ruby
44
+ require 'date'
45
+
46
+ now = Date.today
47
+ before = now + 30
48
+
49
+ project = random_name
50
+ url = ENV.fetch('RUJIRA_URL', 'http://localhost:8080')
51
+ client = Rujira::Client.new(url, debug: true)
52
+
53
+ client.ServerInfo.get
54
+ name = client.Myself.get['name']
55
+
56
+ client.Project.create do
57
+ payload key: project.to_s,
58
+ name: project.to_s,
59
+ projectTypeKey: 'software',
60
+ lead: name
61
+ end
62
+ client.Project.get project.to_s
63
+ client.Issue.create do
64
+ payload fields: {
65
+ project: { key: project.to_s },
66
+ summary: 'BOT: added a new feature.',
67
+ description: 'This task was generated by the bot when creating changes in the repository.',
68
+ issuetype: { name: 'Task' }
69
+ }
70
+ params updateHistory: true
71
+ end
72
+
73
+ client.Board.list
74
+ client.Board.get 1
75
+
76
+ sprint = client.Sprint.create do
77
+ payload name: 'Bot Sprint',
78
+ originBoardId: 1,
79
+ goal: 'Finish core features for release 1.0',
80
+ autoStartStop: false
81
+ end
82
+ client.Sprint.issue sprint['id'], ["#{project}-1"]
83
+
84
+ client.Sprint.replace sprint['id'] do
85
+ payload state: 'future',
86
+ name: 'Bot Sprint',
87
+ originBoardId: 1,
88
+ goal: 'Finish core features for release 1.0',
89
+ startDate: now,
90
+ endDate: before,
91
+ autoStartStop: true
92
+ end
93
+
94
+ update = client.Sprint.update sprint['id'] do
95
+ payload name: "Bot Sprint #{project}"
96
+ end
97
+
98
+ assert_equal 'Bot Sprint', sprint['name']
99
+ assert_equal "Bot Sprint #{project}", update['name']
100
+
101
+ issues = client.Sprint.get_issue sprint['id']
102
+
103
+ assert_not_empty issues['issues']
104
+
105
+ client.Issue.get "#{project}-1"
106
+
107
+ client.Issue.watchers "#{project}-1", name
108
+ search = client.Search.get do
109
+ payload jql: "project = #{project} and status IN (\"To Do\", \"In Progress\") ORDER BY issuekey",
110
+ maxResults: 10,
111
+ startAt: 0,
112
+ fields: %w[id key]
113
+ end
114
+ client.Issue.comment "#{project}-1" do
115
+ payload body: 'Adding a new comment'
116
+ end
117
+ client.Issue.edit "#{project}-1" do
118
+ payload update: {
119
+ labels: [{ add: 'bot' }, { remove: 'some' }]
120
+ },
121
+ fields: {
122
+ assignee: { name: name },
123
+ summary: 'This is a shorthand for a set operation on the summary field'
124
+ }
125
+ end
126
+
127
+ sprints = client.Board.sprint 1
128
+ sprints['values'].each do |sprint|
129
+ client.Sprint.delete sprint['id']
130
+ end
131
+ search['issues'].each do |issue|
132
+ client.Issue.delete issue['id'] do
133
+ params deleteSubtasks: true
134
+ end
135
+ end
136
+ client.Project.delete project.to_s
137
+
138
+ client.Dashboard.list
139
+ client.Dashboard.get 10_000
140
+ ```
141
+
142
+ * The `builder` method automatically applies the authorization token.
143
+ * The block allows customization of headers, parameters, and other request options.
144
+
145
+ ---
146
+
147
+ ## Rake Tasks
148
+
149
+ The gem includes Rake tasks for easy use from the command line. Examples:
150
+
151
+ ```bash
152
+ rake jira:board:get # Get a board
153
+ rake jira:board:list # Get list of boards
154
+ rake jira:board:sprint # Get a boards sprint
155
+ rake jira:dashboard:get # Get a dashboard
156
+ rake jira:dashboard:list # Get list of dashboards
157
+ rake jira:issue:attach # Example usage attaching in issue
158
+ rake jira:issue:create # Create a issue
159
+ rake jira:issue:delete # Delete issue
160
+ rake jira:issue:search # Search issue by fields
161
+ rake jira:project:list # Get list of projects
162
+ rake jira:server_info # Test connection by getting server information
163
+ rake jira:sprint:properties:list # Get sprint properties
164
+ rake jira:whoami # Test connection by getting username
165
+ ```
166
+
167
+ ```bash
168
+ rake jira:issue:create PROJECT=ITMG SUMMARY='The short summary information' \
169
+ DESCRIPTION='The base description of task' ISSUETYPE='Task'
170
+ rake jira:issue:search JQL='project = ITMG'
171
+ rake jira:issue:attach FILE='upload.png' ISSUE_ID='ITMG-1'
172
+ ```
173
+
174
+ ## Development runs
175
+
176
+ ```bash
177
+ docker compose up -d
178
+ open http://localhost:8080
179
+
180
+ ```
181
+
182
+ ```bash
183
+ curl -H "Authorization: Bearer <JIRA_ACCESS_TOKEN>" 'http://localhost:8080/rest/api/2/search?expand=summary'
184
+ curl -vv -X POST --data '"some"' -H "Authorization: Bearer <JIRA_ACCESS_TOKEN>" -H "Content-Type: application/json" 'http://localhost:8080/rest/api/2/issue/ITMG-70/watchers'
185
+ curl -D- -F "file=@upload.png" -X POST -H "X-Atlassian-Token: nocheck" \
186
+ -H "Authorization: Bearer <JIRA_ACCESS_TOKEN>" 'http://localhost:8080/rest/api/2/issue/ITMG-70/attachments'
187
+
188
+ ```
189
+
190
+ ---
191
+
192
+ ## Contribution
193
+
194
+ * Pull requests are welcome.
195
+ * For issues or feature requests, please use GitHub Issues.
196
+
197
+ ---
198
+
199
+ ## License
200
+
201
+ MIT License © 2025 iTmageLAB
data/Rakefile CHANGED
@@ -3,11 +3,14 @@
3
3
  require 'bundler/gem_tasks'
4
4
  require 'rake/testtask'
5
5
  require 'dotenv'
6
- Dotenv.load
7
6
 
8
7
  $LOAD_PATH.unshift File.expand_path('lib', __dir__)
9
- require 'rujira'
10
- require 'rujira/tasks/jira'
8
+
9
+ require_relative 'lib/rujira'
10
+
11
+ Rujira::Tasks::Jira.new
12
+
13
+ Dotenv.load
11
14
 
12
15
  task default: %i[test]
13
16
 
@@ -17,8 +20,6 @@ Rake::TestTask.new do |t|
17
20
  t.verbose = false
18
21
  end
19
22
 
20
- Rujira::Tasks::Jira.new
21
-
22
23
  task :version do
23
24
  puts Rujira::VERSION
24
25
  end
data/cliff.toml ADDED
@@ -0,0 +1,94 @@
1
+ # git-cliff ~ configuration file
2
+ # https://git-cliff.org/docs/configuration
3
+
4
+
5
+ [changelog]
6
+ # A Tera template to be rendered for each release in the changelog.
7
+ # See https://keats.github.io/tera/docs/#introduction
8
+ body = """
9
+ {% if version %}\
10
+ ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
11
+ {% else %}\
12
+ ## [unreleased]
13
+ {% endif %}\
14
+ {% for group, commits in commits | group_by(attribute="group") %}
15
+ ### {{ group | striptags | trim | upper_first }}
16
+ {% for commit in commits %}
17
+ - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
18
+ {% if commit.breaking %}[**breaking**] {% endif %}\
19
+ {{ commit.message | upper_first }}\
20
+ {% endfor %}
21
+ {% endfor %}
22
+ """
23
+ # Remove leading and trailing whitespaces from the changelog's body.
24
+ trim = true
25
+ # Render body even when there are no releases to process.
26
+ render_always = true
27
+ # An array of regex based postprocessors to modify the changelog.
28
+ postprocessors = [
29
+ # Replace the placeholder <REPO> with a URL.
30
+ #{ pattern = '<REPO>', replace = "https://github.com/orhun/git-cliff" },
31
+ ]
32
+ # render body even when there are no releases to process
33
+ # render_always = true
34
+ # output file path
35
+ # output = "test.md"
36
+
37
+ [git]
38
+ # Parse commits according to the conventional commits specification.
39
+ # See https://www.conventionalcommits.org
40
+ conventional_commits = true
41
+ # Exclude commits that do not match the conventional commits specification.
42
+ filter_unconventional = true
43
+ # Require all commits to be conventional.
44
+ # Takes precedence over filter_unconventional.
45
+ require_conventional = false
46
+ # Split commits on newlines, treating each line as an individual commit.
47
+ split_commits = false
48
+ # An array of regex based parsers to modify commit messages prior to further processing.
49
+ commit_preprocessors = [
50
+ # Replace issue numbers with link templates to be updated in `changelog.postprocessors`.
51
+ #{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))"},
52
+ # Check spelling of the commit message using https://github.com/crate-ci/typos.
53
+ # If the spelling is incorrect, it will be fixed automatically.
54
+ #{ pattern = '.*', replace_command = 'typos --write-changes -' },
55
+ ]
56
+ # Prevent commits that are breaking from being excluded by commit parsers.
57
+ protect_breaking_commits = false
58
+ # An array of regex based parsers for extracting data from the commit message.
59
+ # Assigns commits to groups.
60
+ # Optionally sets the commit's scope and can decide to exclude commits from further processing.
61
+ commit_parsers = [
62
+ { message = "^feat", group = "<!-- 0 -->🚀 Features" },
63
+ { message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
64
+ { message = "^doc", group = "<!-- 3 -->📚 Documentation" },
65
+ { message = "^perf", group = "<!-- 4 -->⚡ Performance" },
66
+ { message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
67
+ { message = "^style", group = "<!-- 5 -->🎨 Styling" },
68
+ { message = "^test", group = "<!-- 6 -->🧪 Testing" },
69
+ { message = "^chore\\(release\\): prepare for", skip = true },
70
+ { message = "^chore\\(deps.*\\)", skip = true },
71
+ { message = "^chore\\(pr\\)", skip = true },
72
+ { message = "^chore\\(pull\\)", skip = true },
73
+ { message = "^chore", skip = true },
74
+ { message = "^chore|^ci", group = "<!-- 7 -->⚙️ Miscellaneous Tasks" },
75
+ { body = ".*security", group = "<!-- 8 -->🛡️ Security" },
76
+ { message = "^revert", group = "<!-- 9 -->◀️ Revert" },
77
+ ]
78
+ # Exclude commits that are not matched by any commit parser.
79
+ filter_commits = false
80
+ # An array of link parsers for extracting external references, and turning them into URLs, using regex.
81
+ link_parsers = []
82
+ # Include only the tags that belong to the current branch.
83
+ use_branch_tags = false
84
+ # Order releases topologically instead of chronologically.
85
+ topo_order = false
86
+ # Order releases topologically instead of chronologically.
87
+ topo_order_commits = true
88
+ # Order of commits in each group/release within the changelog.
89
+ # Allowed values: newest, oldest
90
+ sort_commits = "newest"
91
+ # Process submodules commits
92
+ recurse_submodules = false
93
+
94
+ limit_commits = 50
data/compose.yaml CHANGED
@@ -2,6 +2,7 @@
2
2
  services:
3
3
  jira:
4
4
  restart: always
5
+ # mem_limit: 1g
5
6
  image: atlassian/jira-software
6
7
  environment: {}
7
8
  # JVM_SUPPORT_RECOMMENDED_ARGS: "-Datlassian.recovery.password=<PASSWORD>"
@@ -12,6 +13,7 @@ services:
12
13
 
13
14
  db:
14
15
  image: postgres
16
+ mem_limit: 128mb
15
17
  restart: always
16
18
  shm_size: 128mb
17
19
  environment:
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dotenv'
4
+ require_relative '../lib/rujira'
5
+
6
+ Dotenv.load
7
+
8
+ client = Rujira::Client.new('http://localhost:8080')
9
+
10
+ project_name = 'EXAMPLE2'
11
+ name = client.Myself.get['name']
12
+
13
+ begin
14
+ project = client.Project.create do
15
+ payload key: project_name.to_s,
16
+ name: project_name.to_s,
17
+ projectTypeKey: 'software',
18
+ lead: name
19
+ end
20
+ rescue StandardError
21
+ projects = client.Project.list
22
+ project = projects.find { |p| p['name'] == project_name }
23
+ end
24
+
25
+ client.Issue.create do
26
+ payload fields: {
27
+ project: { key: project['key'] },
28
+ summary: 'BOT: added a new feature.',
29
+ description: 'This task was generated by the bot when creating changes in the repository.',
30
+ issuetype: { name: 'Task' }
31
+ }
32
+ params updateHistory: true
33
+ end
34
+
35
+ now = Date.today
36
+ before = now + 30
37
+
38
+ sprint = client.Sprint.create do
39
+ payload name: 'Bot Example Sprint',
40
+ originBoardId: 1,
41
+ goal: 'Finish core features for release 1.0',
42
+ startDate: now,
43
+ endDate: before,
44
+ autoStartStop: true
45
+ end
46
+
47
+ client.Sprint.update sprint['id'] do
48
+ payload state: 'active'
49
+ end
50
+
51
+ client.Sprint.issue sprint['id'], ["#{project_name}-1"]
@@ -3,7 +3,20 @@
3
3
  module Rujira
4
4
  module Api
5
5
  # TODO
6
- # https://docs.atlassian.com/software/jira/docs/api/REST/9.17.0/#api/2/comment/%7BcommentId%7D/properties
7
- class Attachments < Common; end
6
+ # https://docs.atlassian.com/software/jira/docs/api/REST/9.17.0/#api/2/issue/%7BissueIdOrKey%7D/attachments
7
+ class Attachments < Common
8
+ def create(id_or_key, path, &block)
9
+ abort 'Issue ID or KEY is required' if id_or_key.to_s.strip.empty?
10
+ client = @client
11
+ builder do
12
+ path "issue/#{id_or_key}/attachments"
13
+ method :post
14
+ headers 'X-Atlassian-Token': 'no-check'
15
+ payload file: client.file(path)
16
+ instance_eval(&block) if block_given?
17
+ end
18
+ run
19
+ end
20
+ end
8
21
  end
9
22
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rujira
4
+ module Api
5
+ # TODO
6
+ # https://docs.atlassian.com/jira-software/REST/9.17.0/#agile/1.0/board
7
+ class Board < Common
8
+ def initialize(client)
9
+ super
10
+ builder do
11
+ rest_base 'rest/agile/1.0'
12
+ end
13
+ end
14
+
15
+ def get(id)
16
+ abort 'Board ID is required' if id.to_s.strip.empty?
17
+ builder do
18
+ path "board/#{id}"
19
+ end
20
+ run
21
+ end
22
+
23
+ def list
24
+ builder do
25
+ path 'board'
26
+ end
27
+ run
28
+ end
29
+
30
+ def sprint(id)
31
+ abort 'Board ID is required' if id.to_s.strip.empty?
32
+ builder do
33
+ path "board/#{id}/sprint"
34
+ end
35
+ run
36
+ end
37
+ end
38
+ end
39
+ end
@@ -4,6 +4,16 @@ module Rujira
4
4
  module Api
5
5
  # TODO
6
6
  # https://docs.atlassian.com/software/jira/docs/api/REST/9.17.0/#api/2/comment/%7BcommentId%7D/properties
7
- class Comment < Common; end
7
+ class Comment < Common
8
+ def create(id_or_key, &block)
9
+ abort 'Issue ID or KEY is required' if id_or_key.to_s.strip.empty?
10
+ builder do
11
+ path "issue/#{id_or_key}/comment"
12
+ method :post
13
+ instance_eval(&block) if block_given?
14
+ end
15
+ run
16
+ end
17
+ end
8
18
  end
9
19
  end