launchdarkly-server-sdk 8.8.3-java

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.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +13 -0
  3. data/README.md +61 -0
  4. data/lib/launchdarkly-server-sdk.rb +1 -0
  5. data/lib/ldclient-rb/cache_store.rb +45 -0
  6. data/lib/ldclient-rb/config.rb +658 -0
  7. data/lib/ldclient-rb/context.rb +565 -0
  8. data/lib/ldclient-rb/evaluation_detail.rb +387 -0
  9. data/lib/ldclient-rb/events.rb +642 -0
  10. data/lib/ldclient-rb/expiring_cache.rb +77 -0
  11. data/lib/ldclient-rb/flags_state.rb +88 -0
  12. data/lib/ldclient-rb/impl/big_segments.rb +117 -0
  13. data/lib/ldclient-rb/impl/broadcaster.rb +78 -0
  14. data/lib/ldclient-rb/impl/context.rb +96 -0
  15. data/lib/ldclient-rb/impl/context_filter.rb +166 -0
  16. data/lib/ldclient-rb/impl/data_source.rb +188 -0
  17. data/lib/ldclient-rb/impl/data_store.rb +109 -0
  18. data/lib/ldclient-rb/impl/dependency_tracker.rb +102 -0
  19. data/lib/ldclient-rb/impl/diagnostic_events.rb +129 -0
  20. data/lib/ldclient-rb/impl/evaluation_with_hook_result.rb +34 -0
  21. data/lib/ldclient-rb/impl/evaluator.rb +539 -0
  22. data/lib/ldclient-rb/impl/evaluator_bucketing.rb +86 -0
  23. data/lib/ldclient-rb/impl/evaluator_helpers.rb +50 -0
  24. data/lib/ldclient-rb/impl/evaluator_operators.rb +131 -0
  25. data/lib/ldclient-rb/impl/event_sender.rb +100 -0
  26. data/lib/ldclient-rb/impl/event_summarizer.rb +68 -0
  27. data/lib/ldclient-rb/impl/event_types.rb +136 -0
  28. data/lib/ldclient-rb/impl/flag_tracker.rb +58 -0
  29. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +170 -0
  30. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +300 -0
  31. data/lib/ldclient-rb/impl/integrations/file_data_source.rb +229 -0
  32. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +306 -0
  33. data/lib/ldclient-rb/impl/integrations/test_data/test_data_source.rb +40 -0
  34. data/lib/ldclient-rb/impl/migrations/migrator.rb +287 -0
  35. data/lib/ldclient-rb/impl/migrations/tracker.rb +136 -0
  36. data/lib/ldclient-rb/impl/model/clause.rb +45 -0
  37. data/lib/ldclient-rb/impl/model/feature_flag.rb +254 -0
  38. data/lib/ldclient-rb/impl/model/preprocessed_data.rb +64 -0
  39. data/lib/ldclient-rb/impl/model/segment.rb +132 -0
  40. data/lib/ldclient-rb/impl/model/serialization.rb +72 -0
  41. data/lib/ldclient-rb/impl/repeating_task.rb +46 -0
  42. data/lib/ldclient-rb/impl/sampler.rb +25 -0
  43. data/lib/ldclient-rb/impl/store_client_wrapper.rb +141 -0
  44. data/lib/ldclient-rb/impl/store_data_set_sorter.rb +55 -0
  45. data/lib/ldclient-rb/impl/unbounded_pool.rb +34 -0
  46. data/lib/ldclient-rb/impl/util.rb +95 -0
  47. data/lib/ldclient-rb/impl.rb +13 -0
  48. data/lib/ldclient-rb/in_memory_store.rb +100 -0
  49. data/lib/ldclient-rb/integrations/consul.rb +45 -0
  50. data/lib/ldclient-rb/integrations/dynamodb.rb +92 -0
  51. data/lib/ldclient-rb/integrations/file_data.rb +108 -0
  52. data/lib/ldclient-rb/integrations/redis.rb +98 -0
  53. data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +663 -0
  54. data/lib/ldclient-rb/integrations/test_data.rb +213 -0
  55. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +246 -0
  56. data/lib/ldclient-rb/integrations.rb +6 -0
  57. data/lib/ldclient-rb/interfaces.rb +974 -0
  58. data/lib/ldclient-rb/ldclient.rb +822 -0
  59. data/lib/ldclient-rb/memoized_value.rb +32 -0
  60. data/lib/ldclient-rb/migrations.rb +230 -0
  61. data/lib/ldclient-rb/non_blocking_thread_pool.rb +46 -0
  62. data/lib/ldclient-rb/polling.rb +102 -0
  63. data/lib/ldclient-rb/reference.rb +295 -0
  64. data/lib/ldclient-rb/requestor.rb +102 -0
  65. data/lib/ldclient-rb/simple_lru_cache.rb +25 -0
  66. data/lib/ldclient-rb/stream.rb +196 -0
  67. data/lib/ldclient-rb/util.rb +132 -0
  68. data/lib/ldclient-rb/version.rb +3 -0
  69. data/lib/ldclient-rb.rb +27 -0
  70. metadata +400 -0
