data_redactor 0.15.0 → 0.16.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: dfda0b2a543fc9b415c0816dd08a3fb727c1abcfb082c4c0b6d04362c83ee4d2
4
- data.tar.gz: 6e1b528c5ce1759ebbefff1f3f2e72bdfc2b2fa2959e5c523945752bebb999f2
3
+ metadata.gz: 62a1726acf7948f272e0e04bc44dac5068063731953cc7500803f2e1f8c052ed
4
+ data.tar.gz: 64d9f0fb75ef197facaf6538a4c46c5dad11a37e196e8b37de83ba2e629aeb24
5
5
  SHA512:
6
- metadata.gz: a6a3e64351089a1b69b94a9ed33ee50eeb7339a0d359d357ce0ae6ca57c722d3b9fc5c92a4cb3b5fe8d7cb0fa40d0f3e50529e5b27226465294df78a30872714
7
- data.tar.gz: 9f4082c693c639a8f4ab211bbbf92d4c9e6d79422f1696971937d95539670171cac662d64f2a3dd05185d5946b9c79f3b6a35d4ec1c4c4376d8853ecaaea719c
6
+ metadata.gz: ae2923599925002c945f3a488e8eafac91037075a88815975a3c920cddc195732f4186fca51dd6b98485a60e84d9ef2d55fb1fa439e9d88a3203c8f4cd6b9f75
7
+ data.tar.gz: c8f6691b41a419dd5adfbe89b0399d4624f3ae06b7c5a10fdffcbaee774b2009567ba687ab65b46aa0554dcbb31408212c3ebe7bef66dfb892f19191776fa8fe
data/CHANGELOG.md CHANGED
@@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.16.0] - 2026-06-21
11
+
12
+ ### Added
13
+ - **Opt-in `#redact` refinements.** `require "data_redactor/refinements"` then
14
+ `using DataRedactor::Refinements` adds `#redact` to `String` (→
15
+ `DataRedactor.redact`) and to `Hash`/`Array` (→ `DataRedactor.redact_deep`), e.g.
16
+ `"email a@b.com".redact` and `chat.ask(user_input.redact)`. Refinements are
17
+ lexically scoped, so they never pollute the core classes globally — apps that
18
+ don't opt in are unaffected and there is no collision risk. Forwards
19
+ `only:`/`except:`/`placeholder:`; never mutates the receiver.
20
+ `DataRedactor.redact` remains the primary API.
21
+ - **Length-aware placeholder modes.** `placeholder: :length` replaces each match
22
+ with `[REDACTED:N]` and `placeholder: :tagged_length` with `[REDACTED:TAGNAME:N]`,
23
+ where `N` is the **byte length** of the redacted value. Readers can gauge what
24
+ was there without seeing it. Both compose with `only:`/`except:` and are
25
+ forwarded by `redact_deep`, `redact_json`, and the integrations. Additive —
26
+ two new values for the existing `placeholder:` keyword; no behaviour changes.
27
+ - **CI: ASan/UBSan memory-safety gate.** New job builds the matcher engine
28
+ standalone under `-fsanitize=address,undefined` and drives it over an
29
+ adversarial corpus + seeded fuzz loop (catches the `OP_EOL`-class OOB read).
30
+ - **CI: throughput-trend history + PR comment.** New job records the C/pure-Ruby
31
+ ratio over time (history in `actions/cache`, no gh-pages), posts a sticky PR
32
+ comment comparing each run to the previous point, and fails on a >10% drop.
33
+
10
34
  ## [0.15.0] - 2026-06-17
11
35
 
12
36
  ### Changed
@@ -312,7 +336,8 @@ features as 0.7.1 plus the pipeline fix.
312
336
  - `DataRedactor.redact(text)` module function returning the input with every match replaced by `[REDACTED]`.
313
337
  - RSpec suite with one example per pattern.
314
338
 
315
- [Unreleased]: https://github.com/danielefrisanco/data_redactor/compare/v0.15.0...HEAD
339
+ [Unreleased]: https://github.com/danielefrisanco/data_redactor/compare/v0.16.0...HEAD
340
+ [0.16.0]: https://github.com/danielefrisanco/data_redactor/compare/v0.15.0...v0.16.0
316
341
  [0.15.0]: https://github.com/danielefrisanco/data_redactor/compare/v0.14.1...v0.15.0
