rate-limited-jira-ruby 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: f974fc0246b6865d25b39323815e3bf13443b40a242d58dc1c6fd3a2a8f6585a
4
+ data.tar.gz: df93cdcddc729ea281903252440d1f9f15bff60b23e10c6d26c7aef25c21c244
5
+ SHA512:
6
+ metadata.gz: 0b4d577f477bb76a68b789170ebbe9ca85b29e42f80c1f000b5a088580ee12d0f572ceccf108940c7cd148a38be3d4eb83a92ab80a71f8e69b68bcf949ab915c
7
+ data.tar.gz: 104eff897e1ef2252152988e081c6031d95d1f517e0d5b7595f8068edb7b238cc5297fa57b108b9127ff14fc52ee0bc535c202c3009425ca9084390ab279a7b9
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,29 @@
1
+ AllCops:
2
+ NewCops: enable
3
+
4
+ plugins:
5
+ - rubocop-rake
6
+ - rubocop-rspec
7
+
8
+ RSpec/ExampleLength:
9
+ Enabled: true
10
+ Max: 10
11
+
12
+ RSpec/MessageExpectation:
13
+ Enabled: false
14
+
15
+ RSpec/MessageSpies:
16
+ Enabled: false
17
+
18
+ RSpec/MultipleExpectations:
19
+ Enabled: true
20
+ Max: 4
21
+
22
+ Style/Documentation:
23
+ Enabled: false
24
+
25
+ Style/StringLiterals:
26
+ EnforcedStyle: double_quotes
27
+
28
+ Style/StringLiteralsInInterpolation:
29
+ EnforcedStyle: double_quotes
data/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2026-05-18
4
+
5
+ - Initial release: transparent rate-limiting wrapper around jira-ruby's JIRA::Client
6
+ - In-process implementation using ruby-limiter
7
+ - Redis-based implementation using ratelimit + redis
8
+ - `RateLimitedJira::Client.build` factory; blank/nil implementation normalised to `:in_process`
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
6
+
7
+ group :development do
8
+ gem "aruba"
9
+ gem "cucumber"
10
+ gem "rake"
11
+ gem "rake-gem-maintenance"
12
+ gem "rspec"
13
+ gem "rubocop"
14
+ gem "rubocop-rake", require: false
15
+ gem "rubocop-rspec", require: false
16
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,306 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ rate-limited-jira-ruby (0.1.0)
5
+ jira-ruby
6
+ ratelimit
7
+ redis
8
+ ruby-limiter
9
+
10
+ GEM
11
+ remote: https://rubygems.org/
12
+ specs:
13
+ activesupport (8.1.3)
14
+ base64
15
+ bigdecimal
16
+ concurrent-ruby (~> 1.0, >= 1.3.1)
17
+ connection_pool (>= 2.2.5)
18
+ drb
19
+ i18n (>= 1.6, < 2)
20
+ json
21
+ logger (>= 1.4.2)
22
+ minitest (>= 5.1)
23
+ securerandom (>= 0.3)
24
+ tzinfo (~> 2.0, >= 2.0.5)
25
+ uri (>= 0.13.1)
26
+ aruba (2.4.1)
27
+ bundler (>= 1.17)
28
+ contracts (>= 0.16.0, < 0.18.0)
29
+ cucumber (>= 8.0, < 12.0)
30
+ irb (~> 1.16)
31
+ rspec-expectations (>= 3.4, < 5.0)
32
+ thor (~> 1.0)
33
+ ast (2.4.3)
34
+ atlassian-jwt (0.2.1)
35
+ jwt (~> 2.1)
36
+ base64 (0.3.0)
37
+ bigdecimal (4.1.2)
38
+ builder (3.3.0)
39
+ bundler-audit (0.9.3)
40
+ bundler (>= 1.2.0)
41
+ thor (~> 1.0)
42
+ cgi (0.5.1)
43
+ concurrent-ruby (1.3.6)
44
+ connection_pool (3.0.2)
45
+ contracts (0.17.3)
46
+ cucumber (11.0.0)
47
+ base64 (~> 0.2)
48
+ builder (~> 3.2)
49
+ cucumber-ci-environment (> 9, < 12)
50
+ cucumber-core (>= 16.2.0, < 17)
51
+ cucumber-cucumber-expressions (> 17, < 20)
52
+ cucumber-html-formatter (> 21, < 24)
53
+ diff-lcs (~> 1.5)
54
+ logger (~> 1.6)
55
+ mini_mime (~> 1.1)
56
+ multi_test (~> 1.1)
57
+ sys-uname (~> 1.5)
58
+ cucumber-ci-environment (11.0.0)
59
+ cucumber-core (16.2.0)
60
+ cucumber-gherkin (> 36, < 40)
61
+ cucumber-messages (> 31, < 33)
62
+ cucumber-tag-expressions (> 6, < 9)
63
+ cucumber-cucumber-expressions (19.0.0)
64
+ bigdecimal
65
+ cucumber-gherkin (39.1.0)
66
+ cucumber-messages (>= 31, < 33)
67
+ cucumber-html-formatter (23.1.0)
68
+ cucumber-messages (> 23, < 33)
69
+ cucumber-messages (32.3.1)
70
+ cucumber-tag-expressions (8.1.0)
71
+ date (3.5.1)
72
+ diff-lcs (1.6.2)
73
+ drb (2.2.3)
74
+ erb (6.0.4)
75
+ ffi (1.17.4)
76
+ ffi (1.17.4-x86_64-linux-gnu)
77
+ gem-release (2.2.4)
78
+ hashie (5.1.0)
79
+ logger
80
+ i18n (1.14.8)
81
+ concurrent-ruby (~> 1.0)
82
+ io-console (0.8.2)
83
+ irb (1.18.0)
84
+ pp (>= 0.6.0)
85
+ prism (>= 1.3.0)
86
+ rdoc (>= 4.0.0)
87
+ reline (>= 0.4.2)
88
+ jira-ruby (3.1.0)
89
+ activesupport
90
+ atlassian-jwt
91
+ cgi
92
+ multipart-post
93
+ oauth (~> 1.0)
94
+ json (2.19.4)
95
+ jwt (2.10.2)
96
+ base64
97
+ language_server-protocol (3.17.0.5)
98
+ lint_roller (1.1.0)
99
+ logger (1.7.0)
100
+ memoist3 (1.0.0)
101
+ mini_mime (1.1.5)
102
+ minitest (6.0.5)
103
+ drb (~> 2.0)
104
+ prism (~> 1.5)
105
+ multi_test (1.1.0)
106
+ multipart-post (2.4.1)
107
+ oauth (1.1.3)
108
+ base64 (~> 0.1)
109
+ oauth-tty (~> 1.0, >= 1.0.6)
110
+ snaky_hash (~> 2.0)
111
+ version_gem (~> 1.1, >= 1.1.9)
112
+ oauth-tty (1.0.6)
113
+ version_gem (~> 1.1, >= 1.1.9)
114
+ parallel (2.1.0)
115
+ parser (3.3.11.1)
116
+ ast (~> 2.4.1)
117
+ racc
118
+ pp (0.6.3)
119
+ prettyprint
120
+ prettyprint (0.2.0)
121
+ prism (1.9.0)
122
+ psych (5.3.1)
123
+ date
124
+ stringio
125
+ racc (1.8.1)
126
+ rainbow (3.1.1)
127
+ rake (13.4.2)
128
+ rake-gem-maintenance (0.2.0)
129
+ bundler-audit
130
+ gem-release
131
+ rake
132
+ rotp
133
+ ratelimit (1.1.0)
134
+ redis (>= 3.0.0)
135
+ redis-namespace (>= 1.0.0)
136
+ rdoc (7.2.0)
137
+ erb
138
+ psych (>= 4.0.0)
139
+ tsort
140
+ redis (5.4.1)
141
+ redis-client (>= 0.22.0)
142
+ redis-client (0.28.0)
143
+ connection_pool
144
+ redis-namespace (1.11.0)
145
+ redis (>= 4)
146
+ regexp_parser (2.12.0)
147
+ reline (0.6.3)
148
+ io-console (~> 0.5)
149
+ rotp (6.3.0)
150
+ rspec (3.13.2)
151
+ rspec-core (~> 3.13.0)
152
+ rspec-expectations (~> 3.13.0)
153
+ rspec-mocks (~> 3.13.0)
154
+ rspec-core (3.13.6)
155
+ rspec-support (~> 3.13.0)
156
+ rspec-expectations (3.13.5)
157
+ diff-lcs (>= 1.2.0, < 2.0)
158
+ rspec-support (~> 3.13.0)
159
+ rspec-mocks (3.13.8)
160
+ diff-lcs (>= 1.2.0, < 2.0)
161
+ rspec-support (~> 3.13.0)
162
+ rspec-support (3.13.7)
163
+ rubocop (1.86.1)
164
+ json (~> 2.3)
165
+ language_server-protocol (~> 3.17.0.2)
166
+ lint_roller (~> 1.1.0)
167
+ parallel (>= 1.10)
168
+ parser (>= 3.3.0.2)
169
+ rainbow (>= 2.2.2, < 4.0)
170
+ regexp_parser (>= 2.9.3, < 3.0)
171
+ rubocop-ast (>= 1.49.0, < 2.0)
172
+ ruby-progressbar (~> 1.7)
173
+ unicode-display_width (>= 2.4.0, < 4.0)
174
+ rubocop-ast (1.49.1)
175
+ parser (>= 3.3.7.2)
176
+ prism (~> 1.7)
177
+ rubocop-rake (0.7.1)
178
+ lint_roller (~> 1.1)
179
+ rubocop (>= 1.72.1)
180
+ rubocop-rspec (3.9.0)
181
+ lint_roller (~> 1.1)
182
+ rubocop (~> 1.81)
183
+ ruby-limiter (2.3.0)
184
+ ruby-progressbar (1.13.0)
185
+ securerandom (0.4.1)
186
+ snaky_hash (2.0.3)
187
+ hashie (>= 0.1.0, < 6)
188
+ version_gem (>= 1.1.8, < 3)
189
+ stringio (3.2.0)
190
+ sys-uname (1.5.1)
191
+ ffi (~> 1.1)
192
+ memoist3 (~> 1.0.0)
193
+ thor (1.5.0)
194
+ tsort (0.2.0)
195
+ tzinfo (2.0.6)
196
+ concurrent-ruby (~> 1.0)
197
+ unicode-display_width (3.2.0)
198
+ unicode-emoji (~> 4.1)
199
+ unicode-emoji (4.2.0)
200
+ uri (1.1.1)
201
+ version_gem (1.1.9)
202
+
203
+ PLATFORMS
204
+ ruby
205
+ x86_64-linux
206
+
207
+ DEPENDENCIES
208
+ aruba
209
+ cucumber
210
+ rake
211
+ rake-gem-maintenance
212
+ rate-limited-jira-ruby!
213
+ rspec
214
+ rubocop
215
+ rubocop-rake
216
+ rubocop-rspec
217
+
218
+ CHECKSUMS
219
+ activesupport (8.1.3) sha256=21a5e0dfbd4c3ddd9e1317ec6a4d782fa226e7867dc70b0743acda81a1dca20e
220
+ aruba (2.4.1) sha256=c6821949136bd9c953b3cf553971c4780faa26bb40082ac5ce631c541e0caf0d
221
+ ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
222
+ atlassian-jwt (0.2.1) sha256=2fd2d87418773f2e140c038cb22e049069708aff2bd0a423a7e1740574e97823
223
+ base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
224
+ bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
225
+ builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f
226
+ bundler-audit (0.9.3) sha256=81c8766c71e47d0d28a0f98c7eed028539f21a6ea3cd8f685eb6f42333c9b4e9
227
+ cgi (0.5.1) sha256=e93fcafc69b8a934fe1e6146121fa35430efa8b4a4047c4893764067036f18e9
228
+ concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
229
+ connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
230
+ contracts (0.17.3) sha256=e72e626413ea47099becb7b5683beb1c2ea902c69f5bad55c9258fe2b48314d7
231
+ cucumber (11.0.0) sha256=14bb964cc172999e9010fece2fad9f104044b055b3199230091894637a2a784c
232
+ cucumber-ci-environment (11.0.0) sha256=0df79a9e1d0b015b3d9def680f989200d96fef206f4d19ccf86a338c4f71d1e2
233
+ cucumber-core (16.2.0) sha256=592b58a95cf42feef8e5a349f68e363784ba3b6568ffbcf6776e38e136cf970b
234
+ cucumber-cucumber-expressions (19.0.0) sha256=33208ff204732ac9bed42b46993a0a243054f71ece08579d57e53df6a1c9d93a
235
+ cucumber-gherkin (39.1.0) sha256=aed12a0c955d8563d80a012633c1a72075525f4d64d4cc983001df2181b379ed
236
+ cucumber-html-formatter (23.1.0) sha256=7789b4a792c876394b9604aeb66aa5cf4c61514473b7e712c76d5eaedcdd8cdf
237
+ cucumber-messages (32.3.1) sha256=ddc88e4c1cf7afb96c06005b92a4a6f221a2fa435a8b4ca04677d215fd82771c
238
+ cucumber-tag-expressions (8.1.0) sha256=9bd8c4b6654f8e5bf2a9c99329b6f32136a75e50cd39d4cfb3927d0fa9f52e21
239
+ date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
240
+ diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
241
+ drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
242
+ erb (6.0.4) sha256=38e3803694be357fe2bfe312487c74beaf9fb4e5beb3e22498952fe1645b95d9
243
+ ffi (1.17.4) sha256=bcd1642e06f0d16fc9e09ac6d49c3a7298b9789bcb58127302f934e437d60acf
244
+ ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d
245
+ gem-release (2.2.4) sha256=2f11124c1580c811507c3b47e875e420cf3ed792a98105b49df11971e6e94db3
246
+ hashie (5.1.0) sha256=c266471896f323c446ea8207f8ffac985d2718df0a0ba98651a3057096ca3870
247
+ i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
248
+ io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
249
+ irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3
250
+ jira-ruby (3.1.0) sha256=45664dac20565155a5fcde514be8820a10fa6c0153d494f7aa38e77b4248ad0b
251
+ json (2.19.4) sha256=670a7d333fb3b18ca5b29cb255eb7bef099e40d88c02c80bd42a3f30fe5239ac
252
+ jwt (2.10.2) sha256=31e1ee46f7359883d5e622446969fe9c118c3da87a0b1dca765ce269c3a0c4f4
253
+ language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
254
+ lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
255
+ logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
256
+ memoist3 (1.0.0) sha256=686e42402cf150a362050c23143dc57b0ef88f8c344943ff8b7845792b50d56f
257
+ mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef
258
+ minitest (6.0.5) sha256=f007d7246bf4feea549502842cd7c6aba8851cdc9c90ba06de9c476c0d01155c
259
+ multi_test (1.1.0) sha256=e9e550cdd863fb72becfe344aefdcd4cbd26ebf307847f4a6c039a4082324d10
260
+ multipart-post (2.4.1) sha256=9872d03a8e552020ca096adadbf5e3cb1cd1cdd6acd3c161136b8a5737cdb4a8
261
+ oauth (1.1.3) sha256=71ca1b534561bf31a9b2aee01147384064b555e796d1a0fe2591806bb4bdd633
262
+ oauth-tty (1.0.6) sha256=9e8bd1861d367cce18318d8f214f2e1a1d7cb3898de0a9ea79162b4fdecb3152
263
+ parallel (2.1.0) sha256=b35258865c2e31134c5ecb708beaaf6772adf9d5efae28e93e99260877b09356
264
+ parser (3.3.11.1) sha256=d17ace7aabe3e72c3cc94043714be27cc6f852f104d81aa284c2281aecc65d54
265
+ pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6
266
+ prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
267
+ prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
268
+ psych (5.3.1) sha256=eb7a57cef10c9d70173ff74e739d843ac3b2c019a003de48447b2963d81b1974
269
+ racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
270
+ rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
271
+ rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
272
+ rake-gem-maintenance (0.2.0) sha256=d43f2c0387377eb9be78594c7420518379e184a601581506562b3e5fa0f9d89b
273
+ rate-limited-jira-ruby (0.1.0)
274
+ ratelimit (1.1.0) sha256=e3fb5086f14f7119a299027a1eb844ad3c8d07f718e90df9f07ba7863b25f551
275
+ rdoc (7.2.0) sha256=8650f76cd4009c3b54955eb5d7e3a075c60a57276766ebf36f9085e8c9f23192
276
+ redis (5.4.1) sha256=b5e675b57ad22b15c9bcc765d5ac26f60b675408af916d31527af9bd5a81faae
277
+ redis-client (0.28.0) sha256=888892f9cd8787a41c0ece00bdf5f556dfff7770326ce40bb2bc11f1bfec824b
278
+ redis-namespace (1.11.0) sha256=e91a1aa2b2d888b6dea1d4ab8d39e1ae6fac3426161feb9d91dd5cca598a2239
279
+ regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb
280
+ reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
281
+ rotp (6.3.0) sha256=75d40087e65ed0d8022c33055a6306c1c400d1c12261932533b5d6cbcd868854
282
+ rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
283
+ rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
284
+ rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
285
+ rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47
286
+ rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c
287
+ rubocop (1.86.1) sha256=44415f3f01d01a21e01132248d2fd0867572475b566ca188a0a42133a08d4531
288
+ rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035
289
+ rubocop-rake (0.7.1) sha256=3797f2b6810c3e9df7376c26d5f44f3475eda59eb1adc38e6f62ecf027cbae4d
290
+ rubocop-rspec (3.9.0) sha256=8fa70a3619408237d789aeecfb9beef40576acc855173e60939d63332fdb55e2
291
+ ruby-limiter (2.3.0) sha256=a8c247e14bc092c749d67bcd2a2def052580447da02b04612a2baa88cb154464
292
+ ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
293
+ securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
294
+ snaky_hash (2.0.3) sha256=25a3d299566e8153fb02fa23fd9a9358845950f7a523ddbbe1fa1e0d79a6d456
295
+ stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1
296
+ sys-uname (1.5.1) sha256=784d7e6491b0393c25cbbe5ac38324ac7be9fda083a6094832648af669386d7b
297
+ thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73
298
+ tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
299
+ tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b
300
+ unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
301
+ unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
302
+ uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6
303
+ version_gem (1.1.9) sha256=0c1a0962ae543c84a00889bb018d9f14d8f8af6029d26b295d98774e3d2eb9a4
304
+
305
+ BUNDLED WITH
306
+ 4.0.10
data/Rakefile ADDED
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/gem/maintenance/install_tasks"
5
+ require "rspec/core/rake_task"
6
+ require "rubocop/rake_task"
7
+ require "cucumber/rake/task"
8
+
9
+ RSpec::Core::RakeTask.new(:spec)
10
+
11
+ RuboCop::RakeTask.new(:rubocop) do |task|
12
+ task.options = ["--autocorrect"]
13
+ end
14
+
15
+ Cucumber::Rake::Task.new do |t|
16
+ t.profile = "rake"
17
+ end
18
+
19
+ task default: :verify
20
+
21
+ desc "Run all checks"
22
+ task verify: %i[rubocop spec cucumber]
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "rate_limited_jira"
5
+
6
+ begin
7
+ client = RateLimitedJira::Client.build(
8
+ {
9
+ username: ENV.fetch("JIRA_USERNAME"),
10
+ password: ENV.fetch("JIRA_API_TOKEN"),
11
+ site: ENV.fetch("JIRA_SITE_URL"),
12
+ context_path: ENV.fetch("JIRA_CONTEXT_PATH", ""),
13
+ auth_type: :basic
14
+ },
15
+ rate_limit_per_interval: ENV.fetch("JAT_RATE_LIMIT_PER_INTERVAL", "0").to_i,
16
+ rate_interval_in_seconds: ENV.fetch("JAT_RATE_INTERVAL_IN_SECONDS", "0").to_i,
17
+ implementation: ENV.fetch("JAT_RATE_LIMIT_IMPLEMENTATION", "")
18
+ )
19
+ 3.times { client.Board.all }
20
+ rescue StandardError => e
21
+ warn "#{e.class}: #{e.message}"
22
+ exit 1
23
+ end
data/cucumber.yml ADDED
@@ -0,0 +1,2 @@
1
+ default: --format pretty --publish-quiet
2
+ rake: --format progress --publish-quiet --tags 'not @wip'
@@ -0,0 +1,29 @@
1
+ Feature: Control the HTTP request rate limit
2
+ In order to work against a JIRA instance imposing API rate limits
3
+ As a user
4
+ I need the ability to control the HTTP request limit
5
+
6
+ Scenario Outline: Limiting the request rate
7
+ Given the following environment variables are set:
8
+ | name | value |
9
+ | JAT_RATE_INTERVAL_IN_SECONDS | <rate_interval_in_seconds> |
10
+ | JAT_RATE_LIMIT_IMPLEMENTATION | <jat_rate_limit_implementation> |
11
+ | JAT_RATE_LIMIT_PER_INTERVAL | <rate_limit_per_interval> |
12
+ Then successfully running `rate-limited-jira-tool` takes between <minimal_time> and <maximal_time> seconds
13
+
14
+ Examples:
15
+ | jat_rate_limit_implementation | rate_limit_per_interval | rate_interval_in_seconds | minimal_time | maximal_time |
16
+ | | 0 | 0 | 0 | 5 |
17
+ | in_process | 1 | 1 | 1 | 20 |
18
+ | redis | 1 | 2 | 1 | 20 |
19
+ | redis | 1 | 10 | 18 | 120 |
20
+
21
+ Scenario: Unexpected rate limiting implementation generates an error
22
+ Given the following environment variables are set:
23
+ | name | value |
24
+ | JAT_RATE_LIMIT_IMPLEMENTATION | UNKNOWN IMPLEMENTATION |
25
+ When I run `rate-limited-jira-tool`
26
+ Then it should fail with:
27
+ """
28
+ unknown rate limiting implementation
29
+ """
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ BUFFER_TIME_IN_SECONDS = 10
4
+
5
+ Given(/^the following environment variables are set:$/) do |table|
6
+ table.hashes.each do |row|
7
+ set_environment_variable(row.fetch("name"), row.fetch("value"))
8
+ end
9
+ end
10
+
11
+ Then(/^successfully running `(.*)` takes between (.*) and (.*) seconds$/) do |cmd, min, max|
12
+ start_time = Time.now
13
+ run_command_and_stop(cmd, fail_on_error: true, exit_timeout: max.to_i + BUFFER_TIME_IN_SECONDS)
14
+ end_time = Time.now
15
+ expect(end_time - start_time).to be_between(min.to_i, max.to_i)
16
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "aruba/cucumber"
4
+
5
+ ENV["PATH"] = File.join(__dir__, "..", "..", "bin") + File::PATH_SEPARATOR + ENV.fetch("PATH", nil)
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ Before do
4
+ setup_aruba
5
+ ENV["HOME"] = expand_path(".")
6
+ cd(".")
7
+ %w[JIRA_USERNAME JIRA_API_TOKEN JIRA_SITE_URL JIRA_CONTEXT_PATH].each do |var|
8
+ set_environment_variable(var, ENV[var]) if ENV[var]
9
+ end
10
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby-limiter"
4
+
5
+ module RateLimitedJira
6
+ class Client
7
+ class InProcessBased < Client
8
+ def rate_limit(&block)
9
+ rate_queue.shift
10
+
11
+ block.call
12
+ end
13
+
14
+ def rate_queue
15
+ @rate_queue ||=
16
+ Limiter::RateQueue.new(rate_limit_per_interval, interval: rate_interval_in_seconds)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ratelimit"
4
+ require "redis"
5
+
6
+ module RateLimitedJira
7
+ class Client
8
+ class RedisBased < Client
9
+ RATE_LIMITER_KEY = "rate_limited_jira_api_requests"
10
+
11
+ def rate_limit(&block)
12
+ rate_limiter.exec_within_threshold(RATE_LIMITER_KEY,
13
+ interval: rate_interval_in_seconds,
14
+ threshold: rate_limit_per_interval) do
15
+ response = block.call
16
+
17
+ rate_limiter.add(RATE_LIMITER_KEY)
18
+
19
+ response
20
+ end
21
+ end
22
+
23
+ def rate_limiter
24
+ self.class.rate_limiter(RATE_LIMITER_KEY, rate_interval_in_seconds)
25
+ end
26
+
27
+ def self.rate_limiter(rate_limiter_key, rate_interval)
28
+ @rate_limiter ||= Ratelimit.new(rate_limiter_key, bucket_interval: rate_interval)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "jira-ruby"
4
+
5
+ module RateLimitedJira
6
+ class Client < JIRA::Client
7
+ NO_RATE_LIMIT_PER_INTERVAL = 0
8
+ NO_RATE_INTERVAL_IN_SECONDS = 0
9
+
10
+ def self.build(jira_options, rate_limit_per_interval: 0, rate_interval_in_seconds: 0, implementation: :in_process)
11
+ implementation_class_for(implementation)
12
+ .new(jira_options,
13
+ rate_limit_per_interval: rate_limit_per_interval,
14
+ rate_interval_in_seconds: rate_interval_in_seconds)
15
+ end
16
+
17
+ def self.implementation_class_for(implementation)
18
+ impl = implementation.to_s.then { |s| s.empty? ? :in_process : s.to_sym }
19
+ case impl
20
+ when :in_process then InProcessBased
21
+ when :redis then RedisBased
22
+ else
23
+ raise ArgumentError, "#{implementation.inspect}: unknown rate limiting implementation. " \
24
+ "Valid options: :in_process, :redis"
25
+ end
26
+ end
27
+ private_class_method :implementation_class_for
28
+
29
+ attr_reader :rate_interval_in_seconds, :rate_limit_per_interval
30
+
31
+ def initialize(options, rate_interval_in_seconds: 0, rate_limit_per_interval: 0)
32
+ super(options)
33
+ @rate_interval_in_seconds = rate_interval_in_seconds
34
+ @rate_limit_per_interval = rate_limit_per_interval
35
+ end
36
+
37
+ alias original_request request
38
+
39
+ def request(*)
40
+ if rate_limit_per_interval == NO_RATE_LIMIT_PER_INTERVAL
41
+ original_request(*)
42
+ else
43
+ rate_limit { original_request(*) }
44
+ end
45
+ end
46
+
47
+ def rate_limit(&)
48
+ raise NotImplementedError, "rate_limit must be implemented by a subclass"
49
+ end
50
+ end
51
+ end
52
+
53
+ require_relative "client/in_process_based"
54
+ require_relative "client/redis_based"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RateLimitedJira
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rate_limited_jira/version"
4
+ require_relative "rate_limited_jira/client"
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe RateLimitedJira::Client::InProcessBased do
4
+ def build_client
5
+ described_class.new({}, rate_interval_in_seconds:, rate_limit_per_interval:)
6
+ end
7
+
8
+ let(:client) { build_client }
9
+ let(:rate_interval_in_seconds) { 2 }
10
+ let(:rate_limit_per_interval) { 1 }
11
+
12
+ describe "#rate_limit" do
13
+ let(:rate_queue) { instance_double(Limiter::RateQueue) }
14
+
15
+ it "properly initializes the rate queue" do
16
+ allow(Limiter::RateQueue)
17
+ .to receive(:new).with(rate_limit_per_interval, interval: rate_interval_in_seconds)
18
+ .and_return(rate_queue)
19
+
20
+ allow(rate_queue).to receive(:shift)
21
+
22
+ expect(client.rate_limit { :do_nothing }).to eq(:do_nothing)
23
+ end
24
+
25
+ context "when rate limiting multiple requests" do
26
+ let(:rate_limit_4_calls_to_original_request_code) do
27
+ 4.times { client.rate_limit { client.original_request(:get, "/path/to/resource") } }
28
+ end
29
+
30
+ before do
31
+ allow(client).to receive(:original_request).with(:get, "/path/to/resource")
32
+ allow(client).to receive_messages(rate_queue: rate_queue)
33
+ allow(rate_queue).to receive(:shift)
34
+ end
35
+
36
+ it "shifts the queue and performs the request call" do
37
+ rate_limit_4_calls_to_original_request_code
38
+
39
+ expect(rate_queue).to have_received(:shift).exactly(4).times
40
+ expect(client).to have_received(:original_request).exactly(4).times
41
+ end
42
+ end
43
+
44
+ describe "#rate_queue" do
45
+ let(:another_client) { build_client }
46
+
47
+ it "creating a second client returns another queue" do
48
+ expect(another_client.rate_queue).not_to equal(client.rate_queue)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe RateLimitedJira::Client::RedisBased do
4
+ describe "#rate_limit" do
5
+ let(:client) { described_class.new({}, rate_interval_in_seconds:, rate_limit_per_interval:) }
6
+
7
+ let(:rate_interval_in_seconds) { 2 }
8
+ let(:rate_limit_per_interval) { 1 }
9
+ let(:rate_limiter) { instance_double(Ratelimit) }
10
+
11
+ let(:rate_limit_4_calls_to_original_request_code) do
12
+ 4.times { client.rate_limit { client.original_request(:get, "/path/to/resource") } }
13
+ end
14
+
15
+ before do
16
+ allow(described_class).to receive_messages(rate_limiter: rate_limiter)
17
+ allow(client).to receive(:original_request)
18
+ end
19
+
20
+ it "uses :exec_within_threshold to control rate limiting" do
21
+ allow(rate_limiter).to receive(:exec_within_threshold)
22
+
23
+ rate_limit_4_calls_to_original_request_code
24
+
25
+ expect(rate_limiter)
26
+ .to have_received(:exec_within_threshold)
27
+ .with(RateLimitedJira::Client::RedisBased::RATE_LIMITER_KEY,
28
+ { interval: rate_interval_in_seconds, threshold: rate_limit_per_interval })
29
+ .exactly(4).times
30
+ end
31
+
32
+ it "keeps track of rate limiter key calls" do
33
+ allow(rate_limiter).to receive(:exec_within_threshold).and_yield
34
+ allow(rate_limiter).to receive(:add)
35
+
36
+ rate_limit_4_calls_to_original_request_code
37
+
38
+ expect(rate_limiter)
39
+ .to have_received(:add)
40
+ .with(RateLimitedJira::Client::RedisBased::RATE_LIMITER_KEY)
41
+ .exactly(4).times
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe RateLimitedJira::Client do
4
+ describe ".build" do
5
+ let(:jira_options) { {} }
6
+
7
+ context "when implementation is unspecified" do
8
+ subject(:result) { described_class.build(jira_options) }
9
+
10
+ it { expect(result).to be_a(RateLimitedJira::Client::InProcessBased) }
11
+ end
12
+
13
+ context "when implementation is :in_process" do
14
+ subject(:result) { described_class.build(jira_options, implementation: :in_process) }
15
+
16
+ it { expect(result).to be_a(RateLimitedJira::Client::InProcessBased) }
17
+ end
18
+
19
+ context "when implementation is blank" do
20
+ subject(:result) { described_class.build(jira_options, implementation: "") }
21
+
22
+ it { expect(result).to be_a(RateLimitedJira::Client::InProcessBased) }
23
+ end
24
+
25
+ context "when implementation is :redis" do
26
+ subject(:result) { described_class.build(jira_options, implementation: :redis) }
27
+
28
+ it { expect(result).to be_a(RateLimitedJira::Client::RedisBased) }
29
+ end
30
+
31
+ context "when implementation is unknown" do
32
+ subject(:result) { described_class.build(jira_options, implementation: :unknown) }
33
+
34
+ it do
35
+ expect { result }
36
+ .to raise_error(ArgumentError, /:unknown.*unknown rate limiting implementation/)
37
+ end
38
+ end
39
+ end
40
+
41
+ RSpec.shared_examples "a rate limited client" do
42
+ before do
43
+ allow(client).to receive_messages(original_request: :response)
44
+ end
45
+
46
+ it "returns the response" do
47
+ expect(client.request(:get, "/path/to/resource")).to eq(:response)
48
+ end
49
+
50
+ it "calls the original request method" do
51
+ client.request(:get, "/path/to/resource")
52
+
53
+ expect(client).to have_received(:original_request).with(:get, "/path/to/resource")
54
+ end
55
+ end
56
+
57
+ describe "#request" do
58
+ let(:client) { described_class.new({}, rate_interval_in_seconds:, rate_limit_per_interval:) }
59
+
60
+ context "when rate limiting is disabled" do
61
+ let(:rate_interval_in_seconds) { 0 }
62
+ let(:rate_limit_per_interval) { 0 }
63
+
64
+ it_behaves_like "a rate limited client"
65
+
66
+ it "does not use the rate limiter" do
67
+ allow(client).to receive(:original_request).with(:get, "/path/to/resource")
68
+ expect(client).not_to receive(:rate_limit)
69
+
70
+ client.request(:get, "/path/to/resource")
71
+ end
72
+ end
73
+
74
+ context "when rate limiting is enabled" do
75
+ let(:rate_interval_in_seconds) { 2 }
76
+ let(:rate_limit_per_interval) { 1 }
77
+
78
+ it_behaves_like "a rate limited client" do
79
+ before { allow(client).to receive(:rate_limit).and_yield }
80
+ end
81
+
82
+ it "uses the rate limiter" do
83
+ allow(client).to receive(:original_request).with(:get, "/path/to/resource")
84
+ expect(client).to receive(:rate_limit).and_yield
85
+
86
+ client.request(:get, "/path/to/resource")
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rate_limited_jira"
4
+
5
+ RSpec.configure do |config|
6
+ config.disable_monkey_patching!
7
+
8
+ config.expect_with :rspec do |expectations|
9
+ expectations.syntax = :expect
10
+ end
11
+
12
+ config.mock_with :rspec do |mocks|
13
+ mocks.verify_partial_doubles = true
14
+ end
15
+ end
metadata ADDED
@@ -0,0 +1,121 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rate-limited-jira-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Christophe Broult
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: jira-ruby
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: ratelimit
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: redis
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: ruby-limiter
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ email:
69
+ - cbroult@yahoo.com
70
+ executables: []
71
+ extensions: []
72
+ extra_rdoc_files: []
73
+ files:
74
+ - ".rspec"
75
+ - ".rubocop.yml"
76
+ - CHANGELOG.md
77
+ - Gemfile
78
+ - Gemfile.lock
79
+ - Rakefile
80
+ - bin/rate-limited-jira-tool
81
+ - cucumber.yml
82
+ - features/control_http_request_rate_limit.feature
83
+ - features/step_definitions/execution_context_steps.rb
84
+ - features/support/env.rb
85
+ - features/support/hooks.rb
86
+ - lib/rate_limited_jira.rb
87
+ - lib/rate_limited_jira/client.rb
88
+ - lib/rate_limited_jira/client/in_process_based.rb
89
+ - lib/rate_limited_jira/client/redis_based.rb
90
+ - lib/rate_limited_jira/version.rb
91
+ - spec/rate_limited_jira/client/in_process_based_spec.rb
92
+ - spec/rate_limited_jira/client/redis_based_spec.rb
93
+ - spec/rate_limited_jira/client_spec.rb
94
+ - spec/spec_helper.rb
95
+ homepage: https://github.com/cbroult/rate-limited-jira-ruby
96
+ licenses:
97
+ - MIT
98
+ metadata:
99
+ allowed_push_host: https://rubygems.org
100
+ homepage_uri: https://github.com/cbroult/rate-limited-jira-ruby
101
+ source_code_uri: https://github.com/cbroult/rate-limited-jira-ruby
102
+ changelog_uri: https://github.com/cbroult/rate-limited-jira-ruby/blob/main/CHANGELOG.md
103
+ rubygems_mfa_required: 'true'
104
+ rdoc_options: []
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: 3.2.0
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ requirements: []
118
+ rubygems_version: 4.0.10
119
+ specification_version: 4
120
+ summary: Transparent rate-limiting wrapper around jira-ruby's JIRA::Client.
121
+ test_files: []