ruby-jira 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a560af049fc2790fd46fcfa42a8913ff6a3e35fb6da619d0dbee7873a872fdec
4
+ data.tar.gz: d5e1af098eac6a126ca0643545effb8c9f420ef30349c716d127d7018ff3e8c5
5
+ SHA512:
6
+ metadata.gz: c144da359a042b32d600661c40eb4d8588eac575ded10b1a00cec642958631fe50a878f240a97c2d8e6220824aa49973f49103fa1e4cac0f0774f6f39d754886
7
+ data.tar.gz: 15b14457c90a78271c0af4644276d945dd3cea0d980919967f24081d11a53b5b0b8a710204aeaa71757c68b89798a63d5f83ca3301543d79e3f2e9d02d8f92d5
data/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2026-03-09
9
+
10
+ ### Added
11
+
12
+ - Add initial ruby-jira Jira API client gem ([09c7c53](https://github.com/macio/ruby-jira/commit/09c7c5302cec9ccd079d85f0c46466f53775e9da))
13
+
14
+
data/LICENSE.txt ADDED
@@ -0,0 +1,24 @@
1
+ BSD 2-Clause License
2
+
3
+ Copyright (c) 2026, Maciej Kozak
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
19
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,273 @@
1
+ # ruby-jira
2
+
3
+ Ruby client for the [Jira Cloud REST API v3](https://developer.atlassian.com/cloud/jira/platform/rest/v3/).
4
+
5
+ > Inspired by and based on the architecture of [NARKOZ/gitlab](https://github.com/NARKOZ/gitlab) — a Ruby wrapper for the GitLab API. Many thanks for the solid foundation.
6
+
7
+ ## Requirements
8
+
9
+ Ruby **3.2** or newer. Tested on 3.2, 3.3, and 3.4. Ruby 3.1 and older are not supported (EOL).
10
+
11
+ ## Installation
12
+
13
+ Add to your Gemfile:
14
+
15
+ ```ruby
16
+ gem "ruby-jira"
17
+ ```
18
+
19
+ ## Authentication
20
+
21
+ Two auth methods are supported: **Basic** (email + API token) and **OAuth 2.0**.
22
+
23
+ ### Basic auth
24
+
25
+ ```ruby
26
+ Jira.configure do |config|
27
+ config.endpoint = "https://your-domain.atlassian.net"
28
+ config.auth_type = :basic
29
+ config.email = "you@example.com"
30
+ config.api_token = "your-api-token"
31
+ end
32
+ ```
33
+
34
+ Or via environment variables:
35
+
36
+ ```
37
+ JIRA_ENDPOINT=https://your-domain.atlassian.net
38
+ JIRA_EMAIL=you@example.com
39
+ JIRA_API_TOKEN=your-api-token
40
+ ```
41
+
42
+ ### OAuth 2.0 — pre-fetched access token
43
+
44
+ ```ruby
45
+ Jira.configure do |config|
46
+ config.endpoint = "https://your-domain.atlassian.net"
47
+ config.auth_type = :oauth2
48
+ config.cloud_id = "your-cloud-id"
49
+ config.oauth_access_token = "your-access-token"
50
+ end
51
+ ```
52
+
53
+ ### OAuth 2.0 — automatic token refresh (`refresh_token` grant)
54
+
55
+ ```ruby
56
+ Jira.configure do |config|
57
+ config.endpoint = "https://your-domain.atlassian.net"
58
+ config.auth_type = :oauth2
59
+ config.cloud_id = "your-cloud-id"
60
+ config.oauth_grant_type = "refresh_token"
61
+ config.oauth_client_id = "your-client-id"
62
+ config.oauth_client_secret = "your-client-secret"
63
+ config.oauth_refresh_token = "your-refresh-token"
64
+ end
65
+ ```
66
+
67
+ ### OAuth 2.0 — service account (`client_credentials` grant)
68
+
69
+ ```ruby
70
+ Jira.configure do |config|
71
+ config.endpoint = "https://your-domain.atlassian.net"
72
+ config.auth_type = :oauth2
73
+ config.cloud_id = "your-cloud-id"
74
+ config.oauth_grant_type = "client_credentials"
75
+ config.oauth_client_id = "your-client-id"
76
+ config.oauth_client_secret = "your-client-secret"
77
+ end
78
+ ```
79
+
80
+ ## Usage
81
+
82
+ ### Client
83
+
84
+ Create a one-off client or use the global `Jira` facade:
85
+
86
+ ```ruby
87
+ client = Jira.client # uses global configuration
88
+ # or
89
+ client = Jira::Client.new(endpoint: "...", email: "...", api_token: "...")
90
+ ```
91
+
92
+ All methods are also available directly on the `Jira` module:
93
+
94
+ ```ruby
95
+ Jira.projects
96
+ Jira.issue("TEST-1")
97
+ ```
98
+
99
+ ### Response objects
100
+
101
+ All responses are returned as `Jira::ObjectifiedHash` instances, supporting both dot-notation and bracket access:
102
+
103
+ ```ruby
104
+ issue = Jira.issue("TEST-1")
105
+ issue.key # => "TEST-1"
106
+ issue[:key] # => "TEST-1"
107
+ issue.fields.summary
108
+ issue.dig(:fields, :summary)
109
+ issue.to_h # => original Hash
110
+ ```
111
+
112
+ ### Projects
113
+
114
+ ```ruby
115
+ # Search projects (offset-paginated)
116
+ projects = Jira.projects(status: "live", maxResults: 50)
117
+ projects.total # => 42
118
+ projects.next_page? # => true
119
+ projects.map(&:key) # => ["TEST", "DEMO", ...]
120
+
121
+ # Auto-paginate all projects
122
+ all = projects.auto_paginate
123
+ all = projects.paginate_with_limit(100)
124
+
125
+ # Get a single project
126
+ project = Jira.project("TEST")
127
+ project.name
128
+ project.lead.displayName
129
+
130
+ # Archive a project
131
+ Jira.archive_project("TEST")
132
+ ```
133
+
134
+ ### Issues
135
+
136
+ ```ruby
137
+ # Get a single issue
138
+ issue = Jira.issue("TEST-1")
139
+ issue = Jira.issue("TEST-1", expand: "names,renderedFields")
140
+
141
+ # Create an issue
142
+ issue = Jira.create_issue({
143
+ fields: {
144
+ project: { key: "TEST" },
145
+ summary: "Something is broken",
146
+ issuetype: { id: "10001" }
147
+ }
148
+ })
149
+
150
+ # Update an issue
151
+ Jira.edit_issue("TEST-1", { fields: { summary: "Updated summary" } })
152
+ Jira.edit_issue("TEST-1", { fields: { summary: "Silent update" } }, notifyUsers: false)
153
+ ```
154
+
155
+ ### Permission schemes
156
+
157
+ ```ruby
158
+ Jira.permission_scheme("TEST")
159
+ Jira.issue_security_level_scheme("TEST")
160
+ Jira.assign_permission_scheme("TEST", scheme_id: 101)
161
+ ```
162
+
163
+ ### Pagination
164
+
165
+ Offset-paginated responses (`GET /project/search`, `GET /workflow/search`, etc.) return `Jira::PaginatedResponse`:
166
+
167
+ ```ruby
168
+ page = Jira.projects
169
+ page.total # total count
170
+ page.start_at # current offset
171
+ page.max_results # page size
172
+ page.last_page? # isLast flag
173
+ page.next_page?
174
+ page.next_page # fetches the next page
175
+ page.auto_paginate # fetches all pages, returns flat Array
176
+ page.paginate_with_limit(200)
177
+ page.each_page { |p| process(p) }
178
+ ```
179
+
180
+ Cursor-paginated responses (`POST /search/jql`, etc.) return `Jira::CursorPaginatedResponse`:
181
+
182
+ ```ruby
183
+ results = Jira.search_issues(jql: "project = TEST ORDER BY created DESC")
184
+ results.next_page_token # raw token
185
+ results.next_page?
186
+ results.next_page # fetches next page automatically
187
+ results.auto_paginate # fetches all pages
188
+ ```
189
+
190
+ ### Rate limiting
191
+
192
+ > Atlassian enforces a new points-based and tiered quota rate limiting policy for Jira Cloud apps since **March 2, 2026**.
193
+ > This gem follows the current [official Jira Cloud Rate Limiting guide](https://developer.atlassian.com/cloud/jira/platform/rate-limiting/).
194
+
195
+ The client automatically retries `429 Too Many Requests` and `503 Service Unavailable` (when rate-limit headers are present) on idempotent requests (`GET`, `PUT`, `DELETE`).
196
+
197
+ **Supported response headers** (as enforced by Jira Cloud):
198
+
199
+ | Header | Format | Description |
200
+ | ----------------------- | ------------------ | ------------------------------------------------------------------------------ |
201
+ | `Retry-After` | integer seconds | How long to wait before retrying (429 and some 503) |
202
+ | `X-RateLimit-Reset` | ISO 8601 timestamp | When the rate-limit window resets (429 only) |
203
+ | `X-RateLimit-Limit` | integer | Max request rate for the current scope |
204
+ | `X-RateLimit-Remaining` | integer | Remaining capacity in the current window |
205
+ | `X-RateLimit-NearLimit` | `"true"` | Signals < 20% capacity remains — consider throttling proactively |
206
+ | `RateLimit-Reason` | string | Which limit was exceeded (`jira-burst-based`, `jira-quota-tenant-based`, etc.) |
207
+
208
+ **Retry strategy:** exponential backoff with proportional jitter (`delay × rand(0.7..1.3)`), respecting `Retry-After` and `X-RateLimit-Reset` headers. Falls back to backoff when no header is present.
209
+
210
+ Default configuration (aligned with Atlassian recommendations):
211
+
212
+ ```ruby
213
+ Jira.configure do |config|
214
+ config.ratelimit_retries = 4 # max retry attempts
215
+ config.ratelimit_base_delay = 2.0 # seconds, base for exponential backoff
216
+ config.ratelimit_max_delay = 30.0 # seconds, cap on backoff
217
+ end
218
+ ```
219
+
220
+ ### Proxy
221
+
222
+ ```ruby
223
+ Jira.http_proxy("proxy.example.com", 8080, "user", "pass")
224
+ ```
225
+
226
+ ## Configuration reference
227
+
228
+ | Key | ENV variable | Default |
229
+ | ---------------------- | --------------------------- | ---------------------------------------- |
230
+ | `endpoint` | `JIRA_ENDPOINT` | — |
231
+ | `auth_type` | `JIRA_AUTH_TYPE` | `:basic` |
232
+ | `email` | `JIRA_EMAIL` | — |
233
+ | `api_token` | `JIRA_API_TOKEN` | — |
234
+ | `oauth_access_token` | `JIRA_OAUTH_ACCESS_TOKEN` | — |
235
+ | `oauth_client_id` | `JIRA_OAUTH_CLIENT_ID` | — |
236
+ | `oauth_client_secret` | `JIRA_OAUTH_CLIENT_SECRET` | — |
237
+ | `oauth_refresh_token` | `JIRA_OAUTH_REFRESH_TOKEN` | — |
238
+ | `oauth_grant_type` | `JIRA_OAUTH_GRANT_TYPE` | — |
239
+ | `oauth_token_endpoint` | `JIRA_OAUTH_TOKEN_ENDPOINT` | `https://auth.atlassian.com/oauth/token` |
240
+ | `cloud_id` | `JIRA_CLOUD_ID` | — |
241
+ | `ratelimit_retries` | `JIRA_RATELIMIT_RETRIES` | `4` |
242
+ | `ratelimit_base_delay` | `JIRA_RATELIMIT_BASE_DELAY` | `2.0` |
243
+ | `ratelimit_max_delay` | `JIRA_RATELIMIT_MAX_DELAY` | `30.0` |
244
+
245
+ ## Error handling
246
+
247
+ ```ruby
248
+ rescue Jira::Error::Unauthorized # 401
249
+ rescue Jira::Error::Forbidden # 403
250
+ rescue Jira::Error::NotFound # 404
251
+ rescue Jira::Error::TooManyRequests # 429
252
+ rescue Jira::Error::ResponseError # any other 4xx/5xx
253
+ rescue Jira::Error::Base # all gem errors
254
+ ```
255
+
256
+ `Jira::Error::ResponseError` exposes:
257
+
258
+ ```ruby
259
+ e.response_status # HTTP status code
260
+ e.response_message # parsed message from response body
261
+ ```
262
+
263
+ ## Running the example script
264
+
265
+ ```bash
266
+ JIRA_ENDPOINT=https://your-domain.atlassian.net \
267
+ JIRA_EMAIL=you@example.com \
268
+ JIRA_API_TOKEN=your-api-token \
269
+ JIRA_PROJECT_KEY=TEST \
270
+ bundle exec ruby examples/basic_usage.rb
271
+ ```
272
+
273
+ See [examples/basic_usage.rb](examples/basic_usage.rb) for all supported environment variables.
data/lib/jira/api.rb ADDED
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jira
4
+ # @private
5
+ class API < Request
6
+ attr_accessor(*Configuration::VALID_OPTIONS_KEYS)
7
+
8
+ # Creates a new API.
9
+ # @raise [Jira::Error::MissingCredentials]
10
+ # rubocop:disable Lint/MissingSuper
11
+ def initialize(options = {})
12
+ options = Jira.options.merge(options)
13
+ Configuration::VALID_OPTIONS_KEYS.each do |key|
14
+ send("#{key}=", options[key]) if options.key?(key)
15
+ end
16
+
17
+ request_defaults
18
+ self.class.headers "User-Agent" => user_agent
19
+ end
20
+ # rubocop:enable Lint/MissingSuper
21
+ end
22
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jira
4
+ class Client
5
+ # Defines methods related to issues.
6
+ module Issues
7
+ # Creates a new issue
8
+ #
9
+ # @param payload [Hash] Issue payload
10
+ # @return [Hash]
11
+ def create_issue(payload = {})
12
+ post("/issue", body: payload)
13
+ end
14
+
15
+ # Gets a single issue
16
+ #
17
+ # @param issue_id_or_key [Integer, String] The ID or key of an issue
18
+ # @param options [Hash] Query parameters
19
+ # @return [Hash]
20
+ def issue(issue_id_or_key, options = {})
21
+ get("/issue/#{url_encode(issue_id_or_key)}", query: options)
22
+ end
23
+
24
+ # Updates an existing issue
25
+ #
26
+ # @param issue_id_or_key [Integer, String] The ID or key of an issue
27
+ # @param payload [Hash] Issue payload
28
+ # @param options [Hash] Query parameters
29
+ # @return [Hash]
30
+ def edit_issue(issue_id_or_key, payload = {}, options = {})
31
+ put("/issue/#{url_encode(issue_id_or_key)}", body: payload, query: options)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jira
4
+ class Client
5
+ # Defines methods related to project permission schemes.
6
+ module ProjectPermissionSchemes
7
+ # Gets assigned issue security level scheme for a project
8
+ #
9
+ # @param project_key_or_id [Integer, String] Project ID or key
10
+ # @param options [Hash] Query parameters
11
+ # @return [Hash]
12
+ def issue_security_level_scheme(project_key_or_id, options = {})
13
+ get("/project/#{url_encode(project_key_or_id)}/issuesecuritylevelscheme", query: options)
14
+ end
15
+
16
+ # Gets assigned permission scheme for a project
17
+ #
18
+ # @param project_key_or_id [Integer, String] Project ID or key
19
+ # @param options [Hash] Query parameters
20
+ # @return [Hash]
21
+ def permission_scheme(project_key_or_id, options = {})
22
+ get("/project/#{url_encode(project_key_or_id)}/permissionscheme", query: options)
23
+ end
24
+
25
+ # Assigns permission scheme to a project
26
+ #
27
+ # @param project_key_or_id [Integer, String] Project ID or key
28
+ # @param scheme_id [Integer] Permission scheme ID
29
+ # @param options [Hash] Additional payload
30
+ # @return [Hash]
31
+ def assign_permission_scheme(project_key_or_id, scheme_id:, options: {})
32
+ body = { id: scheme_id }.merge(options)
33
+ put("/project/#{url_encode(project_key_or_id)}/permissionscheme", body: body)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jira
4
+ class Client
5
+ # Defines methods related to projects.
6
+ module Projects
7
+ # Search projects
8
+ #
9
+ # @param options [Hash] Query parameters
10
+ # @return [Jira::Request::PaginatedResponse]
11
+ def projects(options = {})
12
+ get("/project/search", query: options)
13
+ end
14
+
15
+ # Gets a single project
16
+ #
17
+ # @param project_id_or_key [Integer, String] Project ID or key
18
+ # @param options [Hash] Query parameters
19
+ # @return [Hash]
20
+ def project(project_id_or_key, options = {})
21
+ get("/project/#{url_encode(project_id_or_key)}", query: options)
22
+ end
23
+
24
+ # Archives a project
25
+ #
26
+ # @param project_id_or_key [Integer, String] Project ID or key
27
+ # @return [Hash]
28
+ def archive_project(project_id_or_key)
29
+ post("/project/#{url_encode(project_id_or_key)}/archive")
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jira
4
+ class Client < API
5
+ Dir[File.expand_path("client/*.rb", __dir__)].each { |file| require file }
6
+
7
+ include Issues
8
+ include ProjectPermissionSchemes
9
+ include Projects
10
+
11
+ # Text representation of the client, masking auth secrets.
12
+ #
13
+ # @return [String]
14
+ def inspect
15
+ inspected = super
16
+ inspected = redact_secret(inspected, :api_token, @api_token) if @api_token
17
+ inspected = redact_secret(inspected, :oauth_access_token, @oauth_access_token) if @oauth_access_token
18
+ inspected = redact_secret(inspected, :oauth_client_secret, @oauth_client_secret) if @oauth_client_secret
19
+ inspected = redact_secret(inspected, :oauth_refresh_token, @oauth_refresh_token) if @oauth_refresh_token
20
+ inspected
21
+ end
22
+
23
+ # Utility method for URL encoding of a string.
24
+ #
25
+ # @return [String]
26
+ def url_encode(url)
27
+ url.to_s.b.gsub(/[^a-zA-Z0-9_\-.~]/n) { |match| format("%%%02X", match.unpack1("C")) }
28
+ end
29
+
30
+ private
31
+
32
+ def redact_secret(inspected, key, secret)
33
+ redacted = only_show_last_four_chars(secret)
34
+ inspected.sub %(@#{key}="#{secret}"), %(@#{key}="#{redacted}")
35
+ end
36
+
37
+ def only_show_last_four_chars(token)
38
+ return "****" if token.size <= 4
39
+
40
+ "#{"*" * (token.size - 4)}#{token[-4..]}"
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Jira
6
+ module Configuration
7
+ VALID_OPTIONS_KEYS = %i[
8
+ endpoint
9
+ user_agent
10
+ httparty
11
+ auth_type
12
+ email
13
+ api_token
14
+ oauth_access_token
15
+ oauth_client_id
16
+ oauth_client_secret
17
+ oauth_refresh_token
18
+ oauth_grant_type
19
+ oauth_token_endpoint
20
+ cloud_id
21
+ ratelimit_retries
22
+ ratelimit_base_delay
23
+ ratelimit_max_delay
24
+ ].freeze
25
+
26
+ DEFAULT_USER_AGENT = "Ruby Jira Gem #{Jira::VERSION}".freeze
27
+ DEFAULT_AUTH_TYPE = :basic
28
+ DEFAULT_RATELIMIT_RETRIES = 4
29
+ DEFAULT_RATELIMIT_BASE_DELAY = 2.0
30
+ DEFAULT_RATELIMIT_MAX_DELAY = 30.0
31
+ DEFAULT_OAUTH_TOKEN_ENDPOINT = "https://auth.atlassian.com/oauth/token"
32
+
33
+ attr_accessor(*VALID_OPTIONS_KEYS)
34
+
35
+ def self.extended(base)
36
+ base.reset
37
+ end
38
+
39
+ def configure
40
+ yield self
41
+ end
42
+
43
+ def options
44
+ VALID_OPTIONS_KEYS.to_h do |key|
45
+ [key, send(key)]
46
+ end
47
+ end
48
+
49
+ def reset # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
50
+ self.endpoint = ENV.fetch("JIRA_ENDPOINT", nil)
51
+ self.auth_type = (ENV["JIRA_AUTH_TYPE"] || DEFAULT_AUTH_TYPE).to_sym
52
+ self.email = ENV.fetch("JIRA_EMAIL", nil)
53
+ self.api_token = ENV.fetch("JIRA_API_TOKEN", nil)
54
+ self.oauth_access_token = ENV.fetch("JIRA_OAUTH_ACCESS_TOKEN", nil)
55
+ self.oauth_client_id = ENV.fetch("JIRA_OAUTH_CLIENT_ID", nil)
56
+ self.oauth_client_secret = ENV.fetch("JIRA_OAUTH_CLIENT_SECRET", nil)
57
+ self.oauth_refresh_token = ENV.fetch("JIRA_OAUTH_REFRESH_TOKEN", nil)
58
+ self.oauth_grant_type = ENV.fetch("JIRA_OAUTH_GRANT_TYPE", nil)
59
+ self.oauth_token_endpoint = ENV.fetch("JIRA_OAUTH_TOKEN_ENDPOINT", DEFAULT_OAUTH_TOKEN_ENDPOINT)
60
+ self.cloud_id = ENV.fetch("JIRA_CLOUD_ID", nil)
61
+ self.ratelimit_retries = integer_env("JIRA_RATELIMIT_RETRIES", DEFAULT_RATELIMIT_RETRIES)
62
+ self.ratelimit_base_delay = float_env("JIRA_RATELIMIT_BASE_DELAY", DEFAULT_RATELIMIT_BASE_DELAY)
63
+ self.ratelimit_max_delay = float_env("JIRA_RATELIMIT_MAX_DELAY", DEFAULT_RATELIMIT_MAX_DELAY)
64
+ self.httparty = get_httparty_config(ENV.fetch("JIRA_HTTPARTY_OPTIONS", nil))
65
+ self.user_agent = DEFAULT_USER_AGENT
66
+ end
67
+
68
+ private
69
+
70
+ def get_httparty_config(options)
71
+ return nil if options.nil? || options.empty?
72
+
73
+ config = YAML.safe_load(options, permitted_classes: [Symbol], aliases: false)
74
+ raise ArgumentError, "HTTParty config should be a Hash." unless config.is_a?(Hash)
75
+
76
+ symbolize_keys(config)
77
+ end
78
+
79
+ def symbolize_keys(value)
80
+ return value unless value.is_a?(Hash)
81
+
82
+ value.each_with_object({}) do |(key, nested_value), output|
83
+ output[key.to_sym] = nested_value.is_a?(Hash) ? symbolize_keys(nested_value) : nested_value
84
+ end
85
+ end
86
+
87
+ def integer_env(key, default)
88
+ value = ENV.fetch(key, nil)
89
+ value ? Integer(value, 10) : default
90
+ rescue ArgumentError
91
+ default
92
+ end
93
+
94
+ def float_env(key, default)
95
+ value = ENV.fetch(key, nil)
96
+ value ? Float(value) : default
97
+ rescue ArgumentError
98
+ default
99
+ end
100
+ end
101
+ end