317
342
  [0.14.1]: https://github.com/danielefrisanco/data_redactor/compare/v0.14.0...v0.14.1
318
343
  [0.14.0]: https://github.com/danielefrisanco/data_redactor/compare/v0.13.0...v0.14.0
data/README.md CHANGED
@@ -6,6 +6,12 @@
6
6
 
7
7
  A Ruby gem with a C extension for high-performance regex-based redaction of sensitive data from strings.
8
8
 
9
+ > 📄 The engineering behind the v19 matching engine is written up as an experience
10
+ > report, *"The Fastest Engine Is Not the Shippable Engine: Replacing a Regex Engine
11
+ > for Data Redaction Under Production Constraints,"* currently under review at
12
+ > *Software: Practice and Experience* (Manuscript ID 7985366). Source and the
13
+ > reproducibility bundle are in [`paper/`](paper/).
14
+
9
15
  ## What it does
10
16
 
11
17
  DataRedactor scans text for sensitive data — API keys and cloud secrets, IBANs,
@@ -103,9 +109,18 @@ DataRedactor.redact(text, placeholder: :hash)
103
109
  # "user@example.com" → "[CONTACT_3d7a]"
104
110
  # "user@example.com" → "[CONTACT_3d7a]" (same every time)
105
111
  # "other@example.com" → "[CONTACT_91fc]" (different value, different hash)
112
+
113
+ # Length — embeds the byte length of the redacted value, so readers can
114
+ # gauge what was there without seeing it.
115
+ DataRedactor.redact(text, placeholder: :length)
116
+ # "user@example.com" → "[REDACTED:16]"
117
+
118
+ # Tagged length — tag name plus byte length.
119
+ DataRedactor.redact(text, placeholder: :tagged_length)
120
+ # "user@example.com" → "[REDACTED:CONTACT:16]"
106
121
  ```
107
122
 
108
- All three modes compose with `only:` and `except:`:
123
+ All modes compose with `only:` and `except:`:
109
124
 
110
125
  ```ruby
111
126
  DataRedactor.redact(text, only: :contact, placeholder: :tagged)
@@ -165,6 +180,24 @@ safe_json = DataRedactor.redact_json('{"email":"alice@example.com","count":3}')
165
180
  DataRedactor.redact_json("not json") # => JSON::ParserError
166
181
  ```
167
182
 
183
+ ### `#redact` refinements (opt-in)
184
+
185
+ Prefer `"text".redact` over `DataRedactor.redact("text")`? Opt into the refinement. It adds `#redact` to `String` (via `redact`) and to `Hash`/`Array` (via `redact_deep`) **only in the files that `using` it** — refinements are lexically scoped, so the core classes are never monkey-patched globally and there is no collision risk for apps that don't opt in. `DataRedactor.redact` remains the primary API.
186
+
187
+ ```ruby
188
+ require "data_redactor/refinements"
189
+ using DataRedactor::Refinements
190
+
191
+ "email alice@example.com".redact # => "email [REDACTED]"
192
+ { token: "AKIAIOSFODNN7EXAMPLE" }.redact # => { token: "[REDACTED]" }
193
+ ["a@b.com", 3].redact # => ["[REDACTED]", 3]
194
+
195
+ # Handy right before sending text to an LLM:
196
+ chat.ask(user_input.redact)
197
+ ```
198
+
199
+ `#redact` forwards `only:`/`except:`/`placeholder:` and never mutates the receiver. Without `using DataRedactor::Refinements` in the current file, `#redact` is not defined.
200
+
168
201
  ### Custom patterns
169
202
 
170
203
  Teams often have internal IDs that the gem can't ship. Register them at boot — or at runtime from any thread (registration is thread-safe, see [Thread safety](#thread-safety)):
@@ -309,6 +342,24 @@ safe_response = DataRedactor::Integrations::OpenAI.redact_response(response)
309
342
 
