idempo 1.2.2 → 1.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 70c151d3823c956aac177dbf39ee4e5f216b9464a739b134c8a47822a01aa765
4
- data.tar.gz: 4306e629c9d0f1bd59ad74bc6e61623e441e4713b88bccb8f69c08e8553dc251
3
+ metadata.gz: 3db7fbb3c612bae5675890f118986c3d026e49c73f3c74c24f15fea51e6a67c7
4
+ data.tar.gz: 65a193261af6e424fe8f7e3437d28e02c20ddb4c9ae199c9556f39676474b8ac
5
5
  SHA512:
6
- metadata.gz: 5e80a42f84a311e633697123f7213af21b0f79d501937d5963e4dd918eb5cabd5d01174d781b0528d4f51923fc9eb038af20afeb164c8cdd51d2cc1983ea1111
7
- data.tar.gz: 4d2ee3d361d009aacc23c42ec38323c9b9373d874c9abbd70596882dbd48d6ed4edcf9eb5c32f44e366aa091e178dfcd71c919b45d6bc1b8c17804094b23629f
6
+ metadata.gz: bd32562481a479070ed2806ef3201d370417fd9acfbbb8499d1aefb2f1000f278ee3dff47ccb0ac640f33001be8c41cccba7c498980d07c893dee519e53f539b
7
+ data.tar.gz: df3b05f901ac7e49bb82bfde9a522e2a179b96953db10751b7814858597a5a0af0aa82ff0fd6824e3a11cae6bdf4ebc84004a35b09cfae7a39958114a307eef8
@@ -34,19 +34,13 @@ jobs:
34
34
  - name: Standard (Lint)
35
35
  run: bundle exec rake standard
36
36
  test:
37
- name: Specs
37
+ name: "Specs (Rack 2 and 3)"
38
38
  runs-on: ubuntu-22.04
39
39
  if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
40
- env:
41
- IDEMPO_RACK_VERSION: ${{ matrix.rack }}
42
40
  strategy:
43
41
  matrix:
44
42
  ruby:
45
43
  - "2.7"
46
- - "3.2"
47
- rack:
48
- - "2.0"
49
- - "3.0"
50
44
  services:
51
45
  mysql:
52
46
  image: mysql:5.7
@@ -76,8 +70,8 @@ jobs:
76
70
  with:
77
71
  ruby-version: ${{ matrix.ruby }}
78
72
  bundler-cache: true
79
- - name: RSpec
80
- run: bundle exec rspec
73
+ - name: RSpec via Appraisal
74
+ run: "bundle exec appraisal install && bundle exec appraisal rspec"
81
75
  env:
82
76
  MYSQL_HOST: 127.0.0.1
83
77
  MYSQL_PORT: 3306
data/Appraisals ADDED
@@ -0,0 +1,9 @@
1
+ appraise "rack-3" do
2
+ gem "rack", ">= 3.0"
3
+ gem "activerecord", "~> 7.0", "< 9.0"
4
+ end
5
+
6
+ appraise "rack-2" do
7
+ gem "rack", ">= 2.0", "< 3.0"
8
+ gem "activerecord", "~> 6", "< 7.0"
9
+ end
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 1.3.0
2
+
3
+ - Streamline integration with both Rack 2 and 3, add tests for request fingerprinting.
4
+
1
5
  ## 1.2.2
2
6
 
3
7
  - Support `#to_ary` on Rack response bodies on newer Rails/Rack versions
data/Rakefile CHANGED
@@ -3,7 +3,11 @@
3
3
  require "bundler/gem_tasks"
4
4
  require "rspec/core/rake_task"
5
5
  require "standard/rake"
6
+ require "appraisal"
6
7
 
7
8
  RSpec::Core::RakeTask.new(:spec)
8
-
9
9
  task default: :spec
10
+
11
+ if !ENV["APPRAISAL_INITIALIZED"]
12
+ task default: :appraisal
13
+ end
data/idempo.gemspec CHANGED
@@ -29,7 +29,7 @@ Gem::Specification.new do |spec|
29
29
  # Specify which files should be added to the gem when it is released.
