dedupe_requests 1.0.0.pre1 → 1.0.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: cabb55d1b9a54a93f4b72f552fed6d9a1c38a8343a3750444196150ce21a3ff0
4
- data.tar.gz: 6308e4ef27a02cb46bf495f1dd1fd00535e83118a0cbbe99a65b2cf91d19cf82
3
+ metadata.gz: ce2b3b3445b8652b681a60c83685ec186c53adad4c3f9011b555c2b0f7ef92c8
4
+ data.tar.gz: bf6a38fb5902632d2e226fd6b342612ec719188d35d285515cdccdceb802c6fc
5
5
  SHA512:
6
- metadata.gz: 561cc06ce39366c6a2e816c6b2f9f4404a0cafd59673e08e69809a8283e8a3ce8adec51cf2225d974a223099525c21c72ac810b437ac809e3c2bd932faf6e209
7
- data.tar.gz: f20afbdb2841ec7cf9c3ece8eb686fc0f6e3574d5dc805bf641c30692cbdbb2f1e333f6b514c7d97721a8b756ead454b366ee9a012bf7e99370f6b8cb7dcb569
6
+ metadata.gz: 2b2703eefed2889522a9e0ca6b2ce71a48d15756c3ef87e74c280c036f244dcfec62083a9fb928732a4fa665696e9dc0cbdcd1795d270adbe9095abdb5f8d090
7
+ data.tar.gz: cc079253f67157943528d0eb01d62e81f6a2b2dd3f357ba0af16c3c800f947614fa01550c6e63af9fe6c52d7239993741bf52c7ad2ee853d08b0fae202ac973d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.0 (2026-06-24)
4
+
5
+ ### Changes
6
+ - **`caller_id` is now required -- set it to a callable (a lambda) - and has no default.** The old default keyed on the `Authorization` header / session cookie — but **rotating tokens are unsafe to key on**, so that default was removed.
7
+ Configure `caller_id` with a callable that returns a stable, non-secret identifier (a user id, a JWT `sub`, an API-client id).
8
+ - When `caller_id` is unset or resolves to `nil`, de-duplication is now **skipped** for that request (it's allowed through) and a warning is logged — instead of treating all unidentified callers as one identity (which could wrongly 409 a different caller's identical request). Return a fixed string from `caller_id` to dedupe globally.
9
+
3
10
  ## 1.0.0.pre1 (2026-06-16)
4
11
 
5
12
  Initial release. See the [README](README.md) for full usage and configuration.
data/README.md CHANGED
@@ -1,10 +1,12 @@
1
1
  # dedupe_requests
2
2
 