310
343
  `content` may be a plain String or an array of content blocks/parts (`{ type: "text", text: "..." }`) — only the `text` of `text` blocks is redacted; image and other block types pass through untouched. For Claude, a top-level `system:` String is also redacted; for OpenAI, a `{ role: "system" }` message in the array is redacted like any other. Pass a bare `messages` array or the whole request Hash (with a `messages` key) — either works.
311
344
 
345
+ ### RubyLLM
346
+
347
+ [RubyLLM](https://rubyllm.com) is a unified Ruby client for every major LLM provider — and a perfect match for `data_redactor`: anything you send to a model is exactly the kind of free text that leaks secrets and PII. Because RubyLLM takes plain strings, you can scrub them with `DataRedactor.redact` before they leave the process — no extra integration required:
348
+
349
+ ```ruby
350
+ require "ruby_llm"
351
+ require "data_redactor"
352
+
353
+ chat = RubyLLM.chat(model: "claude-opus-4-8")
354
+ chat.with_instructions(DataRedactor.redact("You are a support agent for ACME Corp."))
355
+
356
+ user_input = "My card is 4111 1111 1111 1111 and my email is alice@example.com"
357
+ chat.ask(DataRedactor.redact(user_input))
358
+ # the model receives: "My card is [REDACTED] and my email is [REDACTED]"
359
+ ```
360
+
361
+ Wrap each prompt (and any `with_instructions` system prompt) in `DataRedactor.redact` before passing it to `ask`. This is a per-call step you opt into — RubyLLM does not yet expose a request hook for automatic, transparent redaction of every outbound call ([crmne/ruby_llm#765](https://github.com/crmne/ruby_llm/issues/765) tracks the connection-middleware hook that would enable it).
362
+
312
363
  ## Detected patterns (89 total)
313
364
 
314
365
  The table below is a representative sample. Use `DataRedactor.pattern_names` for the canonical, machine-readable list — it stays in sync with the C extension automatically.
@@ -68,9 +68,11 @@ void Init_data_redactor(void) {
68
68
  rb_define_const(mDataRedactor, "BUILTIN_PATTERN_BOUNDARY", rb_ary_freeze(builtin_boundary));
69
69
 
70
70
  /* Placeholder mode constants. */
71
- rb_define_const(mDataRedactor, "PH_MODE_PLAIN", INT2NUM(PLACEHOLDER_MODE_PLAIN));
72
- rb_define_const(mDataRedactor, "PH_MODE_TAGGED", INT2NUM(PLACEHOLDER_MODE_TAGGED));
73
- rb_define_const(mDataRedactor, "PH_MODE_HASH", INT2NUM(PLACEHOLDER_MODE_HASH));
71
+ rb_define_const(mDataRedactor, "PH_MODE_PLAIN", INT2NUM(PLACEHOLDER_MODE_PLAIN));
72
+ rb_define_const(mDataRedactor, "PH_MODE_TAGGED", INT2NUM(PLACEHOLDER_MODE_TAGGED));
73
+ rb_define_const(mDataRedactor, "PH_MODE_HASH", INT2NUM(PLACEHOLDER_MODE_HASH));
74
+ rb_define_const(mDataRedactor, "PH_MODE_LENGTH", INT2NUM(PLACEHOLDER_MODE_LENGTH));
75
+ rb_define_const(mDataRedactor, "PH_MODE_TAGGED_LENGTH", INT2NUM(PLACEHOLDER_MODE_TAGGED_LENGTH));
74
76
 
75
77
  /* Tag bitmask values used by the Ruby wrapper to build only/except masks. */
76
78
  rb_define_const(mDataRedactor, "TAG_CREDENTIALS", INT2NUM(TAG_CREDENTIALS));
@@ -20,6 +20,10 @@ size_t write_placeholder(char *buf, const placeholder_t *ph,
20
20
  unsigned int h = djb2(match, match_len) & 0xFFFF;
21
21
  return (size_t)sprintf(buf, "[%s_%04x]", ph->str, h);
22
22
  }
23
+ case PLACEHOLDER_MODE_LENGTH:
24
+ return (size_t)sprintf(buf, "[REDACTED:%zu]", match_len);
25
+ case PLACEHOLDER_MODE_TAGGED_LENGTH:
26
+ return (size_t)sprintf(buf, "[REDACTED:%s:%zu]", ph->str, match_len);
23
27
  default: /* PLACEHOLDER_MODE_PLAIN */
24
28
  {
25
29
  size_t len = strlen(ph->str);
@@ -29,11 +33,17 @@ size_t write_placeholder(char *buf, const placeholder_t *ph,
29
33
  }
30
34
  }
31
35
 
36
+ /* Widest decimal a size_t byte-length can print to (UINT64_MAX is 20 digits). */
37
+ #define MAX_LEN_DIGITS 20
38
+
32
39
  size_t max_placeholder_len(const placeholder_t *ph) {
33
40
  size_t tag_len = strlen(ph->str);
34
41
  switch (ph->mode) {
35
42
  case PLACEHOLDER_MODE_TAGGED: return 2 + 9 + tag_len + 1; /* "[REDACTED:" + tag + "]" */
36
43
  case PLACEHOLDER_MODE_HASH: return 1 + tag_len + 1 + 4 + 1; /* "[" + tag + "_" + 4hex + "]" */
44
+ case PLACEHOLDER_MODE_LENGTH: return 10 + MAX_LEN_DIGITS + 1; /* "[REDACTED:" + digits + "]" */
45
+ case PLACEHOLDER_MODE_TAGGED_LENGTH:
46
+ return 10 + tag_len + 1 + MAX_LEN_DIGITS + 1; /* "[REDACTED:" + tag + ":" + digits + "]" */
37
47
  default: return tag_len;
38
48
  }
39
49
  }
@@ -3,9 +3,11 @@
3
3
 
4
4
  #include <stddef.h>
5
5
 
6
- #define PLACEHOLDER_MODE_PLAIN 0 /* use ph.str verbatim */
7
- #define PLACEHOLDER_MODE_TAGGED 1 /* "[REDACTED:TAGNAME]" */
8
- #define PLACEHOLDER_MODE_HASH 2 /* "[TAGNAME_xxxx]" (4-hex djb2 suffix) */
6
+ #define PLACEHOLDER_MODE_PLAIN 0 /* use ph.str verbatim */
7
+ #define PLACEHOLDER_MODE_TAGGED 1 /* "[REDACTED:TAGNAME]" */
8
+ #define PLACEHOLDER_MODE_HASH 2 /* "[TAGNAME_xxxx]" (4-hex djb2 suffix) */
9
+ #define PLACEHOLDER_MODE_LENGTH 3 /* "[REDACTED:16]" (byte-length) */
10
+ #define PLACEHOLDER_MODE_TAGGED_LENGTH 4 /* "[REDACTED:TAGNAME:16]" */
9
11
 
10
12
  typedef struct {
11
13
  int mode;
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "data_redactor"
4
+
5
+ module DataRedactor
6
+ # Opt-in refinements that add a `#redact` method to `String`, `Hash`, and
7
+ # `Array` as sugar over {DataRedactor.redact} / {DataRedactor.redact_deep}.
8
+ #
9
+ # Refinements are lexically scoped: `#redact` exists only in files that
10
+ # `using DataRedactor::Refinements`, so loading this file never pollutes the
11
+ # core classes globally. Apps that don't opt in are unaffected, and there is
12
+ # no collision risk with other libraries' `String#redact`.
13
+ #
14
+ # `DataRedactor.redact` remains the primary API; this is convenience only.
15
+ #
16
+ # @example
17
+ # require "data_redactor/refinements"
18
+ # using DataRedactor::Refinements
19
+ #
20
+ # "email alice@example.com".redact #=> "email [REDACTED]"
21
+ # { token: "AKIAIOSFODNN7EXAMPLE" }.redact #=> { token: "[REDACTED]" }
22
+ # chat.ask(user_input.redact) # scrub before sending to an LLM
23
+ module Refinements
24
+ refine String do
25
+ # Redact this String via {DataRedactor.redact}. Returns a new String; the
26
+ # receiver is not mutated.
27
+ #
28
+ # @param only forwarded to {DataRedactor.redact}
29
+ # @param except forwarded to {DataRedactor.redact}
30
+ # @param placeholder forwarded to {DataRedactor.redact}
31
+ # @return [String] the redacted copy.
32
+ # @example
33
+ # "ssn 123-45-6789".redact #=> "ssn [REDACTED]"
34
+ def redact(only: nil, except: nil, placeholder: DataRedactor::PLACEHOLDER_DEFAULT)
35
+ DataRedactor.redact(self, only: only, except: except, placeholder: placeholder)
36
+ end
37
+ end
38
+
39
+ refine Hash do
40
+ # Deep-redact this Hash's String values via {DataRedactor.redact_deep}.
41
+ # Returns a deep copy; the receiver is not mutated and keys are untouched.
42
+ #
43
+ # @param only forwarded to {DataRedactor.redact_deep}
44
+ # @param except forwarded to {DataRedactor.redact_deep}
45
+ # @param placeholder forwarded to {DataRedactor.redact_deep}
46
+ # @return [Hash] a deep copy with String leaves redacted.
47
+ # @example
48
+ # { email: "a@b.com" }.redact #=> { email: "[REDACTED]" }
49
+ def redact(only: nil, except: nil, placeholder: DataRedactor::PLACEHOLDER_DEFAULT)
50
+ DataRedactor.redact_deep(self, only: only, except: except, placeholder: placeholder)
51
+ end
52
+ end
53
+
54
+ refine Array do
55
+ # Deep-redact this Array's String elements via {DataRedactor.redact_deep}.
56
+ # Returns a deep copy; the receiver is not mutated.
57
+ #
58
+ # @param only forwarded to {DataRedactor.redact_deep}
59
+ # @param except forwarded to {DataRedactor.redact_deep}
60
+ # @param placeholder forwarded to {DataRedactor.redact_deep}
61
+ # @return [Array] a deep copy with String leaves redacted.
62
+ # @example
63
+ # ["a@b.com", 3].redact #=> ["[REDACTED]", 3]
64
+ def redact(only: nil, except: nil, placeholder: DataRedactor::PLACEHOLDER_DEFAULT)
65
+ DataRedactor.redact_deep(self, only: only, except: except, placeholder: placeholder)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -1,4 +1,4 @@
1
1
  module DataRedactor
2
2
  # Current gem version. Follows {https://semver.org Semantic Versioning 2.0.0}.
3
- VERSION = "0.15.0"
3
+ VERSION = "0.16.0"
4
4
  end
data/lib/data_redactor.rb CHANGED
@@ -23,8 +23,10 @@ require_relative "data_redactor/name_pattern"
23
23
  #
24
24
  # @example Custom placeholder
25
25
  # DataRedactor.redact(text, placeholder: "***")
26
- # DataRedactor.redact(text, placeholder: :tagged) # => "[REDACTED:CONTACT]"
27
- # DataRedactor.redact(text, placeholder: :hash) # => "[CONTACT_a3f9]"
26
+ # DataRedactor.redact(text, placeholder: :tagged) # => "[REDACTED:CONTACT]"
27
+ # DataRedactor.redact(text, placeholder: :hash) # => "[CONTACT_a3f9]"
28
+ # DataRedactor.redact(text, placeholder: :length) # => "[REDACTED:16]"
29
+ # DataRedactor.redact(text, placeholder: :tagged_length) # => "[REDACTED:CONTACT:16]"
28
30
  #
29
31
  # @example Audit / dry-run
30
32
  # DataRedactor.scan(text)
@@ -125,12 +127,15 @@ module DataRedactor
125
127
  # and/or pattern name(s).
126
128
  # @param except [Symbol, String, Array, nil] exclude the given tag(s)
127
129
  # and/or pattern name(s). May be combined with +only:+.
128
- # @param placeholder [String, :tagged, :hash] replacement strategy.
129
- # A String is used verbatim. +:tagged+ produces +[REDACTED:TAGNAME]+.
130
- # +:hash+ produces a deterministic +[TAGNAME_xxxx]+ token (4-hex djb2)
131
- # so the same input value always maps to the same token.
130
+ # @param placeholder [String, :tagged, :hash, :length, :tagged_length]
131
+ # replacement strategy. A String is used verbatim. +:tagged+ produces
132
+ # +[REDACTED:TAGNAME]+. +:hash+ produces a deterministic +[TAGNAME_xxxx]+
133
+ # token (4-hex djb2) so the same input value always maps to the same token.
134
+ # +:length+ produces +[REDACTED:N]+ and +:tagged_length+ produces
135
+ # +[REDACTED:TAGNAME:N]+, where +N+ is the byte length of the redacted value.
132
136
  # @return [String] a new string with every match replaced.
133
- # @raise [ArgumentError] if +placeholder:+ is not a String/:tagged/:hash.
137
+ # @raise [ArgumentError] if +placeholder:+ is not a String/:tagged/:hash/
138
+ # :length/:tagged_length.
134
139
  # @raise [UnknownTagError] if any Symbol in +only:+/+except:+ is not in {TAGS}.
135
140
  # @raise [UnknownPatternError] if any String in +only:+/+except:+ is not in {pattern_names}.
136
141
  #
@@ -194,7 +199,7 @@ module DataRedactor
194
199
  # Any type is accepted; non-String scalars are returned as-is.
195
200
  # @param only [Symbol, String, Array, nil] forwarded to {redact}.
196
201
  # @param except [Symbol, String, Array, nil] forwarded to {redact}.
197
- # @param placeholder [String, :tagged, :hash] forwarded to {redact}.
202
+ # @param placeholder [String, :tagged, :hash, :length, :tagged_length] forwarded to {redact}.
198
203
  # @return [Hash, Array, String, Object] a new structure of the same shape
199
204
  # with all String leaves redacted.
200
205
  # @raise [ArgumentError] if the structure contains a circular reference.
@@ -217,7 +222,7 @@ module DataRedactor
217
222
  # @param json_string [String] valid JSON input.
218
223
  # @param only [Symbol, String, Array, nil] forwarded to {redact}.
219
224
  # @param except [Symbol, String, Array, nil] forwarded to {redact}.
220
- # @param placeholder [String, :tagged, :hash] forwarded to {redact}.
225
+ # @param placeholder [String, :tagged, :hash, :length, :tagged_length] forwarded to {redact}.
221
226
  # @return [String] a JSON string with all String values redacted.
222
227
  # @raise [JSON::ParserError] if +json_string+ is not valid JSON.
223
228
  #
@@ -425,17 +430,20 @@ module DataRedactor
425
430
  # Translate the user-facing +placeholder:+ value into the +(mode_int, str)+
426
431
  # pair the C layer expects.
427
432
  #
428
- # @param placeholder [String, :tagged, :hash]
433
+ # @param placeholder [String, :tagged, :hash, :length, :tagged_length]
429
434
  # @return [Array(Integer, String)]
430
435
  # @raise [ArgumentError] if +placeholder+ is none of the accepted values.
431
436
  def resolve_placeholder(placeholder)
432
437
  case placeholder
433
- when :tagged then [PH_MODE_TAGGED, ""]
434
- when :hash then [PH_MODE_HASH, ""]
435
- when String then [PH_MODE_PLAIN, placeholder]
438
+ when :tagged then [PH_MODE_TAGGED, ""]
439
+ when :hash then [PH_MODE_HASH, ""]
440
+ when :length then [PH_MODE_LENGTH, ""]
441
+ when :tagged_length then [PH_MODE_TAGGED_LENGTH, ""]
442
+ when String then [PH_MODE_PLAIN, placeholder]
436
443
  else
437
444
  raise ArgumentError,
438
- "placeholder must be a String, :tagged, or :hash got #{placeholder.inspect}"
445
+ "placeholder must be a String, :tagged, :hash, :length, or :tagged_length " \
446
+ "— got #{placeholder.inspect}"
439
447
  end
440
448
  end
441
449
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: data_redactor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.0
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniele Frisanco
@@ -145,6 +145,7 @@ files:
145
145
  - lib/data_redactor/integrations/rack.rb
146
146
  - lib/data_redactor/integrations/rails.rb
147
147
  - lib/data_redactor/name_pattern.rb
148
+ - lib/data_redactor/refinements.rb
148
149
  - lib/data_redactor/version.rb
149
150
  homepage: https://github.com/danielefrisanco/data_redactor
150
151
  licenses: