rack-idempotency_key 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9358732d434938ba1444636f644cb3d8a161e95aa913e2c60e62f77b74cc635c
4
+ data.tar.gz: 9862a6bcb70d4bb24bcb85ae6eaf07265116ecdddcdede65ba6b8effcbb2155a
5
+ SHA512:
6
+ metadata.gz: 78e77d1c68c3df1f39edd5df9e2a53e169e6dd4057fcc8cb1f01ec659d16e33a7c59c290596828ab98159ee2e90f8168986f9aaf668f99b547bc4bced6bf4949
7
+ data.tar.gz: 3c5c345ccd361084b39d86b7ec4458240aaa9b02f399e53bad9e4fe06296ed7d4718ddd262c60adb00096f285a8540ecc493606a703a1257332ecccb349067aa
@@ -0,0 +1 @@
1
+ * @matteoredz
@@ -0,0 +1,20 @@
1
+ name: Lint
2
+
3
+ on:
4
+ pull_request:
5
+ types:
6
+ - opened
7
+ - reopened
8
+ - synchronize
9
+
10
+ jobs:
11
+ lint:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v2
15
+ - uses: ruby/setup-ruby@v1
16
+ with:
17
+ bundler-cache: true
18
+ ruby-version: 2.5.0
19
+ - run: bundle install
20
+ - run: bundle exec rubocop
@@ -0,0 +1,31 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ jobs:
9
+ release-please:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: google-github-actions/release-please-action@v3
13
+ id: release
14
+ with:
15
+ package-name: rack-idempotency_key
16
+ release-type: ruby
17
+ token: ${{secrets.GITHUB_TOKEN}}
18
+ version-file: "lib/rack/idempotency_key/version.rb"
19
+ - uses: actions/checkout@v3
20
+ if: ${{steps.release.outputs.release_created}}
21
+ - uses: ruby/setup-ruby@v1
22
+ with:
23
+ bundler-cache: true
24
+ ruby-version: 2.7.0
25
+ if: ${{steps.release.outputs.release_created}}
26
+ - run: bundle install
27
+ if: ${{steps.release.outputs.release_created}}
28
+ - run: ./bin/release
29
+ env:
30
+ RUBYGEMS_API_KEY: ${{secrets.RUBYGEMS_API_KEY}}
31
+ if: ${{steps.release.outputs.release_created}}
@@ -0,0 +1,25 @@
1
+ name: Test
2
+
3
+ on:
4
+ pull_request:
5
+ types:
6
+ - opened
7
+ - reopened
8
+ - synchronize
9
+
10
+ jobs:
11
+ test:
12
+ runs-on: ubuntu-latest
13
+ continue-on-error: ${{ endsWith(matrix.ruby, 'head') || matrix.ruby == 'debug' }}
14
+ strategy:
15
+ fail-fast: false
16
+ matrix:
17
+ ruby: [2.5, 2.6, 2.7, '3.0', 3.1, head]
18
+ steps:
19
+ - uses: actions/checkout@v2
20
+ - uses: ruby/setup-ruby@v1
21
+ with:
22
+ bundler-cache: true
23
+ ruby-version: ${{ matrix.ruby }}
24
+ - run: bundle install
25
+ - run: bundle exec rake
data/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ *.rbc
2
+ *.swp
3
+ /_yardoc/
4
+ /.bundle/
5
+ /.byebug_history
6
+ /.yardoc
7
+ /.ruby-version
8
+ /coverage/
9
+ /doc/
10
+ /dump.rdb
11
+ /Gemfile.lock
12
+ /pkg/
13
+ /rack-idempotency_key-*.gem
14
+ /spec/reports/
15
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,51 @@
1
+ ---
2
+ require:
3
+ - rubocop-performance
4
+ - rubocop-rake
5
+ - rubocop-rspec
6
+
7
+ AllCops:
8
+ TargetRubyVersion: 2.5
9
+
10
+ Bundler/OrderedGems:
11
+ Enabled: false
12
+
13
+ Layout/EmptyLineBetweenDefs:
14
+ Enabled: true
15
+ AllowAdjacentOneLineDefs: true
16
+
17
+ Layout/IndentationConsistency:
18
+ Enabled: true
19
+ EnforcedStyle: indented_internal_methods
20
+
21
+ Layout/LineLength:
22
+ Enabled: true
23
+ Max: 100
24
+
25
+ Metrics/AbcSize:
26
+ Enabled: false
27
+
28
+ Metrics/BlockLength:
29
+ Enabled: true
30
+ Exclude:
31
+ - 'spec/**/*'
32
+
33
+ Metrics/ClassLength:
34
+ Enabled: true
35
+ Exclude:
36
+ - 'spec/**/*'
37
+
38
+ Metrics/MethodLength:
39
+ Enabled: true
40
+ CountAsOne: ['array', 'hash']
41
+
42
+ RSpec/MultipleMemoizedHelpers:
43
+ Max: 10
44
+
45
+ Style/Documentation:
46
+ Enabled: false
47
+
48
+ Style/StringLiterals:
49
+ Enabled: true
50
+ EnforcedStyle: double_quotes
51
+ ...
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ ---
2
+ language: ruby
3
+ cache: bundler
4
+ rvm:
5
+ - 2.7.0
6
+ before_install: gem install bundler -v 2.1.4
data/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2023-01-20)
4
+
5
+
6
+ ### Miscellaneous Chores
7
+
8
+ * release as 0.1.0 ([13a2d30](https://github.com/matteoredz/rack-idempotency_key/commit/13a2d30f0ed0de82a8e94b0526c70adb6411e79e))
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at mttrss5@gmail.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [https://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: https://contributor-covenant.org
74
+ [version]: https://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
6
+
7
+ gem "byebug"
8
+ gem "mock_redis"
9
+ gem "rack-test"
10
+ gem "rake"
11
+ gem "rspec"
12
+ gem "rubocop"
13
+ gem "rubocop-performance"
14
+ gem "rubocop-rake"
15
+ gem "rubocop-rspec"
16
+ gem "simplecov"
17
+ gem "timecop"
data/README.md ADDED
@@ -0,0 +1,150 @@
1
+ # `Rack::IdempotencyKey`
2
+
3
+ A Rack Middleware implementing the idempotency design principle using the `Idempotency-Key` HTTP header. A cached response, generated by an idempotent request, can be recognized by checking for the presence of the `Idempotent-Replayed` response header.
4
+
5
+ ## What is idempotency?
6
+
7
+ Idempotency is a design principle that allows a client to safely retry API requests that might have failed due to connection issues, without causing duplication or conflicts. In other words, no matter how many times you perform an idempotent operation, the end result will always be the same.
8
+
9
+ To be idempotent, only the state of the server is considered. The response returned by each request may differ: for example, the first call of a `DELETE` will likely return a `200`, while successive ones will likely return a `404`.
10
+
11
+ `POST`, `PATCH` and `CONNECT` are the non-idempotent methods, and this gem exists to make them so.
12
+
13
+ ## Under the hood
14
+
15
+ - A valid idempotent request is cached on the server, using the `store` of choice
16
+ - A cached response expires out of the system after `24 hours`
17
+ - A response with a `400` (BadRequest) HTTP status code isn't cached
18
+
19
+ ## Installation
20
+
21
+ Add this line to your application's Gemfile:
22
+
23
+ ```ruby
24
+ gem "rack-idempotency_key"
25
+ ```
26
+
27
+ And then execute:
28
+
29
+ $ bundle install
30
+
31
+ Or install it yourself as:
32
+
33
+ $ gem install rack-idempotency_key
34
+
35
+ ## General usage
36
+
37
+ You may use this Rack Middleware in any application that conforms to the [Rack Specification](https://github.com/rack/rack/blob/main/SPEC.rdoc). Please refer to the specific application's guidelines.
38
+
39
+ ## Usage with Rails
40
+
41
+ ```ruby
42
+ # config/application.rb
43
+
44
+ module MyApp
45
+ class Application < Rails::Application
46
+ # ...
47
+
48
+ config.middleware.use(
49
+ Rack::IdempotencyKey,
50
+ store: Rack::IdempotencyKey::MemoryStore.new,
51
+ routes: [
52
+ { path: "/posts", method: "POST" },
53
+ { path: "/posts/*", method: "PATCH" }
54
+ ]
55
+ )
56
+ end
57
+ end
58
+ ```
59
+
60
+ ## Available Stores
61
+
62
+ The Store is responsible for getting and setting the response from a cache of a given idempotent request.
63
+
64
+ ### MemoryStore
65
+
66
+ This one is the default store. It caches the response in memory.
67
+
68
+ ```ruby
69
+ Rack::IdempotencyKey::MemoryStore.new
70
+
71
+ # Explicitly set the key's expiration, in seconds. The default is 86_400 (24 hours)
72
+ Rack::IdempotencyKey::MemoryStore.new(expires_in: 43_200)
73
+ ```
74
+
75
+ ### RedisStore
76
+
77
+ This one is the suggested store to use in production. It relies on the [redis gem](https://github.com/redis/redis-rb).
78
+
79
+ ```ruby
80
+ Rack::IdempotencyKey::RedisStore.new(Redis.current)
81
+
82
+ # Explicitly set the key's expiration, in seconds. The default is 86_400 (24 hours)
83
+ Rack::IdempotencyKey::RedisStore.new(Redis.current, expires_in: 43_200)
84
+ ```
85
+
86
+ Every key written to Redis will get prefixed with `idempotency_key` to avoid conflicts on shared instances.
87
+
88
+ ### Custom Store
89
+
90
+ Any object that conforms to the following interface can be used as a custom Store:
91
+
92
+ ```ruby
93
+ # @param [String] key
94
+ #
95
+ # @return [Array]
96
+ def get(key)
97
+
98
+ # @param [String] key
99
+ # @param [Array] value
100
+ #
101
+ # @return [Array]
102
+ def set(key, value)
103
+ ```
104
+
105
+ The Array returned must conform to the [Rack Specification](https://github.com/rack/rack/blob/main/SPEC.rdoc), as follows:
106
+
107
+ ```ruby
108
+ [
109
+ 200, # Response code
110
+ {}, # Response headers
111
+ [] # Response body
112
+ ]
113
+ ```
114
+
115
+ ## Idempotent Routes
116
+
117
+ To declare the routes where you want to enable idempotency, you only need to pass a `route` keyword parameter when the Middleware gets mounted.
118
+
119
+ Each route entry must be compliant with what follows:
120
+
121
+ ```ruby
122
+ routes: [
123
+ { path: "/posts", method: "POST" },
124
+ { path: "/posts/*", method: "PATCH" }
125
+ ]
126
+ ```
127
+
128
+ The `*` char is a placeholder representing a named parameter that will get converted to an any-chars regex.
129
+
130
+ ## Development
131
+
132
+ After checking out the repo, run `bin/setup` to install dependencies.
133
+ Then, run `rake test` to run the tests.
134
+ You can also run `bin/console` for an interactive prompt that will allow you to experiment.
135
+
136
+ To install this gem onto your local machine, run `bundle exec rake install`.
137
+ To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`,
138
+ which will create a git tag for the version, push git commits and tags,
139
+ and push the `.gem` file to [rubygems.org](https://rubygems.org).
140
+
141
+ ## Contributing
142
+
143
+ Bug reports and pull requests are welcome on GitHub at https://github.com/matteoredz/rack-idempotency_key.
144
+ This project is intended to be a safe, welcoming space for collaboration, and contributors are expected
145
+ to adhere to the [code of conduct](https://github.com/matteoredz/rack-idempotency_key/blob/master/CODE_OF_CONDUCT.md).
146
+
147
+ ## Code of Conduct
148
+
149
+ Everyone interacting in the `Rack::IdempotencyKey` project's codebases, issue trackers,
150
+ chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/matteoredz/rack-idempotency_key/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ end
11
+
12
+ task default: :test
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "rack/idempotency_key"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
data/bin/release ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # Setup gem credentials
4
+ mkdir -p ~/.gem
5
+ touch ~/.gem/credentials
6
+ chmod 0600 ~/.gem/credentials
7
+
8
+ cat << EOF > ~/.gem/credentials
9
+ ---
10
+ :rubygems_api_key: ${RUBYGEMS_API_KEY}
11
+ EOF
12
+
13
+ # Build and Push
14
+ gem build *.gemspec
15
+ gem push *.gem
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/rack/idempotency_key/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "rack-idempotency_key"
7
+ spec.version = Rack::IdempotencyKey::VERSION
8
+ spec.licenses = ["MIT"]
9
+ spec.authors = ["Matteo Rossi"]
10
+ spec.email = ["mttrss5@gmail.com"]
11
+ spec.summary = "A Rack Middleware implementing the idempotency principle"
12
+ spec.homepage = "https://github.com/matteoredz/rack-idempotency_key"
13
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
14
+ spec.metadata["homepage_uri"] = spec.homepage
15
+ spec.metadata["source_code_uri"] = "https://github.com/matteoredz/rack-idempotency_key"
16
+ spec.metadata["changelog_uri"] = "https://github.com/matteoredz/rack-idempotency_key/CHANGELOG.md"
17
+
18
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
19
+ `git ls-files -z`.split("\x0").reject do |f|
20
+ f.match(%r{^(test|spec|features)/})
21
+ end
22
+ end
23
+
24
+ spec.bindir = "exe"
25
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
26
+ spec.require_paths = ["lib"]
27
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ class IdempotencyKey
5
+ class IdempotentRequest
6
+ # @param [Rack::Request] request
7
+ # @param [Array] routes
8
+ def initialize(request, routes = [])
9
+ @request = request
10
+ @routes = routes
11
+ end
12
+
13
+ # Check if the `Idempotency-Key` header is present, if the HTTP request method is
14
+ # allowed and if there is any matching route whitelisted in the `routes` array.
15
+ #
16
+ # @return [Boolean]
17
+ def allowed?
18
+ idempotency_key? && allowed_method? && any_matching_route?
19
+ end
20
+
21
+ # Check if the HTTP request method is non-idempotent by design.
22
+ #
23
+ # @return [Boolean]
24
+ def allowed_method?
25
+ %w[POST PATCH CONNECT].include? request.request_method
26
+ end
27
+
28
+ # Check if there is any matching route from the `routes` input array against
29
+ # the currently requested path.
30
+ #
31
+ # @return [Boolean]
32
+ def any_matching_route?
33
+ routes.any? { |route| matching_route?(route[:path]) && matching_method?(route[:method]) }
34
+ end
35
+
36
+ # Check if the given request has the Idempotency-Key header
37
+ #
38
+ # @return [Boolean]
39
+ def idempotency_key?
40
+ request.has_header? "HTTP_IDEMPOTENCY_KEY"
41
+ end
42
+
43
+ # Fetches the Idempotency-Key header value from the request headers
44
+ #
45
+ # @return [String, nil]
46
+ def idempotency_key
47
+ request.get_header "HTTP_IDEMPOTENCY_KEY"
48
+ end
49
+
50
+ private
51
+
52
+ attr_reader :request, :routes
53
+
54
+ def matching_route?(route_path)
55
+ same_segments? segments(route_path)
56
+ end
57
+
58
+ def matching_method?(route_method)
59
+ request.request_method.casecmp(route_method).zero?
60
+ end
61
+
62
+ def path_segments
63
+ @path_segments ||= segments(request.path_info)
64
+ end
65
+
66
+ def segments(path)
67
+ path.split("/").reject(&:empty?)
68
+ end
69
+
70
+ def same_segments?(route_segments)
71
+ path_segments.each_with_index do |path_segment, index|
72
+ route_segment = Regexp.new route_segments[index].gsub("*", '\w+'), Regexp::IGNORECASE
73
+ return false unless path_segment.match?(route_segment)
74
+ end
75
+
76
+ true
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ class IdempotencyKey
5
+ class MemoryStore
6
+ def initialize(expires_in: 86_400)
7
+ @store = {}
8
+ @expires_in = expires_in
9
+ end
10
+
11
+ def get(key)
12
+ value = store[key]
13
+ return if value.nil?
14
+
15
+ if expired?(value[:added_at])
16
+ store.delete(key)
17
+ return
18
+ end
19
+
20
+ value[:value]
21
+ end
22
+
23
+ def set(key, value)
24
+ store[key] ||= { value: value, added_at: Time.now.utc }
25
+ get(key)
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :store, :expires_in
31
+
32
+ def expired?(added_at)
33
+ Time.now.utc - added_at > expires_in
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ class IdempotencyKey
5
+ class RedisStore
6
+ KEY_NAMESPACE = "idempotency_key"
7
+
8
+ def initialize(store, expires_in: 86_400)
9
+ @store = store
10
+ @expires_in = expires_in
11
+ end
12
+
13
+ def get(key)
14
+ value = store.get(namespaced_key(key))
15
+ JSON.parse(value) unless value.nil?
16
+ end
17
+
18
+ def set(key, value)
19
+ store.set(namespaced_key(key), value, nx: true, ex: expires_in)
20
+ get(key)
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :store, :expires_in
26
+
27
+ def namespaced_key(key)
28
+ "#{KEY_NAMESPACE}:#{key.split.join}"
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ class IdempotencyKey
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack/idempotency_key/version"
4
+
5
+ # Stores
6
+ require "rack/idempotency_key/memory_store"
7
+ require "rack/idempotency_key/redis_store"
8
+
9
+ # Collaborators
10
+ require "rack/idempotency_key/idempotent_request"
11
+
12
+ module Rack
13
+ class IdempotencyKey
14
+ Error = Class.new(StandardError)
15
+
16
+ def initialize(app, routes: [], store: MemoryStore.new)
17
+ @app = app
18
+ @routes = routes
19
+ @store = store
20
+ end
21
+
22
+ def call(env)
23
+ request = IdempotentRequest.new(Rack::Request.new(env), routes)
24
+ return app.call(env) unless request.allowed?
25
+
26
+ cached_response = store.get(request.idempotency_key)
27
+
28
+ if cached_response
29
+ cached_response[1]["Idempotent-Replayed"] = true
30
+ return cached_response
31
+ end
32
+
33
+ app.call(env).tap do |response|
34
+ store.set(request.idempotency_key, response) if response[0] != 400
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :app, :store, :routes
41
+ end
42
+ end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack-idempotency_key
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Matteo Rossi
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-01-20 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ - mttrss5@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".github/CODEOWNERS"
21
+ - ".github/workflows/lint.yml"
22
+ - ".github/workflows/release.yml"
23
+ - ".github/workflows/test.yml"
24
+ - ".gitignore"
25
+ - ".rspec"
26
+ - ".rubocop.yml"
27
+ - ".travis.yml"
28
+ - CHANGELOG.md
29
+ - CODE_OF_CONDUCT.md
30
+ - Gemfile
31
+ - README.md
32
+ - Rakefile
33
+ - bin/console
34
+ - bin/release
35
+ - bin/setup
36
+ - idempotency_key.gemspec
37
+ - lib/rack/idempotency_key.rb
38
+ - lib/rack/idempotency_key/idempotent_request.rb
39
+ - lib/rack/idempotency_key/memory_store.rb
40
+ - lib/rack/idempotency_key/redis_store.rb
41
+ - lib/rack/idempotency_key/version.rb
42
+ homepage: https://github.com/matteoredz/rack-idempotency_key
43
+ licenses:
44
+ - MIT
45
+ metadata:
46
+ homepage_uri: https://github.com/matteoredz/rack-idempotency_key
47
+ source_code_uri: https://github.com/matteoredz/rack-idempotency_key
48
+ changelog_uri: https://github.com/matteoredz/rack-idempotency_key/CHANGELOG.md
49
+ post_install_message:
50
+ rdoc_options: []
51
+ require_paths:
52
+ - lib
53
+ required_ruby_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: 2.5.0
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ requirements: []
64
+ rubygems_version: 3.1.2
65
+ signing_key:
66
+ specification_version: 4
67
+ summary: A Rack Middleware implementing the idempotency principle
68
+ test_files: []