3
+ ![Gem Version](https://img.shields.io/gem/v/dedupe_requests) [![codecov](https://codecov.io/gh/tilo/dedupe_requests/branch/main/graph/badge.svg)](https://codecov.io/gh/tilo/dedupe_requests) <!-- [![Downloads](https://img.shields.io/gem/dt/dedupe_requests)](https://rubygems.org/gems/dedupe_requests) --> [![RubyGems](https://img.shields.io/badge/RubyGems-dedupe__requests-brightgreen?logo=rubygems&logoColor=white)](https://rubygems.org/gems/dedupe_requests) [![Ruby Toolbox](https://img.shields.io/badge/Ruby%20Toolbox-dedupe__requests-brightgreen)](https://www.ruby-toolbox.com/projects/dedupe_requests)
4
+
3
5
  Automatic server-side de-duplication of inbound mutating Rails requests (POST / PUT / PATCH), with **no client-side idempotency key required**.
4
6
 
5
7
  When a client re-sends the same mutating request — because of a retry, a network timeout, a double-click, or a buggy client — a non-idempotent endpoint often turns the duplicate into a 5xx (the resource is already created or modified).
6
8
 
7
- One go-to solution for this used to be to require the client to provide a idempotency key together with the request, and then reject duplicate requests (requests that use a previous idemptotency key).
9
+ One go-to solution for this used to be to require the client to provide an idempotency key together with the request, and then reject duplicate requests (requests that use a previous idempotency key).
8
10
 
9
11
  `dedupe_requests` simplifies this, removing the requirement for providing an idempotency key, and instead auto-computes a fingerprint of each mutating request (effectively auto-generating the idempotency key on-the-fly), claims it atomically in Redis, and short-circuits a duplicate seen within a configurable window with a clean **409 Conflict** instead of letting it blow up your app.
10
12
 
@@ -21,7 +23,7 @@ This is different from the usual idempotency-key gems: the **server** computes t
21
23
  - **Key created** → first occurrence. Run the action normally.
22
24
  4. After the action: a **2xx, or a 3xx redirect** (the Post/Redirect/Get pattern is a successful create), keeps the fingerprint until the TTL expires — so a later duplicate is blocked; a **4xx/5xx or a raised exception releases** the fingerprint, so a genuine retry of a failed request is allowed.
23
25
 
24
- GET and DELETE are never deduped. Time is not part of the fingerprint — the window is the Redis TTL.
26
+ GET and DELETE are never deduped. Time is not part of the fingerprint — the time window is the Redis TTL.
25
27
 
26
28
  ## Installation
27
29
 
@@ -30,6 +32,69 @@ GET and DELETE are never deduped. Time is not part of the fingerprint — the wi
30
32
  gem "dedupe_requests"
31
33
  ```
32
34
 
35
+ ## Configuration: Who's your caller?
36
+
37
+ There is an important configuration we can not decide for you: **what identifies your caller?**
38
+
39
+ APIs typically have different callers, and you need to configure a way we can establish a `caller_id` that identifies the unique caller for `dedupe_requests` to work properly.
40
+
41
+ If you have end users, the caller is an individual user.
42
+ If you have a B2B application, the caller is probably your business partner.
43
+
44
+ Make sure to configure the `caller_id` mechanism correctly.
45
+
46
+ **There is no default — you must set `caller_id`.** If it's unset (or your callable returns `nil` for a request), `dedupe_requests` **skips de-duplication for that request** (it's allowed through) and logs a warning. That's deliberate: with no caller identity, two *different* callers sending the same payload would collide and the second would get a wrong 409. So de-duplication only kicks in once `caller_id` resolves to a value.
47
+
48
+ > **⚠️ Do not use a raw bearer token, API key, or session id as the identity.** They are secret and they rotate — so the same caller would look like different callers (silently weakening de-duplication), and you'd be leaking a secret into the dedup layer. Derive a **stable, non-secret** identifier instead: a user id, a JWT `sub`, an API-client id.
49
+
50
+ `caller_id` is a callable given the **controller** (reach the request with `controller.request`):
51
+
52
+ ```ruby
53
+ # config/initializers/dedupe_requests.rb
54
+ DedupeRequests.configure do |c|
55
+ c.caller_id = ->(controller) { controller.current_user&.id }
56
+ end
57
+ ```
58
+
59
+ Here are common ways to identify the caller — read any of them through the `controller` and return it from your `caller_id` lambda (e.g. `->(controller) { controller.request.headers['X-Client-ID'] }`):
60
+
61
+ ### Directly:
62
+ * `current_user.id` in a customer-facing application
63
+
64
+ ### Custom Headers: (only trustworthy if authenticated)
65
+ * `request.headers['X-Client-ID']`
66
+ * `request.headers['X-Organization-Id']`
67
+ * `request.headers['X-Partner-Id']`
68
+
69
+ ### Indirectly: (tokens can rotate or have a nonce)
70
+ * `request.headers["X-API-Key"]`
71
+ `partner = ApiClient.find_by!(api_key: api_key)`
72
+
73
+ * `request.headers["Authorization"]` — decode the JWT and key on a stable claim:
74
+
75
+ ```ruby
76
+ c.caller_id = ->(controller) do
77
+ claims = decode_jwt(controller.request.headers["Authorization"])
78
+ claims["sub"] # or claims["partner_id"]
79
+ end
80
+ ```
81
+
82
+ ### Infrastructure-Provided Identity
83
+
84
+ `request.headers['X-Authenticated-User']`
85
+ `request.headers['X-Forwarded-Client-Cert']`
86
+ `request.headers['X-Amzn-Oidc-Identity']`
87
+ `request.headers['X-Goog-Authenticated-User-Id']`
88
+
89
+ ### Network-Based Identity: (rare and finicky)
90
+ * `caller_ips.include?(request.remote_ip)` # if you know the IP ranges for each caller
91
+
92
+ **Only one caller? Dedupe globally.** If your API has a single caller — or you want to de-duplicate across all callers regardless of who's calling — return a fixed value so every request shares one identity (this also suppresses the no-identity warning):
93
+
94
+ ```ruby
95
+ c.caller_id = ->(_) { "global" }
96
+ ```
97
+
33
98
  ## Usage
34
99
 
35
100
  ### 1. Global defaults — an initializer
@@ -96,29 +161,17 @@ You never specify HTTP verbs per action — the route already determines the ver
96
161
 
97
162
  ### 3. Per-caller identity (`caller_id`)
98
163
 
99
- Dedup is scoped per caller, so two different users sending the same payload don't collide. `caller_id` is a callable given the **controller**, so it can read whatever identifies the caller:
100
-
101
- ```ruby
102
- DedupeRequests.configure do |c|
103
- c.caller_id = ->(controller) { controller.current_user&.id } # current_user
104
- # c.caller_id = ->(controller) { controller.request.get_header("HTTP_X_API_KEY") } # a header
105
- # c.caller_id = ->(controller) { controller.some_method } # any controller method
106
- end
107
- ```
108
-
109
- If you don't set it, the default derives identity from the `Authorization` header, falling back to a Rails session cookie — so token- and cookie-auth apps work with no configuration.
110
-
111
- > **Note:** make sure you configure `caller_id` correctly for your API. If it can't derive an identity (no `Authorization` header and no session cookie), it falls back to `nil` — and then *different* callers sending the same payload to the same endpoint are treated as one request, so the second gets a 409. That's probably not what you want, so set `caller_id` to whatever identifies a caller in your app.
164
+ ⚠️ `caller_id` scopes de-duplication per caller, and it **must be customized and properly configured for your application** — see the **Configuration** section above. There is no default; if it resolves to `nil`, that request is not de-duplicated (and a warning is logged).
112
165
 
113
166
  ## Modes and safe rollout
114
167
 
115
168
  `mode` has three states:
116
169
 
117
170
  - `:off` — disabled; no fingerprinting, no storage.
118
- - `:observe` — **shadow mode**: compute and store fingerprints and fire the metrics hooks, but never return a 409. Duplicates are detected and reported only.
171
+ - `:observe` — **shadow mode**: compute and store fingerprints and fire `on_duplicate_detected`, but never return a 409. Duplicates are detected and reported only.
119
172
  - `:enforce` — detect, store, and reject duplicates with a 409.
120
173
 
121
- Recommended rollout on a live service: enable `:observe`, build a dashboard from the `duplicate_detected` hook, watch real volume for a week or two, then flip to `:enforce`.
174
+ Recommended rollout on a live service: enable `:observe`, build a dashboard from the `on_duplicate_detected` hook, watch real volume for a week or two, then flip to `:enforce`.
122
175
 
123
176
  ## Observability
124
177
 
@@ -131,7 +184,7 @@ DedupeRequests.configure do |c|
131
184
  end
132
185
  ```
133
186
 
134
- Each hook receives `{ fingerprint:, controller:, action:, verb:, path: }`. `duplicate_detected` fires in both `observe` and `enforce`; `duplicate_rejected` only when a 409 is actually returned.
187
+ Each hook receives `{ fingerprint:, controller:, action:, verb:, path: }`. `on_duplicate_detected` fires in both `observe` and `enforce`; `on_duplicate_rejected` only when a 409 is actually returned.
135
188
 
136
189
  When tagging metrics, use only `controller`, `action`, and `verb` — these come from a small fixed set. Do **not** tag with `fingerprint` or `path`: the fingerprint is unique per request and the path usually contains record ids, so tagging with them creates a separate counter per request (a surprise bill on Datadog, or dropped series and broken dashboards). Log those instead if you need them.
137
190
 
@@ -167,7 +220,7 @@ A `409` is deliberate: well-behaved retrying clients do **not** loop on a 409 (t
167
220
  | `ttl` | `90` | Dedup window, in seconds. |
168
221
  | `digest` | `:sha256` | `:sha256` / `:sha512` / `:sha1` / `:md5`, or a callable. |
169
222
  | `namespace` | `"dedupe_requests"` | Redis key prefix (`<namespace>:dedup:<hash>`). |
170
- | `caller_id` | Authorization / session cookie | Callable **given the controller**, returns a per-caller identity (e.g. `->(c){ c.current_user&.id }`, a header via `c.request`, or any controller method). Default derives it from the Authorization header / session cookie. |
223
+ | `caller_id` | none (required) | Callable **given the controller**, returns a stable, non-secret per-caller identity (e.g. `->(c){ c.current_user&.id }`). No default if unset or it returns `nil`, that request is not de-duplicated (and a warning is logged). |
171
224
  | `fingerprint` | `nil` | Callable **given the request**, returns the fingerprint string — fully overriding the default computation. |
172
225
  | `conflict_status` | `409` | Status returned for a rejected duplicate. |
173
226
  | `conflict_body` | structured errors | JSON body for a rejected duplicate. |
data/examples/config.ru CHANGED
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  #
3
4
  # A real Rails API service that exercises every dedupe_requests feature, booted
4
5
  # as a REAL HTTP server (Puma) for examples/end_to_end_test.rb.
@@ -41,7 +42,7 @@ PAYMENT_TTL = Integer(ENV.fetch("DEDUPE_PAYMENT_TTL", "5"))
41
42
  # Test instrumentation: every hook invocation is appended here (one Puma process,
42
43
  # many threads, so a Mutex is enough), and exposed at GET /_hooks so the test can
43
44
  # assert — over HTTP — that the right hooks fired with the right data.
44
- HOOK_EVENTS = []
45
+ HOOK_EVENTS = [] # rubocop:disable Style/MutableConstant -- appended to at runtime, must stay mutable
45
46
  HOOK_MUTEX = Mutex.new
46
47
  def record_hook(event)
47
48
  HOOK_MUTEX.synchronize { HOOK_EVENTS << event }
@@ -51,10 +52,12 @@ DedupeRequests.configure do |c|
51
52
  c.redis = Redis.new(url: ENV.fetch("REDIS_URL", "redis://localhost:6379/15"))
52
53
  c.mode = ENV.fetch("DEDUPE_MODE", "enforce").to_sym
53
54
  c.ttl = GLOBAL_TTL
54
- # caller_id is left at its default, which derives the caller identity from the
55
- # request's Authorization header. The integration test sends a different
56
- # `Authorization: Bearer <token>` per simulated caller, so the same payload from
57
- # two different callers fingerprints differently and is NOT treated as a duplicate.
55
+
56
+ # ⚠️ DEMO ONLY — uses the raw Authorization header as the caller_id to keep the
57
+ # test simple. Do NOT do this in production; see the README "Configuration"
58
+ # section for how to set a stable, non-secret caller_id.
59
+ # (Overridden below when DEDUPE_CUSTOM_CALLER_ID is set.)
60
+ c.caller_id = ->(controller) { controller.request.get_header("HTTP_AUTHORIZATION") }
58
61
 
59
62
  # Record the duplicate-notification hooks. on_duplicate_detected fires whenever a
60
63
  # duplicate is seen (observe AND enforce); on_duplicate_rejected fires only when a
@@ -74,9 +77,9 @@ DedupeRequests.configure do |c|
74
77
  end
75
78
 
76
79
  # When asked, replace caller_id with a custom one that identifies the caller by
77
- # an X-Api-Key header (ignoring the Authorization header the default would use),
78
- # so the test can prove this callable is what drives the per-caller scoping. It
79
- # also records that the hook was invoked.
80
+ # an X-Api-Key header (instead of the Authorization header above), so the test
81
+ # can prove this callable is what drives the per-caller scoping. It also records
82
+ # that the hook was invoked.
80
83
  if ENV["DEDUPE_CUSTOM_CALLER_ID"] == "1"
81
84
  c.caller_id = lambda do |controller|
82
85
  key = controller.request.get_header("HTTP_X_API_KEY")
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  #
3
4
  # End-to-end test of dedupe_requests against a REAL HTTP server.
4
5
  #
@@ -64,12 +65,13 @@ GLOBAL_TTL = 2
64
65
  PAYMENT_TTL = 5
65
66
  SLOW_SECONDS = 1
66
67
 
67
- $port = ENFORCE_PORT # which booted server the request helpers talk to
68
+ # which booted server the request helpers talk to (switched per server in with_server)
69
+ $port = ENFORCE_PORT # rubocop:disable Style/GlobalVars
68
70
 
69
71
  # Simulated callers -> the Authorization header the gem's default caller_id reads.
70
72
  CALLERS = {
71
73
  alice: "Bearer token-alice",
72
- bob: "Bearer token-bob",
74
+ bob: "Bearer token-bob",
73
75
  carol: "Bearer token-carol"
74
76
  }.freeze
75
77
 
@@ -97,7 +99,7 @@ HOOK_PAYLOAD = { event: "hook-check" }.freeze
97
99
  # ---------------------------------------------------------------------------
98
100
  # tiny assertion harness
99
101
  # ---------------------------------------------------------------------------
100
- FAILURES = []
102
+ FAILURES = [] # rubocop:disable Style/MutableConstant -- appended to at runtime, must stay mutable
101
103
  def check(label, got, expected)
102
104
  ok = got == expected
103
105
  puts format(" %-64s %-26s %s", label, "got #{got.inspect}", ok ? "OK" : "FAIL (want #{expected.inspect})")
@@ -117,7 +119,7 @@ def request(method, path, as:, payload: nil, api_key: nil)
117
119
  req["Authorization"] = CALLERS.fetch(as)
118
120
  req["X-Api-Key"] = api_key if api_key
119
121
  req.body = JSON.generate(payload) if payload
120
- Net::HTTP.start(HOST, $port) { |http| http.request(req) }
122
+ Net::HTTP.start(HOST, $port) { |http| http.request(req) } # rubocop:disable Style/GlobalVars
121
123
  end
122
124
 
123
125
  def post(path, payload:, as: :alice, api_key: nil)
@@ -169,7 +171,7 @@ def with_server(port, extra_env = {})
169
171
  FAILURES << "server boot on port #{port}"
170
172
  return
171
173
  end
172
- $port = port
174
+ $port = port # rubocop:disable Style/GlobalVars
173
175
  yield
174
176
  ensure
175
177
  begin
@@ -259,8 +261,8 @@ with_server(ENFORCE_PORT) do
259
261
  check("on_duplicate_detected fired once for /hooked", hooked.count { |e| e["hook"] == "detected" }, 1)
260
262
  check("on_duplicate_rejected fired once for /hooked", hooked.count { |e| e["hook"] == "rejected" }, 1)
261
263
  detected = hooked.find { |e| e["hook"] == "detected" } || {}
262
- check("detected hook carries action=create", detected["action"], "create")
263
- check("detected hook carries verb=POST", detected["verb"], "POST")
264
+ check("detected hook carries action=create", detected["action"], "create")
265
+ check("detected hook carries verb=POST", detected["verb"], "POST")
264
266
  check("detected hook carries a fingerprint", !detected["fingerprint"].to_s.empty?, true)
265
267
  end
266
268
 
@@ -285,7 +287,7 @@ with_server(FINGERPRINT_PORT, "DEDUPE_CUSTOM_FINGERPRINT" => "1") do
285
287
  check("POST /widgets alice (different body B -> still dup)", status(post("/widgets", payload: WIDGET_GREEN)), 409) # body ignored
286
288
  check("POST /widgets bob (different caller -> still dup)", status(post("/widgets", payload: WIDGET_CREATE, as: :bob)), 409) # caller ignored
287
289
  fp = hook_events.select { |e| e["hook"] == "fingerprint" && e["path"] == "/widgets" }
288
- check("custom fingerprint hook was invoked per request", fp.size, 3)
290
+ check("custom fingerprint hook was invoked per request", fp.size, 3)
289
291
  end
290
292
 
291
293
  # ===========================================================================
@@ -12,28 +12,18 @@ module DedupeRequests
12
12
  }]