30
30
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
31
31
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
32
- `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
32
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features|gemfiles)/}) }
33
33
  end
34
34
  spec.bindir = "exe"
35
35
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
@@ -37,8 +37,8 @@ Gem::Specification.new do |spec|
37
37
 
38
38
  spec.add_dependency "msgpack"
39
39
  spec.add_dependency "measurometer", "~> 1.3"
40
+ spec.add_dependency "rack", "< 4"
40
41
 
41
- spec.add_development_dependency "rack", "~> 3"
42
42
  spec.add_development_dependency "rake", "~> 13.0"
43
43
  spec.add_development_dependency "rspec", "~> 3.0"
44
44
  spec.add_development_dependency "redis", "~> 4"
@@ -47,6 +47,7 @@ Gem::Specification.new do |spec|
47
47
  spec.add_development_dependency "mysql2"
48
48
  spec.add_development_dependency "pg"
49
49
  spec.add_development_dependency "standard"
50
+ spec.add_development_dependency "appraisal"
50
51
 
51
52
  # For more information and examples about making a new gem, checkout our
52
53
  # guide at: https://bundler.io/guides/creating_gem.html
@@ -8,6 +8,6 @@ class Idempo::ConcurrentRequestErrorApp
8
8
  message: "Another request with this idempotency key is still in progress, please try again later"
9
9
  }
10
10
  }
11
- [429, {"Retry-After" => RETRY_AFTER_SECONDS, "Content-Type" => "application/json"}, [JSON.pretty_generate(res)]]
11
+ [429, {"retry-after" => RETRY_AFTER_SECONDS, "content-type" => "application/json"}, [JSON.pretty_generate(res)]]
12
12
  end
13
13
  end
@@ -6,6 +6,6 @@ class Idempo::MalformedKeyErrorApp
6
6
  message: "The Idempotency-Key header provided was empty or malformed"
7
7
  }
8
8
  }
9
- [400, {"Content-Type" => "application/json"}, [JSON.pretty_generate(res)]]
9
+ [400, {"content-type" => "application/json"}, [JSON.pretty_generate(res)]]
10
10
  end
11
11
  end
@@ -5,11 +5,24 @@ module Idempo::RequestFingerprint
5
5
  d << rack_request.url << "\n"
6
6
  d << rack_request.request_method << "\n"
7
7
  d << rack_request.get_header("HTTP_AUTHORIZATION").to_s << "\n"
8
- while (chunk = rack_request.env["rack.input"].read(1024 * 65))
9
- d << chunk
8
+
9
+ # Under Rack 3.0 the rack.input may or may not be rewindable (this is done to support
10
+ # streaming HTTP request bodies). If we know a request body is rewindable we can read it
11
+ # out in full and add it to the request fingerprint. If the request body cannot be
12
+ # rewound, we can't really rely on it as it can be fairly large (and we want the
13
+ # downstream app to read the request body, not us).
14
+ if rack_request.env["rack.input"].respond_to?(:rewind)
15
+ read_and_rewind(rack_request.env["rack.input"], d)
10
16
  end
17
+
11
18
  Base64.strict_encode64(d.digest)
19
+ end
20
+
21
+ def self.read_and_rewind(source_io, to_destination_io)
22
+ while (chunk = source_io.read(1024 * 65))
23
+ to_destination_io << chunk
24
+ end
12
25
  ensure
13
- rack_request.env["rack.input"].rewind
26
+ source_io.rewind
14
27
  end
15
28
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Idempo
4
- VERSION = "1.2.2"
4
+ VERSION = "1.3.0"
5
5
  end
data/lib/idempo.rb CHANGED
@@ -55,14 +55,16 @@ class Idempo
55
55
  return from_persisted_response(stored_response)
56
56
  end
57
57
 
58
- status, headers, body = @app.call(env)
58
+ status, raw_headers, body = @app.call(env)
59
+ headers = downcase_keys(raw_headers)
59
60
 
60
- expires_in_seconds = (headers.delete("X-Idempo-Persist-For-Seconds") || @persist_for_seconds).to_i
61
+ expires_in_seconds = (headers.delete("x-idempo-persist-for-seconds") || @persist_for_seconds).to_i
61
62
 
62
- # In some cases `body` could respond to to_ary. In this case, we don't need to call .close on body afterwards.
63
+ # In some cases `body` could respond to to_ary. In this case, we don't need to
64
+ # call .close on the body afterwards, as it is supposed to self-close as per Rack 3.0 SPEC
63
65
  #
64
66
  # @see https://github.com/rack/rack/blob/main/SPEC.rdoc#the-body-
65
- body = body.to_ary if rack_v3? && body.respond_to?(:to_ary)
67
+ body = body.to_ary if body.respond_to?(:to_ary)
66
68
 
67
69
  if response_may_be_persisted?(status, headers, body)
68
70
  # Body is replaced with a cached version since a Rack response body is not rewindable
@@ -83,8 +85,10 @@ class Idempo
83
85
 
84
86
  private
85
87
 
86
- def rack_v3?
87
- Gem::Version.new(Rack.release) >= Gem::Version.new("3.0")
88
+ def downcase_keys(raw_headers)
89
+ raw_headers.each_with_object({}) do |(name, value), hh|
90
+ hh[name.to_s.downcase] = value
91
+ end
88
92
  end
89
93
 
90
94
  def from_persisted_response(marshaled_response)
@@ -120,17 +124,18 @@ class Idempo
120
124
  end
121
125
 
122
126
  def response_may_be_persisted?(status, headers, body)
123
- return false if headers.delete("X-Idempo-Policy") == "no-store"
127
+ return false if headers.delete("x-idempo-policy") == "no-store"
124
128
  return false unless status_may_be_persisted?(status)
125
129
  return false unless body_size_within_limit?(headers, body)
126
130
  true
127
131
  end
128
132
 
129
133
  def body_size_within_limit?(response_headers, body)
130
- return response_headers["Content-Length"].to_i <= SAVED_RESPONSE_BODY_SIZE_LIMIT if response_headers["Content-Length"]
134
+ if response_headers["content-length"]
135
+ return response_headers["content-length"].to_i <= SAVED_RESPONSE_BODY_SIZE_LIMIT
136
+ end
131
137
 
132
138
  return false unless body.is_a?(Array) # Arbitrary iterable of unknown size
133
-
134
139
  sum_of_string_bytesizes(body) <= SAVED_RESPONSE_BODY_SIZE_LIMIT
135
140
  end
136
141
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: idempo
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.2
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julik Tarkhanov
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2024-09-20 00:00:00.000000000 Z
12
+ date: 2024-11-29 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: msgpack
@@ -43,16 +43,16 @@ dependencies:
43
43
  name: rack
44
44
  requirement: !ruby/object:Gem::Requirement
45
45
  requirements:
46
- - - "~>"
46
+ - - "<"
47
47
  - !ruby/object:Gem::Version
48
- version: '3'
49
- type: :development
48
+ version: '4'
49
+ type: :runtime
50
50
  prerelease: false
51
51
  version_requirements: !ruby/object:Gem::Requirement
52
52
  requirements:
53
- - - "~>"
53
+ - - "<"
54
54
  - !ruby/object:Gem::Version
55
- version: '3'
55
+ version: '4'
56
56
  - !ruby/object:Gem::Dependency
57
57
  name: rake
58
58
  requirement: !ruby/object:Gem::Requirement
@@ -165,6 +165,20 @@ dependencies:
165
165
  - - ">="
166
166
  - !ruby/object:Gem::Version
167
167
  version: '0'
168
+ - !ruby/object:Gem::Dependency
169
+ name: appraisal
170
+ requirement: !ruby/object:Gem::Requirement
171
+ requirements:
172
+ - - ">="
173
+ - !ruby/object:Gem::Version
174
+ version: '0'
175
+ type: :development
176
+ prerelease: false
177
+ version_requirements: !ruby/object:Gem::Requirement
178
+ requirements:
179
+ - - ">="
180
+ - !ruby/object:Gem::Version
181
+ version: '0'
168
182
  description: Provides idempotency keys for Rack applications.
169
183
  email:
170
184
  - me@julik.nl
@@ -177,6 +191,7 @@ files:
177
191
  - ".gitignore"
178
192
  - ".rubocop.yml"
179
193
  - ".standard.yml"
194
+ - Appraisals
180
195
  - CHANGELOG.md
181
196
  - Gemfile
182
197
  - LICENSE.txt
@@ -220,7 +235,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
220
235
  - !ruby/object:Gem::Version
221
236
  version: '0'
222
237
  requirements: []
223
- rubygems_version: 3.3.7
238
+ rubygems_version: 3.1.6
224
239
  signing_key:
225
240
  specification_version: 4
226
241
  summary: Idempotency keys for all.