@@ -0,0 +1,88 @@
1
+ require 'json'
2
+
3
+ module LaunchDarkly
4
+ #
5
+ # A snapshot of the state of all feature flags with regard to a specific context, generated by
6
+ # calling the {LDClient#all_flags_state}. Serializing this object to JSON using
7
+ # `JSON.generate` (or the `to_json` method) will produce the appropriate data structure for
8
+ # bootstrapping the LaunchDarkly JavaScript client.
9
+ #
10
+ class FeatureFlagsState
11
+ def initialize(valid)
12
+ @flag_values = {}
13
+ @flag_metadata = {}
14
+ @valid = valid
15
+ end
16
+
17
+ # Used internally to build the state map.
18
+ # @private
19
+ def add_flag(flag_state, with_reasons, details_only_if_tracked)
20
+ key = flag_state[:key]
21
+ @flag_values[key] = flag_state[:value]
22
+ meta = {}
23
+
24
+ omit_details = false
25
+ if details_only_if_tracked
26
+ if !flag_state[:trackEvents] && !flag_state[:trackReason] && !(flag_state[:debugEventsUntilDate] && flag_state[:debugEventsUntilDate] > Impl::Util::current_time_millis)
27
+ omit_details = true
28
+ end
29
+ end
30
+
31
+ reason = (!with_reasons and !flag_state[:trackReason]) ? nil : flag_state[:reason]
32
+
33
+ if !reason.nil? && !omit_details
34
+ meta[:reason] = reason
35
+ end
36
+
37
+ unless omit_details
38
+ meta[:version] = flag_state[:version]
39
+ end
40
+
41
+ meta[:prerequisites] = flag_state[:prerequisites] unless flag_state[:prerequisites].nil? || flag_state[:prerequisites].empty?
42
+ meta[:variation] = flag_state[:variation] unless flag_state[:variation].nil?
43
+ meta[:trackEvents] = true if flag_state[:trackEvents]
44
+ meta[:trackReason] = true if flag_state[:trackReason]
45
+ meta[:debugEventsUntilDate] = flag_state[:debugEventsUntilDate] if flag_state[:debugEventsUntilDate]
46
+ @flag_metadata[key] = meta
47
+ end
48
+
49
+ # Returns true if this object contains a valid snapshot of feature flag state, or false if the
50
+ # state could not be computed (for instance, because the client was offline or there was no context).
51
+ def valid?
52
+ @valid
53
+ end
54
+
55
+ # Returns the value of an individual feature flag at the time the state was recorded.
56
+ # Returns nil if the flag returned the default value, or if there was no such flag.
57
+ def flag_value(key)
58
+ @flag_values[key]
59
+ end
60
+
61
+ # Returns a map of flag keys to flag values. If a flag would have evaluated to the default value,
62
+ # its value will be nil.
63
+ #
64
+ # Do not use this method if you are passing data to the front end to "bootstrap" the JavaScript client.
65
+ # Instead, use as_json.
66
+ def values_map
67
+ @flag_values
68
+ end
69
+
70
+ # Returns a hash that can be used as a JSON representation of the entire state map, in the format
71
+ # used by the LaunchDarkly JavaScript SDK. Use this method if you are passing data to the front end
72
+ # in order to "bootstrap" the JavaScript client.
73
+ #
74
+ # Do not rely on the exact shape of this data, as it may change in future to support the needs of
75
+ # the JavaScript client.
76
+ def as_json(*) # parameter is unused, but may be passed if we're using the json gem
77
+ ret = @flag_values.clone
78
+ ret['$flagsState'] = @flag_metadata
79
+ ret['$valid'] = @valid
80
+ ret
81
+ end
82
+
83
+ # Same as as_json, but converts the JSON structure into a string.
84
+ def to_json(*a)
85
+ as_json.to_json(*a)
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,117 @@
1
+ require "ldclient-rb/config"
2
+ require "ldclient-rb/expiring_cache"
3
+ require "ldclient-rb/impl/repeating_task"
4
+ require "ldclient-rb/interfaces"
5
+ require "ldclient-rb/util"
6
+
7
+ require "digest"
8
+
9
+ module LaunchDarkly
10
+ module Impl
11
+ BigSegmentMembershipResult = Struct.new(:membership, :status)
12
+
13
+ class BigSegmentStoreManager
14
+ # use this as a singleton whenever a membership query returns nil; it's safe to reuse it because
15
+ # we will never modify the membership properties after they're queried
16
+ EMPTY_MEMBERSHIP = {}
17
+
18
+ def initialize(big_segments_config, logger)
19
+ @store = big_segments_config.store
20
+ @stale_after_millis = big_segments_config.stale_after * 1000
21
+ @status_provider = BigSegmentStoreStatusProviderImpl.new(-> { get_status })
22
+ @logger = logger
23
+ @last_status = nil
24
+
25
+ unless @store.nil?
26
+ @cache = ExpiringCache.new(big_segments_config.context_cache_size, big_segments_config.context_cache_time)
27
+ @poll_worker = RepeatingTask.new(big_segments_config.status_poll_interval, 0, -> { poll_store_and_update_status }, logger)
28
+ @poll_worker.start
29
+ end
30
+ end
31
+
32
+ attr_reader :status_provider
33
+
34
+ def stop
35
+ @poll_worker.stop unless @poll_worker.nil?
36
+ @store.stop unless @store.nil?
37
+ end
38
+
39
+ def get_context_membership(context_key)
40
+ return nil unless @store
41
+ membership = @cache[context_key]
42
+ unless membership
43
+ begin
44
+ membership = @store.get_membership(BigSegmentStoreManager.hash_for_context_key(context_key))
45
+ membership = EMPTY_MEMBERSHIP if membership.nil?
46
+ @cache[context_key] = membership
47
+ rescue => e
48
+ LaunchDarkly::Util.log_exception(@logger, "Big Segment store membership query returned error", e)
49
+ return BigSegmentMembershipResult.new(nil, BigSegmentsStatus::STORE_ERROR)
50
+ end
51
+ end
52
+ poll_store_and_update_status unless @last_status
53
+ unless @last_status.available
54
+ return BigSegmentMembershipResult.new(membership, BigSegmentsStatus::STORE_ERROR)
55
+ end
56
+ BigSegmentMembershipResult.new(membership, @last_status.stale ? BigSegmentsStatus::STALE : BigSegmentsStatus::HEALTHY)
57
+ end
58
+
59
+ def get_status
60
+ @last_status || poll_store_and_update_status
61
+ end
62
+
63
+ def poll_store_and_update_status
64
+ new_status = Interfaces::BigSegmentStoreStatus.new(false, false) # default to "unavailable" if we don't get a new status below
65
+ unless @store.nil?
66
+ begin
67
+ metadata = @store.get_metadata
68
+ new_status = Interfaces::BigSegmentStoreStatus.new(true, !metadata || stale?(metadata.last_up_to_date))
69
+ rescue => e
70
+ LaunchDarkly::Util.log_exception(@logger, "Big Segment store status query returned error", e)
71
+ end
72
+ end
73
+ @last_status = new_status
74
+ @status_provider.update_status(new_status)
75
+
76
+ new_status
77
+ end
78
+
79
+ def stale?(timestamp)
80
+ !timestamp || ((Impl::Util.current_time_millis - timestamp) >= @stale_after_millis)
81
+ end
82
+
83
+ def self.hash_for_context_key(context_key)
84
+ Digest::SHA256.base64digest(context_key)
85
+ end
86
+ end
87
+
88
+ #
89
+ # Default implementation of the BigSegmentStoreStatusProvider interface.
90
+ #
91
+ # There isn't much to this because the real implementation is in BigSegmentStoreManager - we pass in a lambda
92
+ # that allows us to get the current status from that class. Also, the standard Observer methods such as
93
+ # add_observer are provided for us because BigSegmentStoreStatusProvider mixes in Observer, so all we need to
94
+ # to do make notifications happen is to call the Observer methods "changed" and "notify_observers".
95
+ #
96
+ class BigSegmentStoreStatusProviderImpl
97
+ include LaunchDarkly::Interfaces::BigSegmentStoreStatusProvider
98
+
99
+ def initialize(status_fn)
100
+ @status_fn = status_fn
101
+ @last_status = nil
102
+ end
103
+
104
+ def status
105
+ @status_fn.call
106
+ end
107
+
108
+ def update_status(new_status)
109
+ if !@last_status || new_status != @last_status
110
+ @last_status = new_status
111
+ changed
112
+ notify_observers(new_status)
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LaunchDarkly
4
+ module Impl
5
+
6
+ #
7
+ # A generic mechanism for registering event listeners and broadcasting
8
+ # events to them.
9
+ #
10
+ # The SDK maintains an instance of this for each available type of listener
11
+ # (flag change, data store status, etc.). They are all intended to share a
12
+ # single executor service; notifications are submitted individually to this
13
+ # service for each listener.
14
+ #
15
+ class Broadcaster
16
+ def initialize(executor, logger)
17
+ @listeners = Concurrent::Set.new
18
+ @executor = executor
19
+ @logger = logger
20
+ end
21
+
22
+ #
23
+ # Register a listener to this broadcaster.
24
+ #
25
+ # @param listener [#update]
26
+ #
27
+ def add_listener(listener)
28
+ unless listener.respond_to? :update
29
+ logger.warn("listener (#{listener.class}) does not respond to :update method. ignoring as registered listener")
30
+ return
31
+ end
32
+
33
+ listeners.add(listener)
34
+ end
35
+
36
+ #
37
+ # Removes a registered listener from this broadcaster.
38
+ #
39
+ def remove_listener(listener)
40
+ listeners.delete(listener)
41
+ end
42
+
43
+ def has_listeners?
44
+ !listeners.empty?
45
+ end
46
+
47
+ #
48
+ # Broadcast the provided event to all registered listeners.
49
+ #
50
+ # Each listener will be notified using the broadcasters executor. This
51
+ # method is non-blocking.
52
+ #
53
+ def broadcast(event)
54
+ listeners.each do |listener|
55
+ executor.post do
56
+ begin
57
+ listener.update(event)
58
+ rescue StandardError => e
59
+ logger.error("listener (#{listener.class}) raised exception (#{e}) processing event (#{event.class})")
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+
66
+ private
67
+
68
+ # @return [Concurrent::ThreadPoolExecutor]
69
+ attr_reader :executor
70
+
71
+ # @return [Logger]
72
+ attr_reader :logger
73
+
74
+ # @return [Concurrent::Set]
75
+ attr_reader :listeners
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,96 @@
1
+ require "erb"
2
+
3
+ module LaunchDarkly
4
+ module Impl
5
+ module Context
6
+ ERR_KIND_NON_STRING = 'context kind must be a string'
7
+ ERR_KIND_CANNOT_BE_KIND = '"kind" is not a valid context kind'
8
+ ERR_KIND_CANNOT_BE_MULTI = '"multi" is not a valid context kind'
9
+ ERR_KIND_INVALID_CHARS = 'context kind contains disallowed characters'
10
+
11
+ ERR_KEY_NON_STRING = 'context key must be a string'
12
+ ERR_KEY_EMPTY = 'context key must not be empty'
13
+
14
+ ERR_NAME_NON_STRING = 'context name must be a string'
15
+
16
+ ERR_ANONYMOUS_NON_BOOLEAN = 'context anonymous must be a boolean'
17
+
18
+ #
19
+ # We allow consumers of this SDK to provide us with either a Hash or an
20
+ # instance of an LDContext. This is convenient for them but not as much
21
+ # for us. To make the conversion slightly more convenient for us, we have
22
+ # created this method.
23
+ #
24
+ # @param context [Hash, LDContext]
25
+ # @return [LDContext]
26
+ #
27
+ def self.make_context(context)
28
+ return context if context.is_a?(LDContext)
29
+
30
+ LDContext.create(context)
31
+ end
32
+
33
+ #
34
+ # Returns an error message if the kind is invalid; nil otherwise.
35
+ #
36
+ # @param kind [any]
37
+ # @return [String, nil]
38
+ #
39
+ def self.validate_kind(kind)
40
+ return ERR_KIND_NON_STRING unless kind.is_a?(String)
41
+ return ERR_KIND_CANNOT_BE_KIND if kind == "kind"
42
+ return ERR_KIND_CANNOT_BE_MULTI if kind == "multi"
43
+ ERR_KIND_INVALID_CHARS unless kind.match?(/^[\w.-]+$/)
44
+ end
45
+
46
+ #
47
+ # Returns an error message if the key is invalid; nil otherwise.
48
+ #
49
+ # @param key [any]
50
+ # @return [String, nil]
51
+ #
52
+ def self.validate_key(key)
53
+ return ERR_KEY_NON_STRING unless key.is_a?(String)
54
+ ERR_KEY_EMPTY if key == ""
55
+ end
56
+
57
+ #
58
+ # Returns an error message if the name is invalid; nil otherwise.
59
+ #
60
+ # @param name [any]
61
+ # @return [String, nil]
62
+ #
63
+ def self.validate_name(name)
64
+ ERR_NAME_NON_STRING unless name.nil? || name.is_a?(String)
65
+ end
66
+
67
+ #
68
+ # Returns an error message if anonymous is invalid; nil otherwise.
69
+ #
70
+ # @param anonymous [any]
71
+ # @param allow_nil [Boolean]
72
+ # @return [String, nil]
73
+ #
74
+ def self.validate_anonymous(anonymous, allow_nil)
75
+ return nil if anonymous.nil? && allow_nil
76
+ return nil if [true, false].include? anonymous
77
+
78
+ ERR_ANONYMOUS_NON_BOOLEAN
79
+ end
80
+
81
+ #
82
+ # @param kind [String]
83
+ # @param key [String]
84
+ # @return [String]
85
+ #
86
+ def self.canonicalize_key_for_kind(kind, key)
87
+ # When building a FullyQualifiedKey, ':' and '%' are percent-escaped;
88
+ # we do not use a full URL-encoding function because implementations of
89
+ # this are inconsistent across platforms.
90
+ encoded = key.gsub("%", "%25").gsub(":", "%3A")
91
+
92
+ "#{kind}:#{encoded}"
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,166 @@
1
+ module LaunchDarkly
2
+ module Impl
3
+ class ContextFilter
4
+ #
5
+ # @param all_attributes_private [Boolean]
6
+ # @param private_attributes [Array<String>]
7
+ #
8
+ def initialize(all_attributes_private, private_attributes)
9
+ @all_attributes_private = all_attributes_private
10
+
11
+ @private_attributes = []
12
+ private_attributes.each do |attribute|
13
+ reference = LaunchDarkly::Reference.create(attribute)
14
+ @private_attributes << reference if reference.error.nil?
15
+ end
16
+ end
17
+
18
+ #
19
+ # Return a hash representation of the provided context with attribute
20
+ # redaction applied.
21
+ #
22
+ # @param context [LaunchDarkly::LDContext]
23
+ # @return [Hash]
24
+ #
25
+ def filter(context)
26
+ internal_filter(context, false)
27
+ end
28
+
29
+ #
30
+ # Return a hash representation of the provided context with attribute
31
+ # redaction applied.
32
+ #
33
+ # If a context is anonyomous, all attributes will be redacted except
34
+ # for key, kind, and anonymous.
35
+ #
36
+ # @param context [LaunchDarkly::LDContext]
37
+ # @return [Hash]
38
+ #
39
+ def filter_redact_anonymous(context)
40
+ internal_filter(context, true)
41
+ end
42
+
43
+ private def internal_filter(context, redact_anonymous)
44
+ return filter_single_context(context, true, redact_anonymous) unless context.multi_kind?
45
+
46
+ filtered = {kind: 'multi'}
47
+ (0...context.individual_context_count).each do |i|
48
+ c = context.individual_context(i)
49
+ next if c.nil?
50
+
51
+ filtered[c.kind] = filter_single_context(c, false, redact_anonymous)
52
+ end
53
+
54
+ filtered
55
+ end
56
+
57
+ #
58
+ # Apply redaction rules for a single context.
59
+ #
60
+ # @param context [LaunchDarkly::LDContext]
61
+ # @param include_kind [Boolean]
62
+ # @return [Hash]
63
+ #
64
+ private def filter_single_context(context, include_kind, redact_anonymous)
65
+ filtered = {key: context.key}
66
+
67
+ filtered[:kind] = context.kind if include_kind
68
+
69
+ anonymous = context.get_value(:anonymous)
70
+ filtered[:anonymous] = true if anonymous
71
+
72
+ redacted = []
73
+ private_attributes = @private_attributes.concat(context.private_attributes)
74
+
75
+ name = context.get_value(:name)
76
+ if !name.nil? && !check_whole_attribute_private(:name, private_attributes, redacted, anonymous && redact_anonymous)
77
+ filtered[:name] = name
78
+ end
79
+
80
+ context.get_custom_attribute_names.each do |attribute|
81
+ unless check_whole_attribute_private(attribute, private_attributes, redacted, anonymous && redact_anonymous)
82
+ value = context.get_value(attribute)
83
+ filtered[attribute] = redact_json_value(nil, attribute, value, private_attributes, redacted)
84
+ end
85
+ end
86
+
87
+ filtered[:_meta] = {redactedAttributes: redacted} unless redacted.empty?
88
+
89
+ filtered
90
+ end
91
+
92
+ #
93
+ # Check if an entire attribute should be redacted.
94
+ #
95
+ # @param attribute [Symbol]
96
+ # @param private_attributes [Array<Reference>]
97
+ # @param redacted [Array<Symbol>]
98
+ # @param redact_all [Boolean]
99
+ # @return [Boolean]
100
+ #
101
+ private def check_whole_attribute_private(attribute, private_attributes, redacted, redact_all)
102
+ if @all_attributes_private || redact_all
103
+ redacted << attribute
104
+ return true
105
+ end
106
+
107
+ private_attributes.each do |private_attribute|
108
+ if private_attribute.component(0) == attribute && private_attribute.depth == 1
109
+ redacted << attribute
110
+ return true
111
+ end
112
+ end
113
+
114
+ false
115
+ end
116
+
117
+ #
118
+ # Apply redaction rules to the provided value.
119
+ #
120
+ # @param parent_path [Array<String>, nil]
121
+ # @param name [String]
122
+ # @param value [any]
123
+ # @param private_attributes [Array<Reference>]
124
+ # @param redacted [Array<Symbol>]
125
+ # @return [any]
126
+ #
127
+ private def redact_json_value(parent_path, name, value, private_attributes, redacted)
128
+ return value unless value.is_a?(Hash)
129
+
130
+ ret = {}
131
+ current_path = parent_path.clone || []
132
+ current_path << name
133
+
134
+ value.each do |k, v|
135
+ was_redacted = false
136
+ private_attributes.each do |private_attribute|
137
+ next unless private_attribute.depth == (current_path.count + 1)
138
+
139
+ component = private_attribute.component(current_path.count)
140
+ next unless component == k
141
+
142
+ match = true
143
+ (0...current_path.count).each do |i|
144
+ unless private_attribute.component(i) == current_path[i]
145
+ match = false
146
+ break
147
+ end
148
+ end
149
+
150
+ if match
151
+ redacted << private_attribute.raw_path.to_sym
152
+ was_redacted = true
153
+ break
154
+ end
155
+ end
156
+
157
+ unless was_redacted
158
+ ret[k] = redact_json_value(current_path, k, v, private_attributes, redacted)
159
+ end
160
+ end
161
+
162
+ ret
163
+ end
164
+ end
165
+ end
166
+ end