13
13
  }.freeze
14
14
 
15
- # Per-caller identity. The callable is given the CONTROLLER, so it can read
16
- # anything the controller exposes `current_user`, a helper method, or a
17
- # header via `controller.request`. Examples:
15
+ # Per-caller identity. There is NO default you MUST configure `caller_id`
16
+ # with a callable that returns a stable, non-secret identifier for the caller
17
+ # (a user id, a JWT `sub`, an API-client id). Do NOT use a raw bearer token or
18
+ # API key: it's secret and it rotates, so the same caller would look like
19
+ # different callers and de-duplication would silently weaken. The callable is
20
+ # given the CONTROLLER, so it can read `current_user`, a helper, or a header via
21
+ # `controller.request`. Examples:
18
22
  # c.caller_id = ->(controller) { controller.current_user&.id }
19
23
  # c.caller_id = ->(controller) { controller.request.get_header("HTTP_X_API_KEY") }
20
- #
21
- # The default derives identity from the request's Authorization header,
22
- # falling back to a Rails-style session cookie (so token- and cookie-auth
23
- # apps work with no configuration). It accepts either a controller or a bare
24
- # request.
25
- DEFAULT_CALLER_ID = lambda do |context|
26
- request = context.respond_to?(:request) ? context.request : context
27
- if request.respond_to?(:get_header)
28
- auth = request.get_header("HTTP_AUTHORIZATION")
29
- return auth if auth && !auth.to_s.empty?
30
- end
31
- if request.respond_to?(:cookies)
32
- request.cookies.each { |name, value| return value if name.to_s =~ /\A_.*_session\z/i }
33
- end
34
- nil
35
- end
36
-
24
+ # When `caller_id` is unset or returns nil, de-duplication is skipped for the
25
+ # request (and a warning is logged), rather than risk treating different callers
26
+ # as one.
37
27
  attr_accessor :redis, :ttl, :digest, :namespace, :caller_id, :fingerprint,
38
28
  :conflict_status, :logger,
39
29
  :on_duplicate_detected, :on_duplicate_rejected
@@ -47,7 +37,7 @@ module DedupeRequests
47
37
  @ttl = 90
48
38
  @digest = :sha256
49
39
  @namespace = "dedupe_requests"
50
- @caller_id = DEFAULT_CALLER_ID
40
+ @caller_id = nil
51
41
  @fingerprint = nil
52
42
  @conflict_status = 409
53
43
  @logger = nil
@@ -66,10 +66,28 @@ module DedupeRequests
66
66
  return
67
67
  end
68
68
 
69
+ # GET/DELETE are never deduped — bail out before resolving caller_id, so the
70
+ # caller_id callable only runs for the verbs we actually de-duplicate.
71
+ unless dedupe_requests_mutating_verb?
72
+ yield
73
+ return
74
+ end
75
+
76
+ caller_id = dedupe_requests_caller_id
77
+ # Without a caller identity, every unidentified caller would share one
78
+ # fingerprint, so two genuinely-different requests with the same body would
79
+ # collide and the second would be wrongly rejected. Skip de-duplication in
80
+ # that case (let the request through) and warn, rather than risk a false 409.
81
+ if caller_id.nil?
82
+ dedupe_requests_warn_missing_caller_id
83
+ yield
84
+ return
85
+ end
86
+
69
87
  result = dedupe_requests_guard.claim(
70
88
  request,
71
89
  ttl: dedupe_requests_ttl_for(action_name),
72
- caller_id: dedupe_requests_caller_id
90
+ caller_id: caller_id
73
91
  )
74
92
 
75
93
  case result.outcome
@@ -112,6 +130,18 @@ module DedupeRequests
112
130
  DedupeRequests.config.caller_id&.call(self)
113
131
  end
114
132
 
133
+ def dedupe_requests_mutating_verb?
134
+ DedupeRequests::MUTATING_VERBS.include?(request.request_method.to_s)
135
+ end
136
+
137
+ # Loud on purpose: a missing caller identity silently weakens de-duplication,
138
+ # so we warn on every such request (via the configured logger, else stderr).
139
+ def dedupe_requests_warn_missing_caller_id
140
+ message = "[dedupe_requests] caller_id resolved to nil for #{controller_name}##{action_name} (#{request.request_method} #{request.path}); de-duplication skipped. Configure DedupeRequests.config.caller_id."
141
+ logger = DedupeRequests.config.logger
142
+ logger ? logger.warn(message) : warn(message)
143
+ end
144
+
115
145
  def dedupe_requests_guard
116
146
  @dedupe_requests_guard ||= DedupeRequests::Guard.new(DedupeRequests.config)
117
147
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DedupeRequests
4
- VERSION = "1.0.0.pre1"
4
+ VERSION = "1.0.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dedupe_requests
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.pre1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tilo Sloboda
@@ -81,6 +81,7 @@ licenses:
81
81
  metadata:
82
82
  homepage_uri: https://github.com/tilo/dedupe_requests
83
83
  source_code_uri: https://github.com/tilo/dedupe_requests
84
+ changelog_uri: https://github.com/tilo/dedupe_requests/blob/main/CHANGELOG.md
84
85
  rubygems_mfa_required: 'false'
85
86
  rdoc_options: []
86
87
  require